Compare commits
18
Commits
aadecbfe44
..
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc1a123fa | ||
|
|
672cb07547 | ||
|
|
e3b2d33a11 | ||
|
|
2a40696292 | ||
|
|
80ee714e91 | ||
|
|
2c572c3250 | ||
|
|
0912a8e40c | ||
|
|
0d17621180 | ||
|
|
0ae6d6fc55 | ||
|
|
9300da258e | ||
|
|
298cbbf424 | ||
|
|
b19b630a77 | ||
|
|
75f0f2071d | ||
|
|
bfea718547 | ||
|
|
5505ac19ba | ||
|
|
9d2dc281ec | ||
|
|
05c8851143 | ||
|
|
71b25bff49 |
@@ -2,7 +2,7 @@ name: Rebuild CI Image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 6 * * 1'
|
- cron: "0 6 * * 1"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: node:26-alpine
|
image: node:26-alpine
|
||||||
env:
|
env:
|
||||||
IMAGE: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
IMAGE: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run: apk add --no-cache docker-cli
|
run: apk add --no-cache docker-cli
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.nl --username aramjonghu --password-stdin
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
||||||
|
|||||||
@@ -4,10 +4,43 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
fmt:
|
||||||
|
runs-on: alpine
|
||||||
|
container:
|
||||||
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
|
||||||
|
-not -path './build/*' \
|
||||||
|
-exec clang-format -i --style=file {} +
|
||||||
|
git diff --exit-code && echo "clang-format: passed"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container:
|
container:
|
||||||
image: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure
|
||||||
|
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ninja -C build
|
||||||
|
|
||||||
|
clang-tidy:
|
||||||
|
runs-on: alpine
|
||||||
|
container:
|
||||||
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Lint & Format (JS/TS)
|
name: JS/TS
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-format:
|
fmt:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ jobs:
|
|||||||
git
|
git
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||||
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
|
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
|
||||||
@@ -26,6 +25,19 @@ jobs:
|
|||||||
echo "No JS/TS files found"
|
echo "No JS/TS files found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git
|
||||||
|
|
||||||
- name: ESLint
|
- name: ESLint
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
name: Lint & Format (Rust)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint-format:
|
|
||||||
runs-on: alpine
|
|
||||||
container: node:26-alpine
|
|
||||||
env:
|
|
||||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Cache cargo packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
env:
|
|
||||||
cache-name: cache-cargo-packages
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.cargo/registry
|
|
||||||
.cargo/git
|
|
||||||
target
|
|
||||||
key: rust-${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
rust-${{ runner.os }}-build-
|
|
||||||
rust-
|
|
||||||
|
|
||||||
- name: Install tools
|
|
||||||
run: |
|
|
||||||
apk add --no-cache \
|
|
||||||
git \
|
|
||||||
cargo \
|
|
||||||
rust \
|
|
||||||
rustfmt \
|
|
||||||
rust-clippy
|
|
||||||
|
|
||||||
- id: format-check
|
|
||||||
name: Format check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
|
||||||
status=0
|
|
||||||
for manifest in $(find . -name "Cargo.toml"); do
|
|
||||||
cargo fmt --manifest-path "$manifest" --check && \
|
|
||||||
echo "$manifest: formatting OK" || \
|
|
||||||
{ echo "$manifest: needs formatting"; status=1; }
|
|
||||||
done
|
|
||||||
exit $status
|
|
||||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
|
||||||
echo "Rust files found but no Cargo.toml"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "No Rust project found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- id: clippy
|
|
||||||
name: Clippy
|
|
||||||
run: |
|
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
|
||||||
status=0
|
|
||||||
for manifest in $(find . -name "Cargo.toml"); do
|
|
||||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
|
||||||
echo "$manifest: Clippy passed" || \
|
|
||||||
{ echo "$manifest: Clippy failed"; status=1; }
|
|
||||||
done
|
|
||||||
exit $status
|
|
||||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
|
||||||
echo "Rust files found but no Cargo.toml"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "No Rust project found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check results
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.format-check.outcome }}" = "failure" ] || [ "${{ steps.clippy.outcome }}" = "failure" ]; then
|
|
||||||
echo "One or more checks failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "All checks passed"
|
|
||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-format:
|
fmt:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|
||||||
@@ -23,11 +23,28 @@ jobs:
|
|||||||
pip install --no-cache-dir ruff
|
pip install --no-cache-dir ruff
|
||||||
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
ruff format --check .
|
ruff format --check .
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
py3-pip
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install --no-cache-dir ruff
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
@@ -63,3 +80,30 @@ jobs:
|
|||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
cd cli
|
cd cli
|
||||||
python -m pytest tests/ -v
|
python -m pytest tests/ -v
|
||||||
|
|
||||||
|
buildcheck:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
py3-pip \
|
||||||
|
build-base \
|
||||||
|
python3-dev \
|
||||||
|
gcc \
|
||||||
|
g++
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install --no-cache-dir nuitka
|
||||||
|
|
||||||
|
- name: Nuitka module check
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
|
nuitka --module --include-package=zshell cli/src/zshell/
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-build-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust
|
||||||
|
|
||||||
|
- name: Cargo check
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo check --manifest-path "$manifest" && \
|
||||||
|
echo "$manifest: check passed" || \
|
||||||
|
{ echo "$manifest: check failed"; exit 1; }
|
||||||
|
done
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust \
|
||||||
|
rustfmt
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
status=0
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo fmt --manifest-path "$manifest" --check && \
|
||||||
|
echo "$manifest: formatting OK" || \
|
||||||
|
{ echo "$manifest: needs formatting"; status=1; }
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust \
|
||||||
|
rust-clippy
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
status=0
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||||
|
echo "$manifest: Clippy passed" || \
|
||||||
|
{ echo "$manifest: Clippy failed"; status=1; }
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
@@ -15,4 +15,3 @@ dist/
|
|||||||
**/target/
|
**/target/
|
||||||
**/test-plugins/
|
**/test-plugins/
|
||||||
**/Charts/
|
**/Charts/
|
||||||
**/network-dev/
|
|
||||||
|
|||||||
+89
-32
@@ -36,13 +36,15 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(ENABLE_MODULES "plugin;shell" CACHE STRING "Modules to build/install")
|
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
||||||
|
|
||||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
||||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
||||||
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
||||||
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
||||||
|
|
||||||
|
set(CMAKE_INSTALL_MESSAGE NEVER)
|
||||||
|
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||||
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
||||||
@@ -72,48 +74,84 @@ if("shell" IN_LIST ENABLE_MODULES)
|
|||||||
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
||||||
set(ZSHELL_CLI_SRC "${CMAKE_SOURCE_DIR}/cli/src/zshell")
|
set(ZSHELL_CLI_SRC "${CMAKE_SOURCE_DIR}/cli/src/zshell")
|
||||||
|
|
||||||
find_program(NUITKA_EXECUTABLE nuitka REQUIRED)
|
find_program(NUITKA_EXECUTABLE nuitka)
|
||||||
|
|
||||||
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
if(NUITKA_EXECUTABLE)
|
||||||
"${ZSHELL_CLI_SRC}/*.py"
|
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||||
)
|
"${ZSHELL_CLI_SRC}/*.py"
|
||||||
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
)
|
||||||
"${ZSHELL_CLI_SRC}/assets/*"
|
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||||
)
|
"${ZSHELL_CLI_SRC}/assets/*"
|
||||||
|
)
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli"
|
OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli"
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}"
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}"
|
||||||
|
|
||||||
COMMAND
|
COMMAND
|
||||||
${NUITKA_EXECUTABLE}
|
${NUITKA_EXECUTABLE}
|
||||||
--standalone
|
--standalone
|
||||||
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
||||||
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
||||||
--output-filename=zshell-cli
|
--output-filename=zshell-cli
|
||||||
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
||||||
|
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
|
||||||
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli")
|
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli")
|
||||||
|
|
||||||
install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli")
|
install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli")
|
||||||
install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE)
|
install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE)
|
||||||
|
|
||||||
configure_file(
|
configure_file(
|
||||||
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in"
|
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in"
|
||||||
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake"
|
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake"
|
||||||
@ONLY
|
@ONLY
|
||||||
)
|
)
|
||||||
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake")
|
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake")
|
||||||
|
else()
|
||||||
|
message(STATUS "Nuitka not found, building zshell-cli as a Python wheel instead")
|
||||||
|
|
||||||
|
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||||
|
|
||||||
|
set(ZSHELL_CLI_DIR "${CMAKE_SOURCE_DIR}")
|
||||||
|
set(ZSHELL_CLI_DIST_DIR "${ZSHELL_CLI_DIR}/dist")
|
||||||
|
set(ZSHELL_CLI_WHEEL_STAMP "${CMAKE_BINARY_DIR}/zshell-cli/wheel.stamp")
|
||||||
|
|
||||||
|
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||||
|
"${ZSHELL_CLI_DIR}/src/zshell/*.py"
|
||||||
|
)
|
||||||
|
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||||
|
"${ZSHELL_CLI_DIR}/src/zshell/assets/*"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${ZSHELL_CLI_WHEEL_STAMP}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/zshell-cli"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST_DIR}"
|
||||||
|
COMMAND ${Python3_EXECUTABLE} -m build --wheel --no-isolation
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E touch "${ZSHELL_CLI_WHEEL_STAMP}"
|
||||||
|
WORKING_DIRECTORY "${ZSHELL_CLI_DIR}"
|
||||||
|
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||||
|
COMMENT "Building zshell-cli python wheel"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_WHEEL_STAMP}")
|
||||||
|
|
||||||
|
configure_file(
|
||||||
|
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli-python.cmake.in"
|
||||||
|
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli-python.cmake"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli-python.cmake")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if("plugin" IN_LIST ENABLE_MODULES)
|
if("plugin" IN_LIST ENABLE_MODULES)
|
||||||
add_subdirectory(Plugins)
|
add_subdirectory(Plugins)
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if("shell" IN_LIST ENABLE_MODULES)
|
if("shell" IN_LIST ENABLE_MODULES)
|
||||||
@@ -130,3 +168,22 @@ if("shell" IN_LIST ENABLE_MODULES)
|
|||||||
# Greeter
|
# Greeter
|
||||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if("m3shapes" IN_LIST ENABLE_MODULES)
|
||||||
|
message(STATUS "Fetching M3Shapes module")
|
||||||
|
include(FetchContent)
|
||||||
|
set(M3SHAPES_REV bdc327b29f95394a732baf3c9b19658ba23755b6)
|
||||||
|
FetchContent_Declare(
|
||||||
|
m3shapes_external
|
||||||
|
GIT_REPOSITORY https://github.com/soramanew/m3shapes.git
|
||||||
|
GIT_TAG ${M3SHAPES_REV}
|
||||||
|
SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/m3shapes-${M3SHAPES_REV}"
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(m3shapes_external)
|
||||||
|
message(STATUS "Done fetching M3Shapes module")
|
||||||
|
|
||||||
|
# Fix m3shapes wrong rpath
|
||||||
|
if(TARGET m3shapesplugin)
|
||||||
|
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ListView {
|
|||||||
|
|
||||||
property bool doneFakeFlick
|
property bool doneFakeFlick
|
||||||
|
|
||||||
interactive: !Visibilities.getForActive().isDrawing
|
interactive: !Visibilities.getForActive()?.isDrawing
|
||||||
maximumFlickVelocity: 3000
|
maximumFlickVelocity: 3000
|
||||||
|
|
||||||
rebound: Transition {
|
rebound: Transition {
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import M3Shapes
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
MaterialShape {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool animated: true
|
||||||
|
property real cRotation
|
||||||
|
property bool containsIcon
|
||||||
|
property real dampingRatio: 0.6
|
||||||
|
property real lRotation
|
||||||
|
property int morphAnimRotation: 60
|
||||||
|
property real morphScale: 0.14
|
||||||
|
property alias rotateAnimDuration: rotateAnim.duration
|
||||||
|
property int shapeIndex
|
||||||
|
property list<int> shapes: {
|
||||||
|
if (containsIcon)
|
||||||
|
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||||
|
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||||
|
}
|
||||||
|
readonly property real springDuration: {
|
||||||
|
const wn = Math.sqrt(stiffness);
|
||||||
|
const r = -dampingRatio * wn;
|
||||||
|
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||||
|
return Math.log(visibilityThreshold / c) / r;
|
||||||
|
}
|
||||||
|
readonly property real springMaxVelocity: {
|
||||||
|
const wn = Math.sqrt(stiffness);
|
||||||
|
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
||||||
|
return wn * factor;
|
||||||
|
}
|
||||||
|
property bool springSettled: true
|
||||||
|
property real stiffness: 180
|
||||||
|
property real thisLRotation
|
||||||
|
property real visibilityThreshold: 0.075
|
||||||
|
|
||||||
|
function spring(t: real): var {
|
||||||
|
const wn = Math.sqrt(stiffness);
|
||||||
|
const za = dampingRatio * wn;
|
||||||
|
|
||||||
|
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||||
|
const r = za / wd;
|
||||||
|
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
||||||
|
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
||||||
|
|
||||||
|
return [pos, vel];
|
||||||
|
}
|
||||||
|
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
implicitSize: 38
|
||||||
|
toShape: shapes[0]
|
||||||
|
|
||||||
|
RotationAnimation on cRotation {
|
||||||
|
id: rotateAnim
|
||||||
|
|
||||||
|
duration: 4666
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
from: 0
|
||||||
|
loops: Animation.Infinite
|
||||||
|
running: root.animated
|
||||||
|
to: 360
|
||||||
|
}
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElapsedTimer {
|
||||||
|
id: timer
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameAnimation {
|
||||||
|
running: root.animated && !root.springSettled
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
const t = timer.elapsed();
|
||||||
|
|
||||||
|
if (t >= root.springDuration) {
|
||||||
|
root.springSettled = true;
|
||||||
|
} else {
|
||||||
|
const [pos, vel] = root.spring(t);
|
||||||
|
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
||||||
|
root.thisLRotation = pos * root.morphAnimRotation;
|
||||||
|
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 650
|
||||||
|
repeat: true
|
||||||
|
running: root.animated
|
||||||
|
triggeredOnStart: true
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.beginBatchUpdate();
|
||||||
|
root.fromShape = root.toShape;
|
||||||
|
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
||||||
|
root.toShape = root.shapes[root.shapeIndex];
|
||||||
|
root.morphProgress = 0;
|
||||||
|
|
||||||
|
root.rotation = root.rotation;
|
||||||
|
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
||||||
|
root.thisLRotation = 0;
|
||||||
|
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
||||||
|
|
||||||
|
root.springSettled = false;
|
||||||
|
timer.restart();
|
||||||
|
root.endBatchUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ MouseArea {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||||
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
|
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
Behavior on stateOpacity {
|
Behavior on stateOpacity {
|
||||||
|
|||||||
@@ -6,25 +6,55 @@ import qs.Effects
|
|||||||
CustomListView {
|
CustomListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||||
property real fadeAmount: 0.1
|
property real fadeAmount: 0.1
|
||||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||||
|
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||||
|
|
||||||
function fadeShouldBeActive(isStart: bool): bool {
|
function contentSize(): real {
|
||||||
// When content is smaller than flickable size, hide fade when rebound starts
|
return horizontal ? contentWidth : contentHeight;
|
||||||
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (isStart)
|
|
||||||
return visibleArea.yPosition > 0;
|
|
||||||
return visibleArea.yPosition + visibleArea.heightRatio < 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
function fadeShouldBeActive(isStart: bool): bool {
|
||||||
layer.enabled: true
|
// When content is smaller than flickable size, hide fade when rebound starts.
|
||||||
orientation: ListView.Vertical
|
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on bottomFadeOpacity {
|
if (isStart)
|
||||||
|
return visibleStart() > 0;
|
||||||
|
|
||||||
|
return visibleStart() + visibleRatio() < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginEnd(): real {
|
||||||
|
return horizontal ? rightMargin : bottomMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginStart(): real {
|
||||||
|
return horizontal ? leftMargin : topMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function overshootStart(): real {
|
||||||
|
return horizontal ? horizontalOvershoot : verticalOvershoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewportSize(): real {
|
||||||
|
return horizontal ? width : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleRatio(): real {
|
||||||
|
return horizontal ? visibleArea.widthRatio : visibleArea.heightRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleStart(): real {
|
||||||
|
return horizontal ? visibleArea.xPosition : visibleArea.yPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
flickableDirection: horizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
|
Behavior on endFadeOpacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
@@ -40,10 +70,10 @@ CustomListView {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
orientation: Gradient.Vertical
|
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
||||||
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
||||||
position: 0
|
position: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +88,13 @@ CustomListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
||||||
position: 1
|
position: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on topFadeOpacity {
|
Behavior on startFadeOpacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property real amplitude: 3
|
||||||
|
property color color: DynamicColors.palette.m3surfaceContainer
|
||||||
|
readonly property real waveHeight: amplitude * 2
|
||||||
|
property int waves: 4
|
||||||
|
|
||||||
|
asynchronous: true
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
fillColor: root.color
|
||||||
|
strokeColor: "transparent"
|
||||||
|
strokeWidth: 0
|
||||||
|
|
||||||
|
Behavior on fillColor {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathSvg {
|
||||||
|
path: {
|
||||||
|
const w = root.width;
|
||||||
|
const h = root.height;
|
||||||
|
const a = root.amplitude;
|
||||||
|
const n = Math.max(1, root.waves);
|
||||||
|
const wl = w / n;
|
||||||
|
const half = wl / 2;
|
||||||
|
|
||||||
|
let d = `M 0,${a} `;
|
||||||
|
for (let i = 0; i < n; ++i) {
|
||||||
|
const x = i * wl;
|
||||||
|
d += `Q ${x + half / 2},${-a} ${x + half},${a} `;
|
||||||
|
d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `;
|
||||||
|
}
|
||||||
|
d += `L ${w},${h} L 0,${h} Z`;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,10 @@ JsonObject {
|
|||||||
id: "tray",
|
id: "tray",
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "network",
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "clock",
|
id: "clock",
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -311,17 +311,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUtilitiesChanged() {
|
|
||||||
if (root.visibilities.utilities) {
|
|
||||||
const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);
|
|
||||||
if (!inUtilitiesArea) {
|
|
||||||
root.utilitiesShortcutActive = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.utilitiesShortcutActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root.visibilities
|
target: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ Region {
|
|||||||
R {
|
R {
|
||||||
panel: root.panels.osdWrapper
|
panel: root.panels.osdWrapper
|
||||||
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
||||||
x: root.win.width - width
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R {
|
R {
|
||||||
|
|||||||
+1
-9
@@ -26,7 +26,7 @@ CustomWindow {
|
|||||||
if (focusGrab.active)
|
if (focusGrab.active)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
|
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace?.lastIpcObject.windows > 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return 100;
|
return 100;
|
||||||
@@ -162,14 +162,6 @@ CustomWindow {
|
|||||||
Component.onCompleted: Visibilities.load(root.screen, this)
|
Component.onCompleted: Visibilities.load(root.screen, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function toggleLauncher(fix: string): void {
|
|
||||||
visibilities.launcher = !visibilities.launcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "visibilities"
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
property: "bar"
|
property: "bar"
|
||||||
target: visibilities
|
target: visibilities
|
||||||
|
|||||||
+15
-42
@@ -11,8 +11,13 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool appleDisplayPresent: false
|
property bool appleDisplayPresent: false
|
||||||
|
readonly property var ddcMonitorMap: {
|
||||||
|
const map = {};
|
||||||
|
for (const m of ddcMonitors)
|
||||||
|
map[m.connector] = m;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
property list<var> ddcMonitors: []
|
property list<var> ddcMonitors: []
|
||||||
property list<var> ddcServiceMon: []
|
|
||||||
readonly property list<Monitor> monitors: variants.instances
|
readonly property list<Monitor> monitors: variants.instances
|
||||||
|
|
||||||
function decreaseBrightness(): void {
|
function decreaseBrightness(): void {
|
||||||
@@ -56,8 +61,6 @@ Singleton {
|
|||||||
|
|
||||||
onMonitorsChanged: {
|
onMonitorsChanged: {
|
||||||
ddcMonitors = [];
|
ddcMonitors = [];
|
||||||
ddcServiceMon = [];
|
|
||||||
ddcServiceProc.running = true;
|
|
||||||
ddcProc.running = true;
|
ddcProc.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,26 +95,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcServiceProc
|
|
||||||
|
|
||||||
command: ["ddcutil-client", "detect"]
|
|
||||||
|
|
||||||
// running: true
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const t = text.replace(/\r\n/g, "\n").trim();
|
|
||||||
|
|
||||||
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
|
|
||||||
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
|
|
||||||
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
|
|
||||||
})).filter(d => d.display > 0);
|
|
||||||
root.ddcServiceMon = output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Increase brightness"
|
description: "Increase brightness"
|
||||||
name: "brightnessUp"
|
name: "brightnessUp"
|
||||||
@@ -183,16 +166,12 @@ Singleton {
|
|||||||
id: monitor
|
id: monitor
|
||||||
|
|
||||||
property real brightness
|
property real brightness
|
||||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
readonly property string busNum: ddcInfo?.busNum ?? ""
|
||||||
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
|
readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null
|
||||||
readonly property Process initProc: Process {
|
readonly property Process initProc: Process {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (monitor.isDdcService) {
|
if (monitor.isAppleDisplay) {
|
||||||
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
|
|
||||||
const val = parseInt(output.trim());
|
|
||||||
monitor.brightness = val / 100;
|
|
||||||
} else if (monitor.isAppleDisplay) {
|
|
||||||
const val = parseInt(text.trim());
|
const val = parseInt(text.trim());
|
||||||
monitor.brightness = val / 101;
|
monitor.brightness = val / 101;
|
||||||
} else {
|
} else {
|
||||||
@@ -203,12 +182,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
|
readonly property bool isDdc: ddcInfo !== null
|
||||||
readonly property bool isDdcService: Config.services.ddcutilService
|
|
||||||
required property ShellScreen modelData
|
required property ShellScreen modelData
|
||||||
property real queuedBrightness: NaN
|
property real queuedBrightness: NaN
|
||||||
readonly property Timer timer: Timer {
|
readonly property Timer timer: Timer {
|
||||||
interval: 500
|
interval: 400
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!isNaN(monitor.queuedBrightness)) {
|
if (!isNaN(monitor.queuedBrightness)) {
|
||||||
@@ -219,9 +197,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initBrightness(): void {
|
function initBrightness(): void {
|
||||||
if (isDdcService)
|
if (isAppleDisplay)
|
||||||
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
|
|
||||||
else if (isAppleDisplay)
|
|
||||||
initProc.command = ["asdbctl", "get"];
|
initProc.command = ["asdbctl", "get"];
|
||||||
else if (isDdc)
|
else if (isDdc)
|
||||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||||
@@ -237,28 +213,25 @@ Singleton {
|
|||||||
if (Math.round(brightness * 100) === rounded)
|
if (Math.round(brightness * 100) === rounded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ((isDdc || isDdcService) && timer.running) {
|
if (isDdc && timer.running) {
|
||||||
queuedBrightness = value;
|
queuedBrightness = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
brightness = value;
|
brightness = value;
|
||||||
|
|
||||||
if (isDdcService)
|
if (isAppleDisplay)
|
||||||
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
|
|
||||||
else if (isAppleDisplay)
|
|
||||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||||
else if (isDdc)
|
else if (isDdc)
|
||||||
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
||||||
else
|
else
|
||||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||||
|
|
||||||
if (isDdc || isDdcService)
|
if (isDdc)
|
||||||
timer.restart();
|
timer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: initBrightness()
|
Component.onCompleted: initBrightness()
|
||||||
onBusNumChanged: initBrightness()
|
onBusNumChanged: initBrightness()
|
||||||
onDisplayNumChanged: initBrightness()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Networking
|
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool active: false
|
|
||||||
readonly property list<Network> connectedNetworks: networks.filter(n => n.connected)
|
|
||||||
readonly property list<NetworkDevice> devices: Networking.devices.values
|
|
||||||
readonly property list<Network> knownNetworks: networks.filter(n => n.known && !n.known.ConnectionState.Connected)
|
|
||||||
readonly property list<Network> networks: {
|
|
||||||
const list = [];
|
|
||||||
for (const d of wifiDevices) {
|
|
||||||
for (const n of d.networks.values) {
|
|
||||||
if (!list.includes(n))
|
|
||||||
list.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
property bool scanning: false
|
|
||||||
readonly property list<Network> unknownNetworks: networks.filter(n => !n.known)
|
|
||||||
readonly property list<WifiDevice> wifiDevices: devices.filter(d => wifiDevice(d))
|
|
||||||
readonly property bool wifiEnabled: Networking.wifiEnabled
|
|
||||||
|
|
||||||
function isSecure(security): bool {
|
|
||||||
return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScan(value: bool): void {
|
|
||||||
for (const d of wifiDevices) {
|
|
||||||
d.scannerEnabled = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWifi(value: bool): void {
|
|
||||||
Networking.wifiEnabled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function wifiDevice(dev): bool {
|
|
||||||
return dev.type === DeviceType.Wifi;
|
|
||||||
}
|
|
||||||
|
|
||||||
onActiveChanged: {
|
|
||||||
for (const d of wifiDevices)
|
|
||||||
d.scannerEnabled = active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+31
-3
@@ -1,12 +1,12 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQml
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import QtQml
|
|
||||||
import ZShell
|
import ZShell
|
||||||
import qs.Config
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -15,7 +15,24 @@ Singleton {
|
|||||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||||
property alias manualActive: props.manualActive
|
property alias manualActive: props.manualActive
|
||||||
|
|
||||||
|
function getArtUrl(player: MprisPlayer): string {
|
||||||
|
if (!player)
|
||||||
|
return "";
|
||||||
|
if (player.trackArtUrl)
|
||||||
|
return player.trackArtUrl;
|
||||||
|
|
||||||
|
const url = player.metadata["xesam:url"] ?? "";
|
||||||
|
if (url.startsWith("https://www.youtube.com/watch")) {
|
||||||
|
// Fallback for youtube
|
||||||
|
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
|
||||||
|
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function getIdentity(player: MprisPlayer): string {
|
function getIdentity(player: MprisPlayer): string {
|
||||||
|
if (!player)
|
||||||
|
return "";
|
||||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||||
return alias?.to ?? player.identity;
|
return alias?.to ?? player.identity;
|
||||||
}
|
}
|
||||||
@@ -25,9 +42,12 @@ Singleton {
|
|||||||
if (!Config.utilities.toasts.nowPlaying) {
|
if (!Config.utilities.toasts.nowPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
|
||||||
|
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: active
|
target: root.active
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistentProperties {
|
PersistentProperties {
|
||||||
@@ -38,8 +58,10 @@ Singleton {
|
|||||||
reloadableId: "players"
|
reloadableId: "players"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Toggle media playback"
|
description: "Toggle media playback"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaToggle"
|
name: "mediaToggle"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -49,8 +71,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Previous track"
|
description: "Previous track"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaPrev"
|
name: "mediaPrev"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -60,8 +84,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Next track"
|
description: "Next track"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaNext"
|
name: "mediaNext"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -71,8 +97,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Stop media playback"
|
description: "Stop media playback"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaStop"
|
name: "mediaStop"
|
||||||
|
|
||||||
onPressed: root.active?.stop()
|
onPressed: root.active?.stop()
|
||||||
|
|||||||
+107
-32
@@ -1,7 +1,7 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import ZShell
|
import ZShell
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
@@ -12,15 +12,15 @@ Singleton {
|
|||||||
property var cc
|
property var cc
|
||||||
property string city
|
property string city
|
||||||
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
||||||
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
|
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
|
||||||
property list<var> forecast
|
property list<var> forecast
|
||||||
property list<var> hourlyForecast
|
property list<var> hourlyForecast
|
||||||
readonly property int humidity: cc?.humidity ?? 0
|
readonly property int humidity: cc?.humidity ?? 0
|
||||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||||
property string loc
|
property string loc
|
||||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||||
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
readonly property string temp: formatTemp(cc?.tempC)
|
||||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||||
|
|
||||||
function fetchCityFromCoords(coords: string): void {
|
function fetchCityFromCoords(coords: string): void {
|
||||||
@@ -29,29 +29,48 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lat, lon] = coords.split(",");
|
const [lat, lon] = coords.split(",").map(s => s.trim());
|
||||||
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||||
Requests.get(url, text => {
|
|
||||||
|
const fallbackToBigDataCloud = () => {
|
||||||
|
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
|
||||||
|
Requests.get(fallbackUrl, text => {
|
||||||
|
const geo = JSON.parse(text);
|
||||||
|
const geoCity = geo.city || geo.locality;
|
||||||
|
if (geoCity) {
|
||||||
|
city = fixCityName(geoCity);
|
||||||
|
cachedCities.set(coords, city);
|
||||||
|
} else {
|
||||||
|
city = "Unknown City";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
|
||||||
|
Requests.get(nominatimUrl, text => {
|
||||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||||
if (geo) {
|
if (geo) {
|
||||||
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
||||||
city = geoCity;
|
if (geoCity) {
|
||||||
cachedCities.set(coords, geoCity);
|
city = fixCityName(geoCity);
|
||||||
} else {
|
cachedCities.set(coords, city);
|
||||||
city = "Unknown City";
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
fallbackToBigDataCloud();
|
||||||
|
}, fallbackToBigDataCloud);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchCoordsFromCity(cityName: string): void {
|
function fetchCoordsFromCity(cityName: string): void {
|
||||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
|
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||||
|
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;
|
||||||
|
|
||||||
Requests.get(url, text => {
|
Requests.get(url, text => {
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
if (json.results && json.results.length > 0) {
|
if (json.results && json.results.length > 0) {
|
||||||
const result = json.results[0];
|
const result = json.results[0];
|
||||||
loc = result.latitude + "," + result.longitude;
|
loc = result.latitude + "," + result.longitude;
|
||||||
city = result.name;
|
city = fixCityName(result.name);
|
||||||
} else {
|
} else {
|
||||||
loc = "";
|
loc = "";
|
||||||
reload();
|
reload();
|
||||||
@@ -72,25 +91,21 @@ Singleton {
|
|||||||
cc = {
|
cc = {
|
||||||
weatherCode: json.current.weather_code,
|
weatherCode: json.current.weather_code,
|
||||||
weatherDesc: getWeatherCondition(json.current.weather_code),
|
weatherDesc: getWeatherCondition(json.current.weather_code),
|
||||||
tempC: Math.round(json.current.temperature_2m),
|
tempC: json.current.temperature_2m,
|
||||||
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
|
feelsLikeC: json.current.apparent_temperature,
|
||||||
feelsLikeC: Math.round(json.current.apparent_temperature),
|
|
||||||
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
|
|
||||||
humidity: json.current.relative_humidity_2m,
|
humidity: json.current.relative_humidity_2m,
|
||||||
windSpeed: json.current.wind_speed_10m,
|
windSpeed: json.current.wind_speed_10m,
|
||||||
isDay: json.current.is_day,
|
isDay: json.current.is_day,
|
||||||
sunrise: json.daily.sunrise[0],
|
sunrise: json.daily.sunrise[0].replace("T", " "),
|
||||||
sunset: json.daily.sunset[0]
|
sunset: json.daily.sunset[0].replace("T", " ")
|
||||||
};
|
};
|
||||||
|
|
||||||
const forecastList = [];
|
const forecastList = [];
|
||||||
for (let i = 0; i < json.daily.time.length; i++)
|
for (let i = 0; i < json.daily.time.length; i++)
|
||||||
forecastList.push({
|
forecastList.push({
|
||||||
date: json.daily.time[i],
|
date: json.daily.time[i].replace(/-/g, "/"),
|
||||||
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
maxTempC: json.daily.temperature_2m_max[i],
|
||||||
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
minTempC: json.daily.temperature_2m_min[i],
|
||||||
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
|
||||||
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
|
|
||||||
weatherCode: json.daily.weather_code[i],
|
weatherCode: json.daily.weather_code[i],
|
||||||
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
||||||
});
|
});
|
||||||
@@ -99,7 +114,8 @@ Singleton {
|
|||||||
const hourlyList = [];
|
const hourlyList = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (let i = 0; i < json.hourly.time.length; i++) {
|
for (let i = 0; i < json.hourly.time.length; i++) {
|
||||||
const time = new Date(json.hourly.time[i]);
|
const time = new Date(json.hourly.time[i].replace("T", " "));
|
||||||
|
|
||||||
if (time < now)
|
if (time < now)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -107,7 +123,7 @@ Singleton {
|
|||||||
timestamp: json.hourly.time[i],
|
timestamp: json.hourly.time[i],
|
||||||
hour: time.getHours(),
|
hour: time.getHours(),
|
||||||
tempC: Math.round(json.hourly.temperature_2m[i]),
|
tempC: Math.round(json.hourly.temperature_2m[i]),
|
||||||
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
|
precipChance: json.hourly.precipitation_probability[i],
|
||||||
weatherCode: json.hourly.weather_code[i],
|
weatherCode: json.hourly.weather_code[i],
|
||||||
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
||||||
});
|
});
|
||||||
@@ -116,6 +132,59 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fixCityName(cityName: string): string {
|
||||||
|
if (!cityName)
|
||||||
|
return "";
|
||||||
|
const mapping = {
|
||||||
|
// Polish
|
||||||
|
"Poznan": "Poznań",
|
||||||
|
"Wroclaw": "Wrocław",
|
||||||
|
"Krakow": "Kraków",
|
||||||
|
"Gdansk": "Gdańsk",
|
||||||
|
"Lodz": "Łódź",
|
||||||
|
"Rzeszow": "Rzeszów",
|
||||||
|
"Torun": "Toruń",
|
||||||
|
"Bialystok": "Białystok",
|
||||||
|
"Czestochowa": "Częstochowa",
|
||||||
|
"Plock": "Płock",
|
||||||
|
"Ruda Slaska": "Ruda Śląska",
|
||||||
|
"Dabrowa Gornicza": "Dąbrowa Górnicza",
|
||||||
|
"Elblag": "Elbląg",
|
||||||
|
"Gorzow Wielkopolski": "Gorzów Wielkopolski",
|
||||||
|
"Zielona Gora": "Zielona Góra",
|
||||||
|
"Slupsk": "Słupsk",
|
||||||
|
|
||||||
|
// German
|
||||||
|
"Munchen": "München",
|
||||||
|
"Koln": "Köln",
|
||||||
|
"Dusseldorf": "Düsseldorf",
|
||||||
|
"Nurnberg": "Nürnberg",
|
||||||
|
|
||||||
|
// French & Spanish & Portuguese
|
||||||
|
"Sao Paulo": "São Paulo",
|
||||||
|
"Montreal": "Montréal",
|
||||||
|
"Quebec": "Québec",
|
||||||
|
"Bogota": "Bogotá",
|
||||||
|
"Medellin": "Medellín",
|
||||||
|
"Cordoba": "Córdoba",
|
||||||
|
|
||||||
|
// Turkish
|
||||||
|
"Istanbul": "İstanbul",
|
||||||
|
"Izmir": "İzmir",
|
||||||
|
|
||||||
|
// Scandinavian & others
|
||||||
|
"Malmo": "Malmö",
|
||||||
|
"Goteborg": "Göteborg",
|
||||||
|
"Zurich": "Zürich",
|
||||||
|
"Geneve": "Genève"
|
||||||
|
};
|
||||||
|
return mapping[cityName] || cityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTemp(temp: var): string {
|
||||||
|
return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
|
||||||
|
}
|
||||||
|
|
||||||
function getWeatherCondition(code: string): string {
|
function getWeatherCondition(code: string): string {
|
||||||
const conditions = {
|
const conditions = {
|
||||||
"0": "Clear",
|
"0": "Clear",
|
||||||
@@ -154,9 +223,9 @@ Singleton {
|
|||||||
if (!loc || loc.indexOf(",") === -1)
|
if (!loc || loc.indexOf(",") === -1)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
const [lat, lon] = loc.split(",");
|
const [lat, lon] = loc.split(",").map(s => s.trim());
|
||||||
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
||||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||||
|
|
||||||
return baseUrl + "?" + params.join("&");
|
return baseUrl + "?" + params.join("&");
|
||||||
}
|
}
|
||||||
@@ -189,7 +258,14 @@ Singleton {
|
|||||||
|
|
||||||
onLocChanged: fetchWeatherData()
|
onLocChanged: fetchWeatherData()
|
||||||
|
|
||||||
// Refresh current location hourly
|
Connections {
|
||||||
|
function onWeatherLocationChanged(): void {
|
||||||
|
root.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Config.services
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 3600000 // 1 hour
|
interval: 3600000 // 1 hour
|
||||||
repeat: true
|
repeat: true
|
||||||
@@ -200,6 +276,5 @@ Singleton {
|
|||||||
|
|
||||||
ElapsedTimer {
|
ElapsedTimer {
|
||||||
id: timer
|
id: timer
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import QtQuick.Layouts
|
|||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Modules.SysTray
|
import qs.Modules.SysTray
|
||||||
|
import qs.Modules.Network
|
||||||
import qs.Modules.Updates
|
import qs.Modules.Updates
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -168,6 +169,15 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: "network"
|
||||||
|
|
||||||
|
delegate: WrappedLoader {
|
||||||
|
sourceComponent: NetworkWidget {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: "media"
|
roleValue: "media"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import qs.Config
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Modules.SysTray
|
import qs.Modules.SysTray
|
||||||
import qs.Modules.SysTray.Widgets
|
import qs.Modules.SysTray.Widgets
|
||||||
|
import qs.Modules.Network
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import QtQuick
|
|||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Modules.WSOverview
|
import qs.Modules.WSOverview
|
||||||
|
import qs.Modules.Network
|
||||||
import qs.Modules.SysTray.Popouts
|
import qs.Modules.SysTray.Popouts
|
||||||
import qs.Modules.Updates
|
import qs.Modules.Updates
|
||||||
|
|
||||||
|
|||||||
+31
-41
@@ -4,76 +4,66 @@ import qs.Components
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
RowLayout {
|
CustomClippingRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
spacing: Appearance.spacing.large * 2
|
implicitHeight: layout.implicitHeight
|
||||||
|
implicitWidth: layout.implicitWidth
|
||||||
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
ColumnLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
id: layout
|
||||||
spacing: Appearance.spacing.normal
|
|
||||||
|
|
||||||
CustomRect {
|
anchors.fill: parent
|
||||||
|
spacing: Appearance.spacing.large * 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
spacing: Appearance.spacing.normal
|
||||||
implicitHeight: weather.implicitHeight
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
topLeftRadius: Appearance.rounding.large
|
|
||||||
|
|
||||||
WeatherInfo {
|
WeatherInfo {
|
||||||
id: weather
|
id: weather
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
rootHeight: root.height
|
rootHeight: root.height
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
|
||||||
implicitHeight: resources.implicitHeight
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
Resources {
|
Resources {
|
||||||
id: resources
|
id: resources
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
CustomClippingRect {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
bottomLeftRadius: Appearance.rounding.large
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
Media {
|
Media {
|
||||||
id: media
|
id: media
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Center {
|
Center {
|
||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.normal
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
bottomRightRadius: Appearance.rounding.large
|
spacing: Appearance.spacing.normal
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
topRightRadius: Appearance.rounding.large
|
|
||||||
|
|
||||||
NotifDock {
|
CustomRect {
|
||||||
lock: root.lock
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
bottomRightRadius: Appearance.rounding.large
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
topRightRadius: Appearance.rounding.large
|
||||||
|
|
||||||
|
NotifDock {
|
||||||
|
lock: root.lock
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-145
@@ -1,201 +1,110 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Modules
|
import Quickshell
|
||||||
|
import ZShell.Components
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
Item {
|
CustomClippingRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
anchors.fill: parent
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
|
FadeImage {
|
||||||
|
id: image
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
source: Players.active?.trackArtUrl ?? ""
|
source: Players.getArtUrl(Players.active)
|
||||||
sourceSize.height: height
|
sourceSize: {
|
||||||
sourceSize.width: width
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
|
return Qt.size(width * dpr, height * dpr);
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: mask
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
duration: Appearance.anim.durations.extraLarge
|
type: Anim.StandardExtraLarge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
CustomRect {
|
||||||
id: mask
|
anchors.fill: parent
|
||||||
|
color: DynamicColors.palette.m3surface
|
||||||
anchors.fill: parent
|
opacity: 0.7
|
||||||
layer.enabled: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
gradient: Gradient {
|
|
||||||
orientation: Gradient.Horizontal
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.5)
|
|
||||||
position: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.2)
|
|
||||||
position: 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
GradientStop {
|
|
||||||
color: Qt.rgba(0, 0, 0, 0)
|
|
||||||
position: 0.8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.left: parent.left
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.extraLarge
|
||||||
|
anchors.right: parent.right
|
||||||
CustomText {
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
Layout.bottomMargin: Appearance.spacing.larger
|
spacing: Appearance.spacing.extraSmall
|
||||||
Layout.topMargin: Appearance.padding.large
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
font.family: Appearance.font.family.mono
|
|
||||||
font.weight: 500
|
|
||||||
text: qsTr("Now playing")
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3primary
|
color: DynamicColors.palette.m3primary
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.family: Appearance.font.family.mono
|
font.pointSize: Appearance.font.size.medium
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
font.weight: 600
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: Players.active?.trackArtist ?? qsTr("No media")
|
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
animate: true
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.family: Appearance.font.family.mono
|
font.pointSize: Appearance.font.size.small
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: Players.active?.trackTitle ?? qsTr("No media")
|
text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist")
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
ButtonRow {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.bottomMargin: Appearance.padding.large
|
Layout.topMargin: Appearance.spacing.small
|
||||||
Layout.topMargin: Appearance.spacing.large * 1.2
|
spacing: Appearance.spacing.extraSmall
|
||||||
spacing: Appearance.spacing.large
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
function onClicked(): void {
|
|
||||||
if (Players.active?.canGoPrevious)
|
|
||||||
Players.active.previous();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
enabled: Players.active?.canGoPrevious
|
||||||
icon: "skip_previous"
|
icon: "skip_previous"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
onClicked: Players.active?.previous()
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerControl {
|
IconButton {
|
||||||
function onClicked(): void {
|
checked: Players.active?.isPlaying ?? false
|
||||||
if (Players.active?.canTogglePlaying)
|
enabled: Players.active?.canTogglePlaying
|
||||||
Players.active.togglePlaying();
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||||
}
|
implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
|
||||||
active: Players.active?.isPlaying ?? false
|
onClicked: Players.active?.togglePlaying()
|
||||||
animate: true
|
|
||||||
icon: active ? "pause" : "play_arrow"
|
|
||||||
level: active ? 2 : 1
|
|
||||||
set_color: "Primary"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerControl {
|
IconButton {
|
||||||
function onClicked(): void {
|
enabled: Players.active?.canGoNext
|
||||||
if (Players.active?.canGoNext)
|
|
||||||
Players.active.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
icon: "skip_next"
|
icon: "skip_next"
|
||||||
}
|
isRound: true
|
||||||
}
|
shapeMorph: true
|
||||||
}
|
type: IconButton.Tonal
|
||||||
|
|
||||||
component PlayerControl: CustomRect {
|
onClicked: Players.active?.next()
|
||||||
id: control
|
|
||||||
|
|
||||||
property bool active
|
|
||||||
property alias animate: controlIcon.animate
|
|
||||||
property alias icon: controlIcon.text
|
|
||||||
property int level: 1
|
|
||||||
property string set_color: "Secondary"
|
|
||||||
|
|
||||||
function onClicked(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)
|
|
||||||
color: active ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
|
|
||||||
implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2
|
|
||||||
implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2
|
|
||||||
radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
|
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth {
|
|
||||||
Anim {
|
|
||||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on radius {
|
|
||||||
Anim {
|
|
||||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Elevation {
|
|
||||||
anchors.fill: parent
|
|
||||||
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
id: controlState
|
|
||||||
|
|
||||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
control.onClicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
id: controlIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
|
||||||
fill: control.active ? 1 : 0
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
|
|
||||||
Behavior on fill {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-47
@@ -1,81 +1,82 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import M3Shapes
|
||||||
import ZShell.Services
|
import ZShell.Services
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Effects
|
||||||
|
|
||||||
GridLayout {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
anchors.left: parent.left
|
readonly property real fontScale: {
|
||||||
anchors.margins: Appearance.padding.large
|
const diff = width / 391 - 1; // 391 is the width at 1080 height screen
|
||||||
anchors.right: parent.right
|
return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
|
||||||
columnSpacing: Appearance.spacing.large
|
}
|
||||||
columns: 2
|
|
||||||
rowSpacing: Appearance.spacing.large
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
rows: 1
|
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Cpu
|
||||||
|
}
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
service: Memory
|
service: Memory
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
service: Cpu
|
service: Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
RowLayout {
|
||||||
Layout.bottomMargin: Appearance.padding.large
|
id: layout
|
||||||
Layout.topMargin: Appearance.padding.large
|
|
||||||
fgColor: DynamicColors.palette.m3primary
|
anchors.fill: parent
|
||||||
icon: "memory"
|
anchors.margins: Appearance.padding.large
|
||||||
value: Cpu.percentage
|
spacing: Appearance.spacing.large
|
||||||
|
|
||||||
|
Resource {
|
||||||
|
id: cpu
|
||||||
|
|
||||||
|
fgColor: DynamicColors.palette.m3primary
|
||||||
|
icon: "memory"
|
||||||
|
value: Cpu.percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource {
|
||||||
|
fgColor: DynamicColors.palette.m3tertiary
|
||||||
|
icon: "memory_alt"
|
||||||
|
value: Memory.percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource {
|
||||||
|
fgColor: DynamicColors.palette.m3secondary
|
||||||
|
icon: "hard_disk"
|
||||||
|
value: Storage.percentage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource {
|
component Resource: CircularProgress {
|
||||||
Layout.bottomMargin: Appearance.padding.large
|
|
||||||
Layout.topMargin: Appearance.padding.large
|
|
||||||
fgColor: DynamicColors.palette.m3secondary
|
|
||||||
icon: "memory_alt"
|
|
||||||
value: Memory.percentage
|
|
||||||
}
|
|
||||||
|
|
||||||
component Resource: CustomRect {
|
|
||||||
id: res
|
id: res
|
||||||
|
|
||||||
required property color fgColor
|
|
||||||
required property string icon
|
required property string icon
|
||||||
required property real value
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
implicitSize: width
|
||||||
implicitHeight: width
|
|
||||||
radius: Appearance.rounding.large
|
|
||||||
|
|
||||||
Behavior on value {
|
Behavior on clampedVal {
|
||||||
Anim {
|
Anim {
|
||||||
duration: Appearance.anim.durations.large
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CircularProgress {
|
|
||||||
id: circ
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
|
||||||
fgColor: res.fgColor
|
|
||||||
padding: Appearance.padding.large * 3
|
|
||||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
|
||||||
value: res.value
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: icon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: res.fgColor
|
color: res.fgColor
|
||||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
font.pointSize: Appearance.font.size.extraLarge
|
||||||
font.weight: 600
|
|
||||||
text: res.icon
|
text: res.icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property int rootHeight
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: Weather.description
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: temp
|
||||||
|
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: Weather.temp
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
text: Weather.icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: qsTr("Feels like %1").arg(Weather.temp)
|
||||||
|
visible: root.rootHeight > 550
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: {
|
||||||
|
const today = Weather.forecast[0];
|
||||||
|
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||||
|
}
|
||||||
|
visible: root.rootHeight > 550
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import M3Shapes
|
||||||
|
import ZShell
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||||
|
implicitHeight: header.anchors.margins + header.implicitHeight + Appearance.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.largeIncreased
|
||||||
|
anchors.top: parent.top
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: "schedule"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: title
|
||||||
|
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: qsTr("Hourly forecast")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalFadeListView {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: Appearance.padding.largeIncreased
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitHeight: contentItem.childrenRect.height
|
||||||
|
model: Weather.hourlyForecast
|
||||||
|
orientation: VerticalFadeListView.Horizontal
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: hour
|
||||||
|
|
||||||
|
readonly property var cond: modelData
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
MaterialShape {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: Qt.alpha(DynamicColors.palette.m3primary, hour.index === 0 ? 1 : 0)
|
||||||
|
implicitSize: temp.implicitHeight + Appearance.padding.normal * 2
|
||||||
|
shape: MaterialShape.Cookie4Sided
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: temp
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: hour.index === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge
|
||||||
|
text: hour.cond.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
text: hour.cond.precipChance + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: Appearance.spacing.extraSmall
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-150
@@ -1,162 +1,23 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import qs.Modules.Lock.Weather
|
||||||
|
import qs.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Config
|
|
||||||
|
|
||||||
ColumnLayout {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property int rootHeight
|
required property int rootHeight
|
||||||
|
readonly property bool showForecast: rootHeight >= 700
|
||||||
|
|
||||||
anchors.left: parent.left
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
anchors.margins: Appearance.padding.large * 2
|
implicitHeight: {
|
||||||
anchors.right: parent.right
|
const base = brief.implicitHeight + brief.anchors.topMargin;
|
||||||
spacing: Appearance.spacing.small
|
if (showForecast)
|
||||||
|
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
||||||
Loader {
|
return base + brief.anchors.topMargin;
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.bottomMargin: -Appearance.padding.large
|
|
||||||
Layout.topMargin: Appearance.padding.large * 2
|
|
||||||
active: root.rootHeight > 610
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: CustomText {
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge
|
|
||||||
font.weight: 500
|
|
||||||
text: qsTr("Weather")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.large
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge * 2.5
|
|
||||||
text: Weather.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
font.weight: 500
|
|
||||||
text: Weather.description
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pointSize: Appearance.font.size.normal
|
|
||||||
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
Layout.rightMargin: Appearance.padding.smaller
|
|
||||||
active: root.width > 400
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: ColumnLayout {
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
elide: Text.ElideLeft
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge
|
|
||||||
font.weight: 500
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: Weather.temp
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3outline
|
|
||||||
elide: Text.ElideLeft
|
|
||||||
font.pointSize: Appearance.font.size.smaller
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: forecastLoader
|
|
||||||
|
|
||||||
Layout.bottomMargin: Appearance.padding.large * 2
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: Appearance.spacing.smaller
|
|
||||||
active: root.rootHeight > 820
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: RowLayout {
|
|
||||||
spacing: Appearance.spacing.large
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
const forecast = Weather.hourlyForecast;
|
|
||||||
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
|
||||||
if (!forecast)
|
|
||||||
return Array.from({
|
|
||||||
length: count
|
|
||||||
}, () => null);
|
|
||||||
|
|
||||||
return forecast.slice(0, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: forecastHour
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: DynamicColors.palette.m3outline
|
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: {
|
|
||||||
const hour = forecastHour.modelData?.hour ?? 0;
|
|
||||||
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge * 1.5
|
|
||||||
font.weight: 500
|
|
||||||
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 900000 // 15 minutes
|
interval: 900000 // 15 minutes
|
||||||
@@ -166,4 +27,27 @@ ColumnLayout {
|
|||||||
|
|
||||||
onTriggered: Weather.reload()
|
onTriggered: Weather.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BriefInfo {
|
||||||
|
id: brief
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Appearance.padding.extraLarge
|
||||||
|
rootHeight: root.rootHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: forecast
|
||||||
|
|
||||||
|
active: root.showForecast
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
anchors.right: parent.right
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Forecast {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Networking
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
import qs.Modules
|
||||||
|
import qs.Helpers as Helpers
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var wrapper
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Helpers.Network.devices
|
||||||
|
|
||||||
|
CustomRadioButton {
|
||||||
|
id: network
|
||||||
|
|
||||||
|
required property NetworkDevice modelData
|
||||||
|
|
||||||
|
checked: Helpers.Network.activeDevice?.name === modelData.name
|
||||||
|
text: modelData.description
|
||||||
|
visible: modelData.name !== "lo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Modules
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.top: parent.top
|
||||||
|
implicitWidth: layout.implicitWidth
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
text: "android_wifi_4_bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,15 +102,13 @@ Item {
|
|||||||
to: 1.0
|
to: 1.0
|
||||||
value: root.brightness
|
value: root.brightness
|
||||||
|
|
||||||
onPressedChanged: {
|
onMoved: {
|
||||||
if (!pressed) {
|
if (Config.osd.allMonBrightness) {
|
||||||
if (Config.osd.allMonBrightness) {
|
for (const mon of Brightness.monitors) {
|
||||||
for (const mon of Brightness.monitors) {
|
mon.setBrightness(value);
|
||||||
mon.setBrightness(value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.monitor?.setBrightness(value);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
root.monitor?.setBrightness(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ Scope {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
function onShouldShowChanged(): void {
|
||||||
|
|
||||||
onShouldShowChanged: {
|
|
||||||
if (root.shouldShow) {
|
if (root.shouldShow) {
|
||||||
panelWindow.visible = true;
|
panelWindow.visible = true;
|
||||||
openAnim.start();
|
openAnim.start();
|
||||||
@@ -38,6 +36,8 @@ Scope {
|
|||||||
closeAnim.start();
|
closeAnim.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
}
|
}
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -32,6 +32,31 @@ Item {
|
|||||||
implicitHeight: width
|
implicitHeight: width
|
||||||
radius: Appearance.rounding.large
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
color: DynamicColors.palette.m3primaryContainer
|
||||||
|
implicitHeight: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||||
|
implicitWidth: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
LoadingIndicator {
|
||||||
|
id: loadingIndicator
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
containsIcon: true
|
||||||
|
implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: img
|
id: img
|
||||||
|
|
||||||
|
|||||||
@@ -148,154 +148,200 @@ VerticalFadeFlickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
Column {
|
||||||
id: resultList
|
id: resultList
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
cacheBuffer: 10000
|
|
||||||
implicitHeight: contentHeight
|
|
||||||
interactive: false
|
|
||||||
spacing: Appearance.padding.large
|
spacing: Appearance.padding.large
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
add: Transition {
|
||||||
id: group
|
Anim {
|
||||||
|
from: 0
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "x,y"
|
||||||
|
}
|
||||||
|
|
||||||
required property int index
|
Anim {
|
||||||
required property var modelData
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spacing: Appearance.spacing.small
|
Repeater {
|
||||||
width: resultList.width
|
model: ScriptModel {
|
||||||
|
objectProp: "pageIdx"
|
||||||
RowLayout {
|
values: root.groups
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: Appearance.padding.small
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: group.modelData.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: group.modelData.page
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
id: group
|
||||||
spacing: Appearance.spacing.extraSmall / 2
|
|
||||||
|
|
||||||
Repeater {
|
required property int index
|
||||||
model: group.modelData.entries
|
required property var modelData
|
||||||
|
|
||||||
CustomRect {
|
spacing: Appearance.spacing.small
|
||||||
id: result
|
width: resultList.width
|
||||||
|
|
||||||
required property int index
|
RowLayout {
|
||||||
readonly property bool isFirst: index === 0
|
Layout.fillWidth: true
|
||||||
readonly property bool isLast: index === group.modelData.entries.length - 1
|
Layout.leftMargin: Appearance.padding.small
|
||||||
required property var modelData
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
fill: 1
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: group.modelData.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
bottomLeftRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
color: DynamicColors.palette.m3secondary
|
||||||
bottomRightRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
elide: Text.ElideRight
|
||||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
font.pointSize: Appearance.font.size.large
|
||||||
implicitHeight: {
|
text: group.modelData.page
|
||||||
const h = resultLayout.implicitHeight + resultLayout.anchors.margins * 2;
|
}
|
||||||
return h % 2 === 0 ? h : h + 1;
|
}
|
||||||
}
|
|
||||||
topLeftRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
|
||||||
topRightRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
|
||||||
|
|
||||||
RadiusBehavior on bottomLeftRadius {
|
Column {
|
||||||
|
id: cardList
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.extraSmall / 2
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
Anim {
|
||||||
|
from: 0
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
RadiusBehavior on bottomRightRadius {
|
}
|
||||||
}
|
move: Transition {
|
||||||
RadiusBehavior on topLeftRadius {
|
Anim {
|
||||||
}
|
properties: "x,y"
|
||||||
RadiusBehavior on topRightRadius {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
Anim {
|
||||||
id: resultLayout
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
Repeater {
|
||||||
anchors.margins: Appearance.padding.large
|
model: ScriptModel {
|
||||||
anchors.rightMargin: result.modelData.togglePath ? toggle.width + Appearance.padding.large * 2 : Appearance.padding.large
|
objectProp: "anchor"
|
||||||
spacing: Appearance.spacing.small / 2
|
values: group.modelData.entries
|
||||||
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
id: result
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
elide: Text.ElideRight
|
required property int index
|
||||||
font.pointSize: Appearance.font.size.small
|
readonly property bool isFirst: index === 0
|
||||||
text: {
|
readonly property bool isLast: index === group.modelData.entries.length - 1
|
||||||
const labels = result.modelData.crumbLabels.slice(1);
|
required property var modelData
|
||||||
const section = result.modelData.section;
|
|
||||||
const parts = section && section !== labels[labels.length - 1] ? labels.concat(section) : labels;
|
bottomLeftRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||||
return parts.join(" \u203a ");
|
bottomRightRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||||
|
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||||
|
implicitHeight: {
|
||||||
|
const h = resultLayout.implicitHeight + resultLayout.anchors.margins * 2;
|
||||||
|
return h % 2 === 0 ? h : h + 1;
|
||||||
|
}
|
||||||
|
topLeftRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||||
|
topRightRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||||
|
width: cardList.width
|
||||||
|
|
||||||
|
RadiusBehavior on bottomLeftRadius {
|
||||||
|
}
|
||||||
|
RadiusBehavior on bottomRightRadius {
|
||||||
|
}
|
||||||
|
RadiusBehavior on topLeftRadius {
|
||||||
|
}
|
||||||
|
RadiusBehavior on topRightRadius {
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: resultLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
anchors.rightMargin: result.modelData.togglePath ? toggle.width + Appearance.padding.large * 2 : Appearance.padding.large
|
||||||
|
spacing: Appearance.spacing.small / 2
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
text: {
|
||||||
|
const labels = result.modelData.crumbLabels.slice(1);
|
||||||
|
const section = result.modelData.section;
|
||||||
|
const parts = section && section !== labels[labels.length - 1] ? labels.concat(section) : labels;
|
||||||
|
return parts.join(" \u203a ");
|
||||||
|
}
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.palette.m3onSurface
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: SettingsSearcher.highlight(result.modelData.title, root.search, DynamicColors.palette.m3primary)
|
||||||
|
textFormat: text.includes("<font") ? Text.StyledText : Text.PlainText
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.palette.m3outline
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
text: SettingsSearcher.highlight(result.modelData.subtext, root.search, DynamicColors.palette.m3primary)
|
||||||
|
textFormat: text.includes("<font") ? Text.StyledText : Text.PlainText
|
||||||
|
visible: result.modelData.subtext.length > 0
|
||||||
}
|
}
|
||||||
visible: text.length > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
StateLayer {
|
||||||
Layout.fillWidth: true
|
id: layer
|
||||||
color: DynamicColors.palette.m3onSurface
|
|
||||||
elide: Text.ElideRight
|
z: 1
|
||||||
font.pointSize: Appearance.font.size.medium
|
|
||||||
text: SettingsSearcher.highlight(result.modelData.title, root.search, DynamicColors.palette.m3primary)
|
onClicked: {
|
||||||
textFormat: text.includes("<font") ? Text.StyledText : Text.PlainText
|
root.sState.jumpToSetting(result.modelData.pageIdx, result.modelData.subPath, result.modelData.anchor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomSwitch {
|
||||||
Layout.fillWidth: true
|
id: toggle
|
||||||
color: DynamicColors.palette.m3outline
|
|
||||||
elide: Text.ElideRight
|
anchors.right: parent.right
|
||||||
font.pointSize: Appearance.font.size.small
|
anchors.rightMargin: Appearance.padding.large
|
||||||
text: SettingsSearcher.highlight(result.modelData.subtext, root.search, DynamicColors.palette.m3primary)
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
textFormat: text.includes("<font") ? Text.StyledText : Text.PlainText
|
cLayer: 3
|
||||||
visible: result.modelData.subtext.length > 0
|
checked: result.modelData.toggleValue
|
||||||
|
scale: 0.85
|
||||||
|
transformOrigin: Item.Right
|
||||||
|
visible: result.modelData.togglePath
|
||||||
|
z: 2
|
||||||
|
|
||||||
|
onToggled: result.modelData.setToggle(checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
id: layer
|
|
||||||
|
|
||||||
z: 1
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.sState.jumpToSetting(result.modelData.pageIdx, result.modelData.subPath, result.modelData.anchor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomSwitch {
|
|
||||||
id: toggle
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Appearance.padding.large
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
cLayer: 3
|
|
||||||
checked: result.modelData.toggleValue
|
|
||||||
scale: 0.85
|
|
||||||
transformOrigin: Item.Right
|
|
||||||
visible: result.modelData.togglePath
|
|
||||||
z: 2
|
|
||||||
|
|
||||||
onToggled: result.modelData.setToggle(checked)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model: ScriptModel {
|
|
||||||
objectProp: "pageIdx"
|
|
||||||
values: root.groups
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ PageBase {
|
|||||||
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
|
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
|
||||||
active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
|
active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
|
||||||
first: true
|
first: true
|
||||||
last: true
|
|
||||||
menuItems: root.clockFormats
|
menuItems: root.clockFormats
|
||||||
settingAnchor: "bar-clock-format"
|
settingAnchor: "bar-clock-format"
|
||||||
subtext: qsTr("Change how time is displayed in the widget")
|
subtext: qsTr("Change how time is displayed in the widget")
|
||||||
@@ -58,5 +57,15 @@ PageBase {
|
|||||||
Config.general.dateFormat = item.value;
|
Config.general.dateFormat = item.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
checked: Config.services.useTwelveHourClock
|
||||||
|
last: true
|
||||||
|
settingAnchor: "bar-clock-twelve-hour"
|
||||||
|
subtext: qsTr("Format timestamps for twelve or twenty-four hours in UI")
|
||||||
|
text: qsTr("Twelve hour clock")
|
||||||
|
|
||||||
|
onToggled: Config.services.useTwelveHourClock = checked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,14 +39,13 @@ PageBase {
|
|||||||
onToggled: Config.bar.tray.showMicrophone = checked
|
onToggled: Config.bar.tray.showMicrophone = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleRow {
|
|
||||||
checked: Config.bar.tray.showNetwork
|
|
||||||
text: qsTr("Network")
|
|
||||||
|
|
||||||
onToggled: Config.bar.tray.showNetwork = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
//////// FOR LATER:
|
//////// FOR LATER:
|
||||||
|
// ToggleRow {
|
||||||
|
// checked: Config.bar.tray.showNetwork
|
||||||
|
// text: qsTr("Network")
|
||||||
|
//
|
||||||
|
// onToggled: Config.bar.tray.showNetwork = checked
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// ToggleRow {
|
// ToggleRow {
|
||||||
// checked: Config.bar.tray.showWifi
|
// checked: Config.bar.tray.showWifi
|
||||||
@@ -95,15 +94,5 @@ PageBase {
|
|||||||
|
|
||||||
onToggled: Config.bar.popouts.upower = checked
|
onToggled: Config.bar.popouts.upower = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleRow {
|
|
||||||
checked: Config.bar.popouts.network
|
|
||||||
last: true
|
|
||||||
settingAnchor: "bar-status-network-popout"
|
|
||||||
subtext: qsTr("Show a details popout when hovering the network icon")
|
|
||||||
text: qsTr("Network popout on hover")
|
|
||||||
|
|
||||||
onToggled: Config.bar.popouts.network = checked
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import qs.Components
|
|
||||||
import qs.Config
|
|
||||||
import qs.Helpers
|
|
||||||
|
|
||||||
CustomClippingRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var wrapper
|
|
||||||
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
|
||||||
implicitHeight: networkPopContent.height + networks.implicitHeight + networkPopContent.anchors.margins + networks.anchors.margins * 2
|
|
||||||
implicitWidth: 500 + 8 * 2
|
|
||||||
radius: (20 - Appearance.padding.small) * Appearance.rounding.scale
|
|
||||||
|
|
||||||
Component.onCompleted: Network.active = true
|
|
||||||
Component.onDestruction: Network.active = false
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: networkPopContent
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.large
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
|
||||||
Layout.rightMargin: Appearance.padding.extraSmall
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: qsTr("Wifi")
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle {
|
|
||||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
|
||||||
checked: Network.wifiEnabled
|
|
||||||
label: qsTr("WiFi enabled")
|
|
||||||
|
|
||||||
toggle.onToggled: Network.setWifi(checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: networks
|
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.normal
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: networkPopContent.bottom
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.leftMargin: Appearance.padding.normal
|
|
||||||
text: qsTr("Connected")
|
|
||||||
visible: Network.connectedNetworks.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
visible: Network.connectedNetworks.length > 0
|
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: [...Network.connectedNetworks].sort((a, b) => {
|
|
||||||
return b.signalStrength - a.signalStrength;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkItem {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer {
|
|
||||||
visible: Network.connectedNetworks.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.leftMargin: Appearance.padding.normal
|
|
||||||
text: qsTr("Known networks")
|
|
||||||
visible: Network.knownNetworks.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
visible: Network.knownNetworks.length > 0
|
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: [...Network.knownNetworks].sort((a, b) => {
|
|
||||||
return b.signalStrength - a.signalStrength;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkItem {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer {
|
|
||||||
visible: Network.knownNetworks.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.leftMargin: Appearance.padding.normal
|
|
||||||
text: qsTr("Networks")
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.leftMargin: Appearance.padding.normal
|
|
||||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
font.pointSize: Appearance.font.size.extraSmall
|
|
||||||
text: qsTr("%1 networks available").arg(Network.networks.length) // qmllint disable missing-property
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: networkRepeater
|
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: [...Network.unknownNetworks].sort((a, b) => {
|
|
||||||
return b.signalStrength - a.signalStrength;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkItem {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component NetworkItem: CustomRect {
|
|
||||||
id: knownNetworkItem
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: visible ? knownNetworkRow.implicitHeight + Appearance.padding.smaller * 2 : 0
|
|
||||||
Layout.rightMargin: Appearance.padding.extraSmall
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: knownNetworkRow
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Appearance.padding.larger
|
|
||||||
anchors.rightMargin: Appearance.padding.normal
|
|
||||||
opacity: 0
|
|
||||||
scale: 0.7
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on scale {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
opacity = 1;
|
|
||||||
scale = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
text: Icons.getNetworkIcon(knownNetworkItem.modelData.signalStrength * 100, Network.isSecure(knownNetworkItem.modelData.security))
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: Appearance.spacing.extraSmall
|
|
||||||
Layout.rightMargin: Appearance.spacing.extraSmall
|
|
||||||
color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
|
|
||||||
elide: Text.ElideRight
|
|
||||||
text: knownNetworkItem.modelData.name
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
color: Qt.alpha(DynamicColors.palette.m3primary, knownNetworkItem.modelData.active ? 1 : 0)
|
|
||||||
implicitHeight: knownWirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall
|
|
||||||
implicitWidth: implicitHeight
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
|
|
||||||
// CircularIndicator {
|
|
||||||
// anchors.fill: parent
|
|
||||||
// running: knownNetworkItem.loading
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
NetworkPassword.requestOpen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
component Spacer: Item {
|
|
||||||
id: spacer
|
|
||||||
|
|
||||||
Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.extraSmall : 0
|
|
||||||
}
|
|
||||||
component Toggle: RowLayout {
|
|
||||||
property alias checked: toggle.checked
|
|
||||||
required property string label
|
|
||||||
property alias toggle: toggle
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.rightMargin: Appearance.padding.extraSmall
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: parent.label
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomSwitch {
|
|
||||||
id: toggle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,11 +52,6 @@ RowLayout {
|
|||||||
id: "audio",
|
id: "audio",
|
||||||
item: child
|
item: child
|
||||||
};
|
};
|
||||||
if (child.objectName === "networkWidget" && Config.bar.popouts.network)
|
|
||||||
return {
|
|
||||||
id: "network",
|
|
||||||
item: child
|
|
||||||
};
|
|
||||||
if (child.objectName === "upowerWidget" && Config.bar.popouts.upower)
|
if (child.objectName === "upowerWidget" && Config.bar.popouts.upower)
|
||||||
return {
|
return {
|
||||||
id: "upower",
|
id: "upower",
|
||||||
@@ -151,10 +146,6 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkWidget {
|
|
||||||
objectName: "networkWidget"
|
|
||||||
}
|
|
||||||
|
|
||||||
UPowerWidget {
|
UPowerWidget {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
objectName: "upowerWidget"
|
objectName: "upowerWidget"
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.Daemons
|
|
||||||
import qs.Modules
|
|
||||||
import qs.Config
|
|
||||||
import qs.Components
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// property color barColor: DynamicColors.palette.m3primary
|
|
||||||
property color textColor: DynamicColors.palette.m3onSurface
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
animate: true
|
|
||||||
color: root.textColor // Network.connected ? root.textColor : DynamicColors.palette.m3error
|
|
||||||
fill: 1
|
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
text: "android_wifi_4_bar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,10 +12,10 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool completed
|
property bool completed
|
||||||
property real cropHeight: displayData.height ?? 1.0
|
property real cropHeight: displayData?.height ?? 1.0
|
||||||
property real cropWidth: displayData.width ?? 1.0
|
property real cropWidth: displayData?.width ?? 1.0
|
||||||
property real cropX: displayData.x ?? 0.0
|
property real cropX: displayData?.x ?? 0.0
|
||||||
property real cropY: displayData.y ?? 0.0
|
property real cropY: displayData?.y ?? 0.0
|
||||||
property WallpaperImage current
|
property WallpaperImage current
|
||||||
readonly property var displayData: Wallpapers.getCrop(screen.name)
|
readonly property var displayData: Wallpapers.getCrop(screen.name)
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
file(GLOB ZSHELL_CLI_WHEEL "@ZSHELL_CLI_DIST_DIR@/*.whl")
|
||||||
|
if(NOT ZSHELL_CLI_WHEEL)
|
||||||
|
message(FATAL_ERROR "No zshell-cli wheel found in @ZSHELL_CLI_DIST_DIR@")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_zshell_installer_args "--prefix=@CMAKE_INSTALL_PREFIX@")
|
||||||
|
if(DEFINED ENV{DESTDIR} AND NOT "$ENV{DESTDIR}" STREQUAL "")
|
||||||
|
list(APPEND _zshell_installer_args "--destdir=$ENV{DESTDIR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND "@Python3_EXECUTABLE@" -m installer
|
||||||
|
${_zshell_installer_args}
|
||||||
|
${ZSHELL_CLI_WHEEL}
|
||||||
|
RESULT_VARIABLE ZSHELL_CLI_INSTALL_RESULT
|
||||||
|
)
|
||||||
|
if(NOT ZSHELL_CLI_INSTALL_RESULT EQUAL 0)
|
||||||
|
message(FATAL_ERROR "zshell-cli wheel install failed")
|
||||||
|
endif()
|
||||||
+51
-33
@@ -1,12 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from typer._completion_shared import install, _get_shell_name
|
|
||||||
from typer._completion_classes import completion_init
|
from typer._completion_classes import completion_init
|
||||||
from zshell.subcommands import shell, scheme, screenshot, wallpaper, record
|
from typer._completion_shared import _get_shell_name, install
|
||||||
|
|
||||||
|
from zshell.subcommands import record, scheme, screenshot, shell, wallpaper
|
||||||
|
|
||||||
app = typer.Typer(name="zshell-cli", add_completion=False)
|
app = typer.Typer(name="zshell-cli", add_completion=False)
|
||||||
|
|
||||||
@@ -18,40 +20,56 @@ app.add_typer(record.app, name="record")
|
|||||||
|
|
||||||
|
|
||||||
def _completion_installed() -> bool:
|
def _completion_installed() -> bool:
|
||||||
shell = _get_shell_name()
|
shell = _get_shell_name()
|
||||||
match shell:
|
match shell:
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return (Path.home() / ".zfunc" / "_zshell-cli").exists()
|
return (Path.home() / ".zfunc" / "_zshell-cli").exists()
|
||||||
case "bash":
|
case "bash":
|
||||||
return (Path.home() / ".bash_completions" / "zshell-cli.sh").exists()
|
return (
|
||||||
case "fish":
|
Path.home() / ".bash_completions" / "zshell-cli.sh"
|
||||||
return (Path.home() / ".config" / "fish" / "completions" / "zshell-cli.fish").exists()
|
).exists()
|
||||||
return False
|
case "fish":
|
||||||
|
return (
|
||||||
|
Path.home()
|
||||||
|
/ ".config"
|
||||||
|
/ "fish"
|
||||||
|
/ "completions"
|
||||||
|
/ "zshell-cli.fish"
|
||||||
|
).exists()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _install_completion() -> None:
|
def _install_completion() -> None:
|
||||||
if _completion_installed():
|
if _completion_installed():
|
||||||
print("zshell-cli: Shell completion already installed.")
|
print("zshell-cli: Shell completion already installed.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
shell = _get_shell_name()
|
shell = _get_shell_name()
|
||||||
if shell is None:
|
if shell is None:
|
||||||
print("zshell-cli: Unable to detect shell type.", file=sys.stderr)
|
print("zshell-cli: Unable to detect shell type.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
_, path = install(prog_name="zshell-cli")
|
_, path = install(prog_name="zshell-cli")
|
||||||
print(f"zshell-cli: Shell completion installed ({shell}: {path})")
|
print(f"zshell-cli: Shell completion installed ({shell}: {path})")
|
||||||
print("zshell-cli: Restart your shell or source the file to enable tab-completion.")
|
print(
|
||||||
except Exception as e:
|
"zshell-cli: Restart your shell or source the file to enable tab-completion."
|
||||||
print(f"zshell-cli: Failed to install shell completion: {e}", file=sys.stderr)
|
)
|
||||||
raise typer.Exit(code=1)
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"zshell-cli: Failed to install shell completion: {e}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
raise typer.Exit(code=1) from None
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if "--install-autocomplete" in sys.argv:
|
if "--install-autocomplete" in sys.argv:
|
||||||
_install_completion()
|
_install_completion()
|
||||||
return
|
return
|
||||||
if "_ZSHELL_CLI_COMPLETE" in os.environ:
|
if "_ZSHELL_CLI_COMPLETE" in os.environ:
|
||||||
completion_init()
|
completion_init()
|
||||||
if sys.stdout.isatty() and not _completion_installed():
|
if sys.stdout.isatty() and not _completion_installed():
|
||||||
print("zshell-cli: Tip: run with --install-autocomplete for tab completion.", file=sys.stderr)
|
print(
|
||||||
app()
|
"zshell-cli: Tip: run with --install-autocomplete for tab completion.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
app()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from zshell import main
|
from zshell import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import os
|
import contextlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
@@ -18,193 +18,253 @@ TEMP_RECORDING = STATE_DIR / "recording.mp4"
|
|||||||
REPLAY_RECORDING = STATE_DIR / "replay.mp4"
|
REPLAY_RECORDING = STATE_DIR / "replay.mp4"
|
||||||
NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
|
NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
|
||||||
|
|
||||||
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR", str(Path(HOME) / "Videos/Recordings"))
|
RECORDINGS_DIR = os.getenv(
|
||||||
|
"ZSHELL_RECORDINGS_DIR", str(Path(HOME) / "Videos/Recordings")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _read_extra_args() -> list[str]:
|
def _read_extra_args() -> list[str]:
|
||||||
try:
|
try:
|
||||||
if CONFIG.is_file():
|
if CONFIG.is_file():
|
||||||
data = json.loads(CONFIG.read_text())
|
data = json.loads(CONFIG.read_text())
|
||||||
return data.get("record", {}).get("extraArgs", [])
|
return data.get("record", {}).get("extraArgs", [])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _is_recording() -> bool:
|
def _is_recording() -> bool:
|
||||||
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
return (
|
||||||
|
subprocess.run(
|
||||||
|
["pidof", RECORDER],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
).returncode
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _notify(summary: str, body: str = "", actions: list | None = None, timeout: int = 5000) -> Optional[int]:
|
def _notify(
|
||||||
args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
|
summary: str,
|
||||||
if actions:
|
body: str = "",
|
||||||
for action in actions:
|
actions: list | None = None,
|
||||||
args.extend(["-A", action])
|
timeout: int = 5000,
|
||||||
try:
|
) -> int | None:
|
||||||
proc = subprocess.run(args, capture_output=True, text=True)
|
args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
|
||||||
return int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
|
if actions:
|
||||||
except Exception:
|
for action in actions:
|
||||||
return None
|
args.extend(["-A", action])
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(args, capture_output=True, text=True)
|
||||||
|
return (
|
||||||
|
int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _close_notification(notif_id: int):
|
def _close_notification(notif_id: int):
|
||||||
subprocess.run(["notify-send", "--close", str(notif_id)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.run(
|
||||||
|
["notify-send", "--close", str(notif_id)],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_monitors() -> list[dict]:
|
def _get_monitors() -> list[dict]:
|
||||||
try:
|
try:
|
||||||
res = subprocess.run(["hyprctl", "monitors", "-j"], capture_output=True, text=True)
|
res = subprocess.run(
|
||||||
return json.loads(res.stdout)
|
["hyprctl", "monitors", "-j"], capture_output=True, text=True
|
||||||
except Exception:
|
)
|
||||||
return []
|
return json.loads(res.stdout)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _focused_monitor_name() -> Optional[str]:
|
def _focused_monitor_name() -> str | None:
|
||||||
for m in _get_monitors():
|
for m in _get_monitors():
|
||||||
if m.get("focused"):
|
if m.get("focused"):
|
||||||
return m["name"]
|
return m["name"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
||||||
region = (x, y, x + w, y + h)
|
region = (x, y, x + w, y + h)
|
||||||
intersecting = []
|
intersecting = []
|
||||||
for m in _get_monitors():
|
for m in _get_monitors():
|
||||||
mx, my, mw, mh = m["x"], m["y"], m["width"], m["height"]
|
mx, my, mw, mh = m["x"], m["y"], m["width"], m["height"]
|
||||||
if not (region[2] <= mx or region[0] >= mx + mw or region[3] <= my or region[1] >= my + mh):
|
if not (
|
||||||
intersecting.append(m)
|
region[2] <= mx
|
||||||
return intersecting
|
or region[0] >= mx + mw
|
||||||
|
or region[3] <= my
|
||||||
|
or region[1] >= my + mh
|
||||||
|
):
|
||||||
|
intersecting.append(m)
|
||||||
|
return intersecting
|
||||||
|
|
||||||
|
|
||||||
def _highest_refresh(monitors: list[dict]) -> float:
|
def _highest_refresh(monitors: list[dict]) -> float:
|
||||||
return max((m["refreshRate"] for m in monitors), default=60.0)
|
return max((m["refreshRate"] for m in monitors), default=60.0)
|
||||||
|
|
||||||
|
|
||||||
def _slurp_region() -> Optional[str]:
|
def _slurp_region() -> str | None:
|
||||||
try:
|
try:
|
||||||
return subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True).strip()
|
return subprocess.check_output(
|
||||||
except subprocess.CalledProcessError:
|
["slurp", "-f", "%wx%h+%x+%y"], text=True
|
||||||
return None
|
).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
|
def _parse_geometry(geometry: str) -> tuple[int, int, int, int] | None:
|
||||||
import re
|
import re
|
||||||
|
|
||||||
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
|
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
|
||||||
if match:
|
if match:
|
||||||
return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2))
|
return (
|
||||||
return None
|
int(match.group(3)),
|
||||||
|
int(match.group(4)),
|
||||||
|
int(match.group(1)),
|
||||||
|
int(match.group(2)),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def start_recording(region: Optional[str], sound: bool):
|
def start_recording(region: str | None, sound: bool):
|
||||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
cmd = [RECORDER]
|
cmd = [RECORDER]
|
||||||
extra_args = _read_extra_args()
|
extra_args = _read_extra_args()
|
||||||
|
|
||||||
if region:
|
if region:
|
||||||
if region.lower() == "slurp" or not region:
|
if region.lower() == "slurp" or not region:
|
||||||
geometry = _slurp_region()
|
geometry = _slurp_region()
|
||||||
if not geometry:
|
if not geometry:
|
||||||
typer.echo("Region selection cancelled.")
|
typer.echo("Region selection cancelled.")
|
||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
else:
|
else:
|
||||||
geometry = region
|
geometry = region
|
||||||
|
|
||||||
parsed = _parse_geometry(geometry)
|
parsed = _parse_geometry(geometry)
|
||||||
if not parsed:
|
if not parsed:
|
||||||
typer.echo("Invalid geometry format.")
|
typer.echo("Invalid geometry format.")
|
||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
x, y, w, h = parsed
|
x, y, w, h = parsed
|
||||||
|
|
||||||
monitors = _monitors_intersecting_region(x, y, w, h)
|
monitors = _monitors_intersecting_region(x, y, w, h)
|
||||||
framerate = _highest_refresh(monitors)
|
framerate = _highest_refresh(monitors)
|
||||||
cmd.extend(["-w", "region", "-region", geometry, "-f", str(int(framerate))])
|
cmd.extend(
|
||||||
|
["-w", "region", "-region", geometry, "-f", str(int(framerate))]
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
monitor_name = _focused_monitor_name()
|
monitor_name = _focused_monitor_name()
|
||||||
if not monitor_name:
|
if not monitor_name:
|
||||||
typer.echo("No focused monitor found.")
|
typer.echo("No focused monitor found.")
|
||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
|
|
||||||
monitors = _get_monitors()
|
monitors = _get_monitors()
|
||||||
mon = next((m for m in monitors if m["name"] == monitor_name), None)
|
mon = next((m for m in monitors if m["name"] == monitor_name), None)
|
||||||
rate = int(mon["refreshRate"]) if mon else 60
|
rate = int(mon["refreshRate"]) if mon else 60
|
||||||
cmd.extend(["-w", monitor_name, "-f", str(rate)])
|
cmd.extend(["-w", monitor_name, "-f", str(rate)])
|
||||||
|
|
||||||
if sound:
|
if sound:
|
||||||
cmd.extend(["-a", "default_output"])
|
cmd.extend(["-a", "default_output"])
|
||||||
|
|
||||||
cmd.extend(extra_args)
|
cmd.extend(extra_args)
|
||||||
cmd.extend(["-o", str(TEMP_RECORDING)])
|
cmd.extend(["-o", str(TEMP_RECORDING)])
|
||||||
|
|
||||||
subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
start_new_session=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
|
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
|
||||||
if notif_id is not None:
|
if notif_id is not None:
|
||||||
NOTIF_ID_FILE.write_text(str(notif_id))
|
NOTIF_ID_FILE.write_text(str(notif_id))
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if not _is_recording():
|
if not _is_recording():
|
||||||
_notify("Recording failed", "Check gpu-screen-recorder output.", timeout=5000)
|
_notify(
|
||||||
raise typer.Exit(code=1)
|
"Recording failed",
|
||||||
|
"Check gpu-screen-recorder output.",
|
||||||
|
timeout=5000,
|
||||||
|
)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
def stop_recording(clipboard: bool):
|
def stop_recording(clipboard: bool):
|
||||||
subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.run(
|
||||||
|
["pkill", "-f", RECORDER],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
for _ in range(50):
|
for _ in range(50):
|
||||||
if not _is_recording():
|
if not _is_recording():
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
dest_dir = Path(RECORDINGS_DIR)
|
dest_dir = Path(RECORDINGS_DIR)
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
|
timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
final_path = dest_dir / f"recording_{timestamp}.mp4"
|
final_path = dest_dir / f"recording_{timestamp}.mp4"
|
||||||
|
|
||||||
if TEMP_RECORDING.exists():
|
if TEMP_RECORDING.exists():
|
||||||
TEMP_RECORDING.rename(final_path)
|
TEMP_RECORDING.rename(final_path)
|
||||||
|
|
||||||
if NOTIF_ID_FILE.is_file():
|
if NOTIF_ID_FILE.is_file():
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
|
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
|
||||||
except Exception:
|
NOTIF_ID_FILE.unlink()
|
||||||
pass
|
|
||||||
NOTIF_ID_FILE.unlink()
|
|
||||||
|
|
||||||
if clipboard:
|
if clipboard:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
|
["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
|
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
|
||||||
|
|
||||||
|
|
||||||
def toggle_pause():
|
def toggle_pause():
|
||||||
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.run(
|
||||||
typer.echo("Toggled pause.")
|
["pkill", "-USR2", "-f", RECORDER],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
typer.echo("Toggled pause.")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def record(
|
def record(
|
||||||
region: Optional[str] = typer.Option(
|
region: str | None = typer.Option(
|
||||||
None,
|
None,
|
||||||
"--region",
|
"--region",
|
||||||
"-r",
|
"-r",
|
||||||
help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.",
|
help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.",
|
||||||
),
|
),
|
||||||
sound: bool = typer.Option(False, "--sound", "-s", help="Record audio from default output."),
|
sound: bool = typer.Option(
|
||||||
pause: bool = typer.Option(False, "--pause", "-p", help="Toggle pause/resume."),
|
False, "--sound", "-s", help="Record audio from default output."
|
||||||
clipboard: bool = typer.Option(False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
|
),
|
||||||
|
pause: bool = typer.Option(
|
||||||
|
False, "--pause", "-p", help="Toggle pause/resume."
|
||||||
|
),
|
||||||
|
clipboard: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--clipboard",
|
||||||
|
"-c",
|
||||||
|
help="Copy the final recording path to clipboard.",
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""Start or stop a screen recording with gpu-screen-recorder."""
|
"""Start or stop a screen recording with gpu-screen-recorder."""
|
||||||
if pause:
|
if pause:
|
||||||
toggle_pause()
|
toggle_pause()
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
|
|
||||||
if _is_recording():
|
if _is_recording():
|
||||||
stop_recording(clipboard)
|
stop_recording(clipboard)
|
||||||
else:
|
else:
|
||||||
start_recording(region, sound)
|
start_recording(region, sound)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
args = ["qs", "-c", "zshell"]
|
args = ["qs", "-c", "zshell"]
|
||||||
@@ -8,9 +9,9 @@ app = typer.Typer()
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def start():
|
def start():
|
||||||
subprocess.run(args + ["ipc"] + ["call"] + ["picker"] + ["open"], check=True)
|
subprocess.run([*args, "ipc", "call", "picker", "open"], check=True)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def start_freeze():
|
def start_freeze():
|
||||||
subprocess.run(args + ["ipc"] + ["call"] + ["picker"] + ["openFreeze"], check=True)
|
subprocess.run([*args, "ipc", "call", "picker", "openFreeze"], check=True)
|
||||||
|
|||||||
@@ -11,76 +11,84 @@ app = typer.Typer()
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def kill():
|
def kill():
|
||||||
result = subprocess.run(args + ["kill"], capture_output=True)
|
result = subprocess.run([*args, "kill"], capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
sys.stderr.write("No running instance to kill.\n")
|
sys.stderr.write("No running instance to kill.\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.stderr.write(result.stderr.decode())
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
|
||||||
|
|
||||||
def start_instance(no_daemon: bool = False) -> None:
|
def start_instance(no_daemon: bool = False) -> None:
|
||||||
result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True)
|
result = subprocess.run(
|
||||||
stdout = result.stdout.decode().strip()
|
args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True
|
||||||
if stdout:
|
)
|
||||||
if "already running" in stdout.lower():
|
stdout = result.stdout.decode().strip()
|
||||||
sys.stderr.write(stdout + "\n")
|
if stdout and "already running" in stdout.lower():
|
||||||
sys.exit(1)
|
sys.stderr.write(stdout + "\n")
|
||||||
if result.returncode != 0:
|
sys.exit(1)
|
||||||
stderr = result.stderr.decode().strip()
|
if result.returncode != 0:
|
||||||
sys.stderr.write(stderr + "\n")
|
stderr = result.stderr.decode().strip()
|
||||||
sys.exit(1)
|
sys.stderr.write(stderr + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def start(no_daemon: bool = False):
|
def start(no_daemon: bool = False):
|
||||||
start_instance(no_daemon)
|
start_instance(no_daemon)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def restart(no_daemon: bool = False):
|
def restart(no_daemon: bool = False):
|
||||||
subprocess.run(args + ["kill"], capture_output=True)
|
subprocess.run([*args, "kill"], capture_output=True)
|
||||||
deadline = time.monotonic() + 2.5
|
deadline = time.monotonic() + 2.5
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
result = subprocess.run(args + ["kill"], capture_output=True)
|
result = subprocess.run([*args, "kill"], capture_output=True)
|
||||||
if result.returncode == 255:
|
if result.returncode == 255:
|
||||||
break
|
break
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
start_instance(no_daemon=no_daemon)
|
start_instance(no_daemon=no_daemon)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def show():
|
def show():
|
||||||
result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True)
|
result = subprocess.run([*args, "ipc", "show"], capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
sys.stderr.write(result.stderr.decode())
|
sys.stderr.write(result.stderr.decode())
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.stdout.write(result.stdout.decode())
|
sys.stdout.write(result.stdout.decode())
|
||||||
sys.stderr.write(result.stderr.decode())
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def log():
|
def log():
|
||||||
result = subprocess.run(args + ["log"], capture_output=True)
|
result = subprocess.run([*args, "log"], capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
sys.stderr.write(result.stderr.decode())
|
sys.stderr.write(result.stderr.decode())
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.stdout.write(result.stdout.decode())
|
sys.stdout.write(result.stdout.decode())
|
||||||
sys.stderr.write(result.stderr.decode())
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def lock():
|
def lock():
|
||||||
result = subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], capture_output=True)
|
result = subprocess.run(
|
||||||
if result.returncode != 0:
|
[*args, "ipc", "call", "lock", "lock"], capture_output=True
|
||||||
sys.stderr.write(result.stderr.decode())
|
)
|
||||||
sys.exit(1)
|
if result.returncode != 0:
|
||||||
sys.stderr.write(result.stderr.decode())
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
sys.exit(1)
|
||||||
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def call(target: str, method: str, method_args: list[str] = typer.Argument(None)):
|
def call(
|
||||||
result = subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), capture_output=True)
|
target: str, method: str, method_args: list[str] = typer.Argument(None)
|
||||||
if result.returncode != 0:
|
):
|
||||||
sys.stderr.write(result.stderr.decode())
|
result = subprocess.run(
|
||||||
sys.exit(1)
|
args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []),
|
||||||
sys.stderr.write(result.stderr.decode())
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
sys.exit(1)
|
||||||
|
sys.stderr.write(result.stderr.decode())
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import typer
|
|
||||||
|
|
||||||
from typing import Annotated
|
|
||||||
from PIL import Image, ImageFilter
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from PIL import Image, ImageFilter
|
||||||
|
|
||||||
args = ["qs", "-c", "zshell"]
|
args = ["qs", "-c", "zshell"]
|
||||||
|
|
||||||
@@ -12,32 +12,35 @@ app = typer.Typer()
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def set(wallpaper: Path):
|
def set(wallpaper: Path):
|
||||||
subprocess.run(args + ["ipc"] + ["call"] + ["wallpaper"] + ["set"] + [wallpaper], check=True)
|
subprocess.run(
|
||||||
|
[*args, "ipc", "call", "wallpaper", "set", wallpaper],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def lockscreen(
|
def lockscreen(
|
||||||
input_image: Annotated[
|
input_image: Annotated[
|
||||||
Path,
|
Path,
|
||||||
typer.Option(),
|
typer.Option(),
|
||||||
],
|
],
|
||||||
output_path: Annotated[
|
output_path: Annotated[
|
||||||
Path,
|
Path,
|
||||||
typer.Option(),
|
typer.Option(),
|
||||||
],
|
],
|
||||||
blur_amount: int = 20,
|
blur_amount: int = 20,
|
||||||
):
|
):
|
||||||
img = Image.open(input_image)
|
img = Image.open(input_image)
|
||||||
size = img.size
|
size = img.size
|
||||||
if blur_amount == 0:
|
if blur_amount == 0:
|
||||||
img.save(output_path, "PNG")
|
img.save(output_path, "PNG")
|
||||||
return
|
return
|
||||||
|
|
||||||
if size[0] < 3840 or size[1] < 2160:
|
if size[0] < 3840 or size[1] < 2160:
|
||||||
img = img.resize((size[0] // 2, size[1] // 2), Image.Resampling.NEAREST)
|
img = img.resize((size[0] // 2, size[1] // 2), Image.Resampling.NEAREST)
|
||||||
else:
|
else:
|
||||||
img = img.resize((size[0] // 4, size[1] // 4), Image.Resampling.NEAREST)
|
img = img.resize((size[0] // 4, size[1] // 4), Image.Resampling.NEAREST)
|
||||||
|
|
||||||
img = img.filter(ImageFilter.GaussianBlur(blur_amount))
|
img = img.filter(ImageFilter.GaussianBlur(blur_amount))
|
||||||
|
|
||||||
img.save(output_path, "PNG")
|
img.save(output_path, "PNG")
|
||||||
|
|||||||
@@ -10,137 +10,140 @@ ASSETS: Traversable = files("zshell") / "assets" / "schemes"
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SchemeVariant:
|
class SchemeVariant:
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
modes: frozenset[str]
|
modes: frozenset[str]
|
||||||
accents: tuple[str, ...] = ()
|
accents: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SchemeMeta:
|
class SchemeMeta:
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
variants: tuple[SchemeVariant, ...]
|
variants: tuple[SchemeVariant, ...]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Palette:
|
class Palette:
|
||||||
colors: dict[str, str]
|
colors: dict[str, str]
|
||||||
mode: str
|
mode: str
|
||||||
scheme: str
|
scheme: str
|
||||||
variant: str
|
variant: str
|
||||||
accent: str | None = None
|
accent: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def _parse_txt(path: Traversable) -> dict[str, str]:
|
def _parse_txt(path: Traversable) -> dict[str, str]:
|
||||||
colors: dict[str, str] = {}
|
colors: dict[str, str] = {}
|
||||||
for line in path.read_text().splitlines():
|
for line in path.read_text().splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
parts = line.split(None, 1)
|
parts = line.split(None, 1)
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
key, val = parts
|
key, val = parts
|
||||||
colors[key] = f"#{val}" if not val.startswith("#") else val
|
colors[key] = f"#{val}" if not val.startswith("#") else val
|
||||||
return colors
|
return colors
|
||||||
|
|
||||||
|
|
||||||
def _discover_schemes() -> dict[str, SchemeMeta]:
|
def _discover_schemes() -> dict[str, SchemeMeta]:
|
||||||
schemes: dict[str, SchemeMeta] = {}
|
schemes: dict[str, SchemeMeta] = {}
|
||||||
|
|
||||||
for scheme_dir in sorted(ASSETS.iterdir(), key=lambda p: p.name):
|
for scheme_dir in sorted(ASSETS.iterdir(), key=lambda p: p.name):
|
||||||
if not scheme_dir.is_dir() or scheme_dir.name.startswith("."):
|
if not scheme_dir.is_dir() or scheme_dir.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sid = scheme_dir.name
|
sid = scheme_dir.name
|
||||||
display_name = sid.capitalize()
|
display_name = sid.capitalize()
|
||||||
|
|
||||||
variants: list[SchemeVariant] = []
|
variants: list[SchemeVariant] = []
|
||||||
for var_dir in sorted(scheme_dir.iterdir(), key=lambda p: p.name):
|
for var_dir in sorted(scheme_dir.iterdir(), key=lambda p: p.name):
|
||||||
if not var_dir.is_dir() or var_dir.name.startswith("."):
|
if not var_dir.is_dir() or var_dir.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
modes: set[str] = set()
|
modes: set[str] = set()
|
||||||
accents: set[str] = set()
|
accents: set[str] = set()
|
||||||
|
|
||||||
for f in var_dir.iterdir():
|
for f in var_dir.iterdir():
|
||||||
name = PurePosixPath(f.name)
|
name = PurePosixPath(f.name)
|
||||||
if name.suffix != ".txt":
|
if name.suffix != ".txt":
|
||||||
continue
|
continue
|
||||||
stem = name.stem
|
stem = name.stem
|
||||||
if "-" in stem:
|
if "-" in stem:
|
||||||
maybe_accent, maybe_mode = stem.rsplit("-", 1)
|
maybe_accent, maybe_mode = stem.rsplit("-", 1)
|
||||||
if maybe_mode in ("dark", "light"):
|
if maybe_mode in ("dark", "light"):
|
||||||
modes.add(maybe_mode)
|
modes.add(maybe_mode)
|
||||||
accents.add(maybe_accent)
|
accents.add(maybe_accent)
|
||||||
else:
|
else:
|
||||||
modes.add(stem)
|
modes.add(stem)
|
||||||
else:
|
else:
|
||||||
if stem in ("dark", "light"):
|
if stem in ("dark", "light"):
|
||||||
modes.add(stem)
|
modes.add(stem)
|
||||||
|
|
||||||
if modes:
|
if modes:
|
||||||
vname = var_dir.name.capitalize()
|
vname = var_dir.name.capitalize()
|
||||||
variants.append(
|
variants.append(
|
||||||
SchemeVariant(
|
SchemeVariant(
|
||||||
id=var_dir.name,
|
id=var_dir.name,
|
||||||
name=vname,
|
name=vname,
|
||||||
modes=frozenset(modes),
|
modes=frozenset(modes),
|
||||||
accents=tuple(sorted(accents)),
|
accents=tuple(sorted(accents)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
schemes[sid] = SchemeMeta(
|
schemes[sid] = SchemeMeta(
|
||||||
id=sid,
|
id=sid,
|
||||||
name=display_name,
|
name=display_name,
|
||||||
variants=tuple(variants),
|
variants=tuple(variants),
|
||||||
)
|
)
|
||||||
|
|
||||||
return schemes
|
return schemes
|
||||||
|
|
||||||
|
|
||||||
SCHEMES: dict[str, SchemeMeta] = _discover_schemes()
|
SCHEMES: dict[str, SchemeMeta] = _discover_schemes()
|
||||||
|
|
||||||
|
|
||||||
def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None) -> Palette:
|
def get_palette(
|
||||||
if scheme not in SCHEMES:
|
scheme: str, variant: str, mode: str, accent: str | None = None
|
||||||
raise KeyError(
|
) -> Palette:
|
||||||
f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}")
|
if scheme not in SCHEMES:
|
||||||
|
raise KeyError(
|
||||||
|
f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}"
|
||||||
|
)
|
||||||
|
|
||||||
meta = SCHEMES[scheme]
|
meta = SCHEMES[scheme]
|
||||||
var_ids = {v.id for v in meta.variants}
|
var_ids = {v.id for v in meta.variants}
|
||||||
if variant not in var_ids:
|
if variant not in var_ids:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
f"Unknown variant '{variant}' for scheme '{scheme}'. Available: {', '.join(sorted(var_ids))}")
|
f"Unknown variant '{variant}' for scheme '{scheme}'. Available: {', '.join(sorted(var_ids))}"
|
||||||
|
)
|
||||||
|
|
||||||
if accent:
|
filename = f"{accent}-{mode}.txt" if accent else f"{mode}.txt"
|
||||||
filename = f"{accent}-{mode}.txt"
|
|
||||||
else:
|
|
||||||
filename = f"{mode}.txt"
|
|
||||||
|
|
||||||
txt_path = ASSETS / scheme / variant / filename
|
txt_path = ASSETS / scheme / variant / filename
|
||||||
if not txt_path.is_file():
|
if not txt_path.is_file():
|
||||||
txt_path = ASSETS / scheme / variant / f"{mode}.txt"
|
txt_path = ASSETS / scheme / variant / f"{mode}.txt"
|
||||||
|
|
||||||
if not txt_path.is_file():
|
if not txt_path.is_file():
|
||||||
var_info = next(v for v in meta.variants if v.id == variant)
|
var_info = next(v for v in meta.variants if v.id == variant)
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"No {mode} palette for '{scheme}:{variant}'. Available modes: {sorted(var_info.modes)}"
|
f"No {mode} palette for '{scheme}:{variant}'. Available modes: {sorted(var_info.modes)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
colors = _parse_txt(txt_path)
|
colors = _parse_txt(txt_path)
|
||||||
|
|
||||||
return Palette(colors=colors, mode=mode, scheme=scheme, variant=variant, accent=accent)
|
return Palette(
|
||||||
|
colors=colors, mode=mode, scheme=scheme, variant=variant, accent=accent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_schemes() -> dict[str, SchemeMeta]:
|
def list_schemes() -> dict[str, SchemeMeta]:
|
||||||
return dict(SCHEMES)
|
return dict(SCHEMES)
|
||||||
|
|
||||||
|
|
||||||
def resolve_preset(spec: str) -> tuple[str, str]:
|
def resolve_preset(spec: str) -> tuple[str, str]:
|
||||||
parts = spec.split(":")
|
parts = spec.split(":")
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
return parts[0], parts[1]
|
return parts[0], parts[1]
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return parts[0], "default"
|
return parts[0], "default"
|
||||||
raise ValueError(f"Invalid preset spec '{spec}'. Use <scheme>:<variant>")
|
raise ValueError(f"Invalid preset spec '{spec}'. Use <scheme>:<variant>")
|
||||||
|
|||||||
+149
-121
@@ -1,167 +1,195 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
from zshell.utils import schemepalettes as sp
|
from zshell.utils import schemepalettes as sp
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tmp_schemes(tmp_path: Path) -> Path:
|
def tmp_schemes(tmp_path: Path) -> Path:
|
||||||
schemes = tmp_path / "schemes"
|
schemes = tmp_path / "schemes"
|
||||||
schemes.mkdir()
|
schemes.mkdir()
|
||||||
|
|
||||||
gmedium = schemes / "gruvbox" / "medium"
|
gmedium = schemes / "gruvbox" / "medium"
|
||||||
gmedium.mkdir(parents=True)
|
gmedium.mkdir(parents=True)
|
||||||
(gmedium / "dark.txt").write_text("background 101415\nonBackground e0e3e4\nprimary 81d3e0\nsurface 1c2021\n")
|
(gmedium / "dark.txt").write_text(
|
||||||
(gmedium / "light.txt").write_text("background fbf1c7\nonBackground 3c3836\nprimary 6b5f10\nsurface fbf1c7\n")
|
"background 101415\nonBackground e0e3e4\nprimary 81d3e0\nsurface 1c2021\n"
|
||||||
|
)
|
||||||
|
(gmedium / "light.txt").write_text(
|
||||||
|
"background fbf1c7\nonBackground 3c3836\nprimary 6b5f10\nsurface fbf1c7\n"
|
||||||
|
)
|
||||||
|
|
||||||
ghard = schemes / "gruvbox" / "hard"
|
ghard = schemes / "gruvbox" / "hard"
|
||||||
ghard.mkdir(parents=True)
|
ghard.mkdir(parents=True)
|
||||||
(ghard / "dark.txt").write_text("background 0b0d0e\nprimary 81d3e0\n")
|
(ghard / "dark.txt").write_text("background 0b0d0e\nprimary 81d3e0\n")
|
||||||
|
|
||||||
cmocha = schemes / "catppuccin" / "mocha"
|
cmocha = schemes / "catppuccin" / "mocha"
|
||||||
cmocha.mkdir(parents=True)
|
cmocha.mkdir(parents=True)
|
||||||
(cmocha / "dark.txt").write_text("background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n")
|
(cmocha / "dark.txt").write_text(
|
||||||
(cmocha / "mauve-dark.txt").write_text("background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n")
|
"background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n"
|
||||||
(cmocha / "green-dark.txt").write_text("background 1e1e2e\nprimary a6e3a1\nsecondary 5b8964\nsurface 313244\n")
|
)
|
||||||
|
(cmocha / "mauve-dark.txt").write_text(
|
||||||
|
"background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n"
|
||||||
|
)
|
||||||
|
(cmocha / "green-dark.txt").write_text(
|
||||||
|
"background 1e1e2e\nprimary a6e3a1\nsecondary 5b8964\nsurface 313244\n"
|
||||||
|
)
|
||||||
|
|
||||||
clatte = schemes / "catppuccin" / "latte"
|
clatte = schemes / "catppuccin" / "latte"
|
||||||
clatte.mkdir(parents=True)
|
clatte.mkdir(parents=True)
|
||||||
(clatte / "light.txt").write_text("background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n")
|
(clatte / "light.txt").write_text(
|
||||||
(clatte / "mauve-light.txt").write_text("background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n")
|
"background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n"
|
||||||
|
)
|
||||||
|
(clatte / "mauve-light.txt").write_text(
|
||||||
|
"background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n"
|
||||||
|
)
|
||||||
|
|
||||||
cextra = schemes / "extra" / "default"
|
cextra = schemes / "extra" / "default"
|
||||||
cextra.mkdir(parents=True)
|
cextra.mkdir(parents=True)
|
||||||
(cextra / "dark.txt").write_text(
|
(cextra / "dark.txt").write_text(
|
||||||
"# this is a comment\n\nbackground 000000\nprimary ffffff\n\n # indented comment \n secondary cccccc\n"
|
"# this is a comment\n\nbackground 000000\nprimary ffffff\n\n # indented comment \n secondary cccccc\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
return schemes
|
return schemes
|
||||||
|
|
||||||
|
|
||||||
class TestParseTxt:
|
class TestParseTxt:
|
||||||
def test_basic(self, tmp_schemes):
|
def test_basic(self, tmp_schemes):
|
||||||
path = tmp_schemes / "gruvbox" / "medium" / "dark.txt"
|
path = tmp_schemes / "gruvbox" / "medium" / "dark.txt"
|
||||||
colors = sp._parse_txt(path)
|
colors = sp._parse_txt(path)
|
||||||
assert colors["background"] == "#101415"
|
assert colors["background"] == "#101415"
|
||||||
assert colors["primary"] == "#81d3e0"
|
assert colors["primary"] == "#81d3e0"
|
||||||
assert colors["surface"] == "#1c2021"
|
assert colors["surface"] == "#1c2021"
|
||||||
|
|
||||||
def test_adds_hash_prefix(self, tmp_schemes):
|
def test_adds_hash_prefix(self, tmp_schemes):
|
||||||
path = tmp_schemes / "gruvbox" / "medium" / "dark.txt"
|
path = tmp_schemes / "gruvbox" / "medium" / "dark.txt"
|
||||||
colors = sp._parse_txt(path)
|
colors = sp._parse_txt(path)
|
||||||
for v in colors.values():
|
for v in colors.values():
|
||||||
assert v.startswith("#"), f"value {v!r} missing # prefix"
|
assert v.startswith("#"), f"value {v!r} missing # prefix"
|
||||||
|
|
||||||
def test_skips_comments_and_empty_lines(self, tmp_schemes):
|
def test_skips_comments_and_empty_lines(self, tmp_schemes):
|
||||||
path = tmp_schemes / "extra" / "default" / "dark.txt"
|
path = tmp_schemes / "extra" / "default" / "dark.txt"
|
||||||
colors = sp._parse_txt(path)
|
colors = sp._parse_txt(path)
|
||||||
assert colors["background"] == "#000000"
|
assert colors["background"] == "#000000"
|
||||||
assert colors["primary"] == "#ffffff"
|
assert colors["primary"] == "#ffffff"
|
||||||
assert colors["secondary"] == "#cccccc"
|
assert colors["secondary"] == "#cccccc"
|
||||||
assert len(colors) == 3
|
assert len(colors) == 3
|
||||||
|
|
||||||
|
|
||||||
class TestDiscoverSchemes:
|
class TestDiscoverSchemes:
|
||||||
def test_discovers_all_schemes(self):
|
def test_discovers_all_schemes(self):
|
||||||
schemes = sp._discover_schemes()
|
schemes = sp._discover_schemes()
|
||||||
assert "gruvbox" in schemes
|
assert "gruvbox" in schemes
|
||||||
assert "catppuccin" in schemes
|
assert "catppuccin" in schemes
|
||||||
assert "everforest" in schemes
|
assert "everforest" in schemes
|
||||||
assert "nord" in schemes
|
assert "nord" in schemes
|
||||||
assert len(schemes) >= 10
|
assert len(schemes) >= 10
|
||||||
|
|
||||||
def test_scheme_has_variants(self):
|
def test_scheme_has_variants(self):
|
||||||
schemes = sp._discover_schemes()
|
schemes = sp._discover_schemes()
|
||||||
gruvbox = schemes["gruvbox"]
|
gruvbox = schemes["gruvbox"]
|
||||||
var_ids = {v.id for v in gruvbox.variants}
|
var_ids = {v.id for v in gruvbox.variants}
|
||||||
assert "medium" in var_ids
|
assert "medium" in var_ids
|
||||||
assert "hard" in var_ids
|
assert "hard" in var_ids
|
||||||
assert "soft" in var_ids
|
assert "soft" in var_ids
|
||||||
|
|
||||||
def test_variant_has_modes(self):
|
def test_variant_has_modes(self):
|
||||||
schemes = sp._discover_schemes()
|
schemes = sp._discover_schemes()
|
||||||
gmedium = next(v for v in schemes["gruvbox"].variants if v.id == "medium")
|
gmedium = next(
|
||||||
assert "dark" in gmedium.modes
|
v for v in schemes["gruvbox"].variants if v.id == "medium"
|
||||||
assert "light" in gmedium.modes
|
)
|
||||||
|
assert "dark" in gmedium.modes
|
||||||
|
assert "light" in gmedium.modes
|
||||||
|
|
||||||
def test_catppuccin_has_accents(self):
|
def test_catppuccin_has_accents(self):
|
||||||
schemes = sp._discover_schemes()
|
schemes = sp._discover_schemes()
|
||||||
mocha = next(v for v in schemes["catppuccin"].variants if v.id == "mocha")
|
mocha = next(
|
||||||
assert "mauve" in mocha.accents
|
v for v in schemes["catppuccin"].variants if v.id == "mocha"
|
||||||
assert "green" in mocha.accents
|
)
|
||||||
assert "rosewater" in mocha.accents
|
assert "mauve" in mocha.accents
|
||||||
assert len(mocha.accents) >= 14
|
assert "green" in mocha.accents
|
||||||
|
assert "rosewater" in mocha.accents
|
||||||
|
assert len(mocha.accents) >= 14
|
||||||
|
|
||||||
def test_non_accent_scheme_has_no_accents(self):
|
def test_non_accent_scheme_has_no_accents(self):
|
||||||
schemes = sp._discover_schemes()
|
schemes = sp._discover_schemes()
|
||||||
gmedium = next(v for v in schemes["gruvbox"].variants if v.id == "medium")
|
gmedium = next(
|
||||||
assert gmedium.accents == ()
|
v for v in schemes["gruvbox"].variants if v.id == "medium"
|
||||||
|
)
|
||||||
|
assert gmedium.accents == ()
|
||||||
|
|
||||||
|
|
||||||
class TestGetPalette:
|
class TestGetPalette:
|
||||||
def test_loads_basic_palette(self):
|
def test_loads_basic_palette(self):
|
||||||
pal = sp.get_palette("gruvbox", "medium", "dark")
|
pal = sp.get_palette("gruvbox", "medium", "dark")
|
||||||
assert pal.scheme == "gruvbox"
|
assert pal.scheme == "gruvbox"
|
||||||
assert pal.variant == "medium"
|
assert pal.variant == "medium"
|
||||||
assert pal.mode == "dark"
|
assert pal.mode == "dark"
|
||||||
assert pal.colors["background"].startswith("#")
|
assert pal.colors["background"].startswith("#")
|
||||||
assert pal.colors["primary"].startswith("#")
|
assert pal.colors["primary"].startswith("#")
|
||||||
|
|
||||||
def test_loads_accent_palette(self):
|
def test_loads_accent_palette(self):
|
||||||
pal = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve")
|
pal = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve")
|
||||||
assert pal.accent == "mauve"
|
assert pal.accent == "mauve"
|
||||||
assert pal.colors["primary"] == "#cba6f7"
|
assert pal.colors["primary"] == "#cba6f7"
|
||||||
|
|
||||||
def test_different_accent_changes_colors(self):
|
def test_different_accent_changes_colors(self):
|
||||||
mauve = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve")
|
mauve = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve")
|
||||||
green = sp.get_palette("catppuccin", "mocha", "dark", accent="green")
|
green = sp.get_palette("catppuccin", "mocha", "dark", accent="green")
|
||||||
assert mauve.colors["primary"] != green.colors["primary"]
|
assert mauve.colors["primary"] != green.colors["primary"]
|
||||||
assert mauve.colors["secondary"] != green.colors["secondary"]
|
assert mauve.colors["secondary"] != green.colors["secondary"]
|
||||||
|
|
||||||
def test_unknown_scheme_raises(self):
|
def test_unknown_scheme_raises(self):
|
||||||
with pytest.raises(KeyError, match="Unknown scheme 'nope'"):
|
with pytest.raises(KeyError, match="Unknown scheme 'nope'"):
|
||||||
sp.get_palette("nope", "medium", "dark")
|
sp.get_palette("nope", "medium", "dark")
|
||||||
|
|
||||||
def test_unknown_variant_raises(self):
|
def test_unknown_variant_raises(self):
|
||||||
with pytest.raises(KeyError, match="Unknown variant 'bogus' for scheme 'gruvbox'"):
|
with pytest.raises(
|
||||||
sp.get_palette("gruvbox", "bogus", "dark")
|
KeyError, match="Unknown variant 'bogus' for scheme 'gruvbox'"
|
||||||
|
):
|
||||||
|
sp.get_palette("gruvbox", "bogus", "dark")
|
||||||
|
|
||||||
def test_unknown_accent_falls_back(self):
|
def test_unknown_accent_falls_back(self):
|
||||||
pal = sp.get_palette("catppuccin", "mocha", "dark", accent="nonexistent")
|
pal = sp.get_palette(
|
||||||
assert pal.accent == "nonexistent"
|
"catppuccin", "mocha", "dark", accent="nonexistent"
|
||||||
assert pal.colors["primary"] is not None
|
)
|
||||||
|
assert pal.accent == "nonexistent"
|
||||||
|
assert pal.colors["primary"] is not None
|
||||||
|
|
||||||
def test_accent_on_non_accent_scheme(self):
|
def test_accent_on_non_accent_scheme(self):
|
||||||
pal = sp.get_palette("gruvbox", "medium", "dark", accent="mauve")
|
pal = sp.get_palette("gruvbox", "medium", "dark", accent="mauve")
|
||||||
assert pal.colors is not None
|
assert pal.colors is not None
|
||||||
|
|
||||||
def test_non_existent_mode_raises(self):
|
def test_non_existent_mode_raises(self):
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
sp.get_palette("catppuccin", "mocha", "light")
|
sp.get_palette("catppuccin", "mocha", "light")
|
||||||
|
|
||||||
|
|
||||||
class TestListSchemes:
|
class TestListSchemes:
|
||||||
def test_returns_dict(self):
|
def test_returns_dict(self):
|
||||||
schemes = sp.list_schemes()
|
schemes = sp.list_schemes()
|
||||||
assert isinstance(schemes, dict)
|
assert isinstance(schemes, dict)
|
||||||
|
|
||||||
def test_includes_known_schemes(self):
|
def test_includes_known_schemes(self):
|
||||||
schemes = sp.list_schemes()
|
schemes = sp.list_schemes()
|
||||||
assert "catppuccin" in schemes
|
assert "catppuccin" in schemes
|
||||||
assert "gruvbox" in schemes
|
assert "gruvbox" in schemes
|
||||||
|
|
||||||
|
|
||||||
class TestResolvePreset:
|
class TestResolvePreset:
|
||||||
def test_two_parts(self):
|
def test_two_parts(self):
|
||||||
assert sp.resolve_preset("gruvbox:medium") == ("gruvbox", "medium")
|
assert sp.resolve_preset("gruvbox:medium") == ("gruvbox", "medium")
|
||||||
|
|
||||||
def test_three_parts(self):
|
def test_three_parts(self):
|
||||||
with pytest.raises(ValueError, match="Invalid preset spec"):
|
with pytest.raises(ValueError, match="Invalid preset spec"):
|
||||||
sp.resolve_preset("catppuccin:mocha:mauve")
|
sp.resolve_preset("catppuccin:mocha:mauve")
|
||||||
|
|
||||||
def test_one_part(self):
|
def test_one_part(self):
|
||||||
assert sp.resolve_preset("default") == ("default", "default")
|
assert sp.resolve_preset("default") == ("default", "default")
|
||||||
|
|
||||||
def test_edge_spaces(self):
|
def test_edge_spaces(self):
|
||||||
assert sp.resolve_preset(" catppuccin : mocha ") == (" catppuccin ", " mocha ")
|
assert sp.resolve_preset(" catppuccin : mocha ") == (
|
||||||
|
" catppuccin ",
|
||||||
|
" mocha ",
|
||||||
|
)
|
||||||
|
|||||||
+134
-93
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from subprocess import CompletedProcess
|
from subprocess import CompletedProcess
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
from zshell.subcommands.shell import app
|
from zshell.subcommands.shell import app
|
||||||
@@ -10,122 +10,163 @@ runner = CliRunner()
|
|||||||
|
|
||||||
|
|
||||||
def invoke(*args: str):
|
def invoke(*args: str):
|
||||||
result = runner.invoke(app, args)
|
result = runner.invoke(app, args)
|
||||||
if result.exit_code != 0:
|
if result.exit_code != 0:
|
||||||
raise RuntimeError(result.output)
|
raise RuntimeError(result.output)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class TestKill:
|
class TestKill:
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_kill_runs_qs_kill_success(self, mock_run):
|
def test_kill_runs_qs_kill_success(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n")
|
mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n")
|
||||||
invoke("kill")
|
invoke("kill")
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "kill"], capture_output=True)
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "kill"], capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_kill_no_instance_errors(self, mock_run):
|
def test_kill_no_instance_errors(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 255, b"", b"No running instances\n")
|
mock_run.return_value = CompletedProcess(
|
||||||
result = runner.invoke(app, ["kill"])
|
[], 255, b"", b"No running instances\n"
|
||||||
assert result.exit_code != 0
|
)
|
||||||
assert "No running instance to kill" in result.output
|
result = runner.invoke(app, ["kill"])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "No running instance to kill" in result.output
|
||||||
|
|
||||||
|
|
||||||
class TestStart:
|
class TestStart:
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_start_default_daemon(self, mock_run):
|
def test_start_default_daemon(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n")
|
mock_run.return_value = CompletedProcess(
|
||||||
invoke("start")
|
[], 0, b"", b"Launching config\n"
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], capture_output=True)
|
)
|
||||||
|
invoke("start")
|
||||||
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "-n", "-d"], capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_start_no_daemon(self, mock_run):
|
def test_start_no_daemon(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n")
|
mock_run.return_value = CompletedProcess(
|
||||||
invoke("start", "--no-daemon")
|
[], 0, b"", b"Launching config\n"
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], capture_output=True)
|
)
|
||||||
|
invoke("start", "--no-daemon")
|
||||||
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "-n"], capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_start_already_running_errors(self, mock_run):
|
def test_start_already_running_errors(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"An instance of this configuration is already running.\n", b"")
|
mock_run.return_value = CompletedProcess(
|
||||||
result = runner.invoke(app, ["start"])
|
[],
|
||||||
assert result.exit_code != 0
|
0,
|
||||||
assert "already running" in result.output
|
b"An instance of this configuration is already running.\n",
|
||||||
|
b"",
|
||||||
|
)
|
||||||
|
result = runner.invoke(app, ["start"])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "already running" in result.output
|
||||||
|
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_start_other_failure_errors(self, mock_run):
|
def test_start_other_failure_errors(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 1, b"", b"Config error\n")
|
mock_run.return_value = CompletedProcess([], 1, b"", b"Config error\n")
|
||||||
result = runner.invoke(app, ["start"])
|
result = runner.invoke(app, ["start"])
|
||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
assert "Config error" in result.output
|
assert "Config error" in result.output
|
||||||
|
|
||||||
|
|
||||||
class TestShow:
|
class TestShow:
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_show_runs_ipc_show(self, mock_run):
|
def test_show_runs_ipc_show(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"target visibilities\n", b"")
|
mock_run.return_value = CompletedProcess(
|
||||||
result = invoke("show")
|
[], 0, b"target visibilities\n", b""
|
||||||
assert "target visibilities" in result.output
|
)
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True)
|
result = invoke("show")
|
||||||
|
assert "target visibilities" in result.output
|
||||||
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "ipc", "show"], capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestLog:
|
class TestLog:
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_log_runs_qs_log(self, mock_run):
|
def test_log_runs_qs_log(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"")
|
mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"")
|
||||||
invoke("log")
|
invoke("log")
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "log"], capture_output=True)
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "log"], capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestLock:
|
class TestLock:
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_lock_runs_ipc_call_lock(self, mock_run):
|
def test_lock_runs_ipc_call_lock(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
||||||
invoke("lock")
|
invoke("lock")
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "lock", "lock"], capture_output=True)
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "ipc", "call", "lock", "lock"],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCall:
|
class TestCall:
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_call_no_args(self, mock_run):
|
def test_call_no_args(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
||||||
invoke("call", "target", "method")
|
invoke("call", "target", "method")
|
||||||
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "target", "method"], capture_output=True)
|
mock_run.assert_called_once_with(
|
||||||
|
["qs", "-c", "zshell", "ipc", "call", "target", "method"],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_call_with_args(self, mock_run):
|
def test_call_with_args(self, mock_run):
|
||||||
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
||||||
invoke("call", "target", "method", "arg1", "arg2")
|
invoke("call", "target", "method", "arg1", "arg2")
|
||||||
mock_run.assert_called_once_with(
|
mock_run.assert_called_once_with(
|
||||||
["qs", "-c", "zshell", "ipc", "call", "target", "method", "arg1", "arg2"],
|
[
|
||||||
capture_output=True,
|
"qs",
|
||||||
)
|
"-c",
|
||||||
|
"zshell",
|
||||||
|
"ipc",
|
||||||
|
"call",
|
||||||
|
"target",
|
||||||
|
"method",
|
||||||
|
"arg1",
|
||||||
|
"arg2",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestRestart:
|
class TestRestart:
|
||||||
@patch("zshell.subcommands.shell.start_instance")
|
@patch("zshell.subcommands.shell.start_instance")
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_restart_kills_then_starts(self, mock_run, mock_start):
|
def test_restart_kills_then_starts(self, mock_run, mock_start):
|
||||||
mock_run.side_effect = [
|
mock_run.side_effect = [
|
||||||
CompletedProcess([], 0, b"", b"Killed abc\n"), # first kill (captured)
|
CompletedProcess(
|
||||||
CompletedProcess([], 255, b"", b""), # poll → no instance
|
[], 0, b"", b"Killed abc\n"
|
||||||
]
|
), # first kill (captured)
|
||||||
invoke("restart")
|
CompletedProcess([], 255, b"", b""), # poll → no instance
|
||||||
assert mock_run.call_args_list == [
|
]
|
||||||
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
invoke("restart")
|
||||||
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
assert mock_run.call_args_list == [
|
||||||
]
|
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
||||||
mock_start.assert_called_once_with(no_daemon=False)
|
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
||||||
|
]
|
||||||
|
mock_start.assert_called_once_with(no_daemon=False)
|
||||||
|
|
||||||
@patch("zshell.subcommands.shell.start_instance")
|
@patch("zshell.subcommands.shell.start_instance")
|
||||||
@patch("zshell.subcommands.shell.subprocess.run")
|
@patch("zshell.subcommands.shell.subprocess.run")
|
||||||
def test_restart_no_daemon(self, mock_run, mock_start):
|
def test_restart_no_daemon(self, mock_run, mock_start):
|
||||||
mock_run.side_effect = [
|
mock_run.side_effect = [
|
||||||
CompletedProcess([], 0, b"", b"Killed abc\n"),
|
CompletedProcess([], 0, b"", b"Killed abc\n"),
|
||||||
CompletedProcess([], 255, b"", b""),
|
CompletedProcess([], 255, b"", b""),
|
||||||
]
|
]
|
||||||
invoke("restart", "--no-daemon")
|
invoke("restart", "--no-daemon")
|
||||||
assert mock_run.call_args_list == [
|
assert mock_run.call_args_list == [
|
||||||
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
||||||
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
call(["qs", "-c", "zshell", "kill"], capture_output=True),
|
||||||
]
|
]
|
||||||
mock_start.assert_called_once_with(no_daemon=True)
|
mock_start.assert_called_once_with(no_daemon=True)
|
||||||
|
|||||||
@@ -21,17 +21,39 @@ source = "vcs"
|
|||||||
|
|
||||||
[tool.hatch.build]
|
[tool.hatch.build]
|
||||||
include = [
|
include = [
|
||||||
"src/zshell/assets/**",
|
"cli/src/zshell/assets/**",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["cli/src/zshell"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
only-include = [
|
only-include = [
|
||||||
"src",
|
"cli/src",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 80
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "tab"
|
||||||
|
line-ending = "lf"
|
||||||
|
docstring-code-format = true
|
||||||
|
docstring-code-line-length = "dynamic"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
ignore = ["E501", "B008"]
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"I",
|
||||||
|
"UP",
|
||||||
|
"B",
|
||||||
|
"SIM",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["cli/tests"]
|
||||||
pythonpath = ["src"]
|
pythonpath = ["cli/src"]
|
||||||
+352
-307
@@ -4,22 +4,24 @@ import json
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import lru_cache
|
from functools import cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
@cache
|
||||||
def read_lines(path: Path) -> tuple[str, ...]:
|
def read_lines(path: Path) -> tuple[str, ...]:
|
||||||
return tuple(path.read_text().splitlines())
|
return tuple(path.read_text().splitlines())
|
||||||
|
|
||||||
|
|
||||||
ROW_RE = re.compile(
|
ROW_RE = re.compile(
|
||||||
r'^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|InfoRow|PopupRow|OverlayRow|DefaultRow)\s*\{')
|
r"^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|InfoRow|PopupRow|OverlayRow|DefaultRow)\s*\{"
|
||||||
|
)
|
||||||
LABEL_RE = re.compile(r'^\s*(?:label|text):\s*qsTr\("([^"]+)"\)')
|
LABEL_RE = re.compile(r'^\s*(?:label|text):\s*qsTr\("([^"]+)"\)')
|
||||||
ANCHOR_RE = re.compile(r'^\s*settingAnchor:\s*"([^"]+)"')
|
ANCHOR_RE = re.compile(r'^\s*settingAnchor:\s*"([^"]+)"')
|
||||||
CHECKED_RE = re.compile(r'^\s*checked:\s*(?:Config)\.([\w.]+)\s*$')
|
CHECKED_RE = re.compile(r"^\s*checked:\s*(?:Config)\.([\w.]+)\s*$")
|
||||||
ONTOGGLED_RE = re.compile(
|
ONTOGGLED_RE = re.compile(
|
||||||
r'^\s*onToggled:\s*(?:Config)\.([\w.]+)\s*=\s*checked\s*$')
|
r"^\s*onToggled:\s*(?:Config)\.([\w.]+)\s*=\s*checked\s*$"
|
||||||
|
)
|
||||||
ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
|
ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
|
||||||
SKIP_LABELS = {"Muted", "None"}
|
SKIP_LABELS = {"Muted", "None"}
|
||||||
FIELD_WEIGHT = {"title": 1.0, "keywords": 0.4}
|
FIELD_WEIGHT = {"title": 1.0, "keywords": 0.4}
|
||||||
@@ -27,14 +29,14 @@ STOPWORDS = {"the", "a", "an", "of", "and", "or", "to", "on", "in", "for"}
|
|||||||
|
|
||||||
|
|
||||||
def find_pages_dir(settings: Path) -> Path:
|
def find_pages_dir(settings: Path) -> Path:
|
||||||
return settings / "Pages"
|
return settings / "Pages"
|
||||||
|
|
||||||
|
|
||||||
def discover_files(settings: Path) -> dict[str, Path]:
|
def discover_files(settings: Path) -> dict[str, Path]:
|
||||||
files: dict[str, Path] = {}
|
files: dict[str, Path] = {}
|
||||||
for p in find_pages_dir(settings).rglob("*.qml"):
|
for p in find_pages_dir(settings).rglob("*.qml"):
|
||||||
files[p.stem] = p
|
files[p.stem] = p
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
PAGE_NAME_RE = re.compile(r'^\s*name:\s*qsTr\("([^"]+)"\)')
|
PAGE_NAME_RE = re.compile(r'^\s*name:\s*qsTr\("([^"]+)"\)')
|
||||||
@@ -42,366 +44,409 @@ PAGE_ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
|
|||||||
|
|
||||||
|
|
||||||
def parse_page_registry(settings: Path) -> list[tuple[str, str]]:
|
def parse_page_registry(settings: Path) -> list[tuple[str, str]]:
|
||||||
text = (settings / "PageRegistry.qml").read_text().splitlines()
|
text = (settings / "PageRegistry.qml").read_text().splitlines()
|
||||||
|
|
||||||
start = next(
|
start = next(
|
||||||
i for i, line in enumerate(text)
|
i for i, line in enumerate(text) if re.search(r"\bpages\s*:\s*\[", line)
|
||||||
if re.search(r'\bpages\s*:\s*\[', line)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
out: list[tuple[str, str]] = []
|
out: list[tuple[str, str]] = []
|
||||||
i = start + 1
|
i = start + 1
|
||||||
|
|
||||||
while i < len(text):
|
while i < len(text):
|
||||||
line = text[i].strip()
|
line = text[i].strip()
|
||||||
|
|
||||||
if line.startswith("]"):
|
if line.startswith("]"):
|
||||||
break
|
break
|
||||||
|
|
||||||
if line.startswith("//") or not line:
|
if line.startswith("//") or not line:
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith("{"):
|
if line.startswith("{"):
|
||||||
name = None
|
name = None
|
||||||
icon = None
|
icon = None
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
while i < len(text):
|
while i < len(text):
|
||||||
s = text[i].strip()
|
s = text[i].strip()
|
||||||
|
|
||||||
if s.startswith("}"):
|
if s.startswith("}"):
|
||||||
if name is not None:
|
if name is not None:
|
||||||
out.append((icon or "tune", name))
|
out.append((icon or "tune", name))
|
||||||
break
|
break
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
m = PAGE_NAME_RE.match(text[i])
|
m = PAGE_NAME_RE.match(text[i])
|
||||||
if m:
|
if m:
|
||||||
name = m.group(1)
|
name = m.group(1)
|
||||||
|
|
||||||
if icon is None:
|
if icon is None:
|
||||||
mi = PAGE_ICON_RE.match(text[i])
|
mi = PAGE_ICON_RE.match(text[i])
|
||||||
if mi:
|
if mi:
|
||||||
icon = mi.group(1)
|
icon = mi.group(1)
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
BLOCK_RE = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$')
|
BLOCK_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$")
|
||||||
|
|
||||||
|
|
||||||
def _strip_comment(line: str) -> str:
|
def _strip_comment(line: str) -> str:
|
||||||
return line.split("//", 1)[0].rstrip()
|
return line.split("//", 1)[0].rstrip()
|
||||||
|
|
||||||
|
|
||||||
def parse_block(lines: list[str], i: int) -> tuple[str, list[tuple[str, list]], int]:
|
def parse_block(
|
||||||
line = _strip_comment(lines[i]).strip()
|
lines: list[str], i: int
|
||||||
m = BLOCK_RE.match(line)
|
) -> tuple[str, list[tuple[str, list]], int]:
|
||||||
if not m:
|
line = _strip_comment(lines[i]).strip()
|
||||||
raise ValueError(f"Expected block start at line {i + 1}: {lines[i]!r}")
|
m = BLOCK_RE.match(line)
|
||||||
|
if not m:
|
||||||
|
raise ValueError(f"Expected block start at line {i + 1}: {lines[i]!r}")
|
||||||
|
|
||||||
name = m.group(1)
|
name = m.group(1)
|
||||||
i += 1
|
i += 1
|
||||||
children: list[tuple[str, list]] = []
|
children: list[tuple[str, list]] = []
|
||||||
|
|
||||||
while i < len(lines):
|
while i < len(lines):
|
||||||
s = _strip_comment(lines[i]).strip()
|
s = _strip_comment(lines[i]).strip()
|
||||||
if not s:
|
if not s:
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if s.startswith("}"):
|
if s.startswith("}"):
|
||||||
return name, children, i + 1
|
return name, children, i + 1
|
||||||
|
|
||||||
if BLOCK_RE.match(s):
|
if BLOCK_RE.match(s):
|
||||||
child_name, child_children, i = parse_block(lines, i)
|
child_name, child_children, i = parse_block(lines, i)
|
||||||
children.append((child_name, child_children))
|
children.append((child_name, child_children))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
raise ValueError(f"Unterminated block: {name}")
|
raise ValueError(f"Unterminated block: {name}")
|
||||||
|
|
||||||
|
|
||||||
def collect_page_names(block: tuple[str, list[tuple[str, list]]]) -> list[str]:
|
def collect_page_names(block: tuple[str, list[tuple[str, list]]]) -> list[str]:
|
||||||
name, children = block
|
name, children = block
|
||||||
|
|
||||||
if name != "Component":
|
if name != "Component":
|
||||||
return [name]
|
return [name]
|
||||||
|
|
||||||
for child_name, child_children in children:
|
for child_name, child_children in children:
|
||||||
if child_name == "StackPage":
|
if child_name == "StackPage":
|
||||||
out: list[str] = []
|
out: list[str] = []
|
||||||
for grand_name, grand_children in child_children:
|
for grand_name, grand_children in child_children:
|
||||||
if grand_name == "Component":
|
if grand_name == "Component":
|
||||||
out.extend(collect_page_names((grand_name, grand_children)))
|
out.extend(collect_page_names((grand_name, grand_children)))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
if child_name != "Component":
|
if child_name != "Component":
|
||||||
return [child_name]
|
return [child_name]
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def parse_page_comps(settings: Path) -> list[list[str]]:
|
def parse_page_comps(settings: Path) -> list[list[str]]:
|
||||||
text = (settings / "PageCompRegistry.qml").read_text().splitlines()
|
text = (settings / "PageCompRegistry.qml").read_text().splitlines()
|
||||||
|
|
||||||
start = next(
|
start = next(
|
||||||
i for i, line in enumerate(text)
|
i
|
||||||
if re.search(r'\bpageComps\s*:\s*\[', _strip_comment(line))
|
for i, line in enumerate(text)
|
||||||
)
|
if re.search(r"\bpageComps\s*:\s*\[", _strip_comment(line))
|
||||||
|
)
|
||||||
|
|
||||||
comps: list[list[str]] = []
|
comps: list[list[str]] = []
|
||||||
i = start + 1
|
i = start + 1
|
||||||
|
|
||||||
while i < len(text):
|
while i < len(text):
|
||||||
s = _strip_comment(text[i]).strip()
|
s = _strip_comment(text[i]).strip()
|
||||||
if not s:
|
if not s:
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
if s.startswith("]"):
|
if s.startswith("]"):
|
||||||
break
|
break
|
||||||
|
|
||||||
if BLOCK_RE.match(s) and BLOCK_RE.match(s).group(1) == "Component":
|
if BLOCK_RE.match(s) and BLOCK_RE.match(s).group(1) == "Component":
|
||||||
block = parse_block(text, i)
|
block = parse_block(text, i)
|
||||||
names = collect_page_names((block[0], block[1]))
|
names = collect_page_names((block[0], block[1]))
|
||||||
if names:
|
if names:
|
||||||
comps.append(names)
|
comps.append(names)
|
||||||
i = block[2]
|
i = block[2]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return comps
|
return comps
|
||||||
|
|
||||||
|
|
||||||
def dedup_crumbs(labels: list[str], icons: list[str]) -> tuple[list[str], list[str]]:
|
def dedup_crumbs(
|
||||||
out_labels: list[str] = []
|
labels: list[str], icons: list[str]
|
||||||
out_icons: list[str] = []
|
) -> tuple[list[str], list[str]]:
|
||||||
for lbl, ico in zip(labels, icons):
|
out_labels: list[str] = []
|
||||||
if out_labels and out_labels[-1] == lbl:
|
out_icons: list[str] = []
|
||||||
continue
|
for lbl, ico in zip(labels, icons, strict=False):
|
||||||
out_labels.append(lbl)
|
if out_labels and out_labels[-1] == lbl:
|
||||||
out_icons.append(ico)
|
continue
|
||||||
return out_labels, out_icons
|
out_labels.append(lbl)
|
||||||
|
out_icons.append(ico)
|
||||||
|
return out_labels, out_icons
|
||||||
|
|
||||||
|
|
||||||
def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
|
def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
|
||||||
comps = parse_page_comps(settings)
|
comps = parse_page_comps(settings)
|
||||||
registry = parse_page_registry(settings)
|
registry = parse_page_registry(settings)
|
||||||
|
|
||||||
top_meta: dict[int, tuple[str, str]] = {}
|
top_meta: dict[int, tuple[str, str]] = {}
|
||||||
for i, (icon, label) in enumerate(registry):
|
for i, (icon, label) in enumerate(registry):
|
||||||
top_meta[i] = (icon, label)
|
top_meta[i] = (icon, label)
|
||||||
|
|
||||||
nav_children: dict[str, dict[int, tuple[str, str, str]]] = {}
|
nav_children: dict[str, dict[int, tuple[str, str, str]]] = {}
|
||||||
for names in comps:
|
for names in comps:
|
||||||
for name in names:
|
for name in names:
|
||||||
pf = files.get(name)
|
pf = files.get(name)
|
||||||
if not pf:
|
if not pf:
|
||||||
continue
|
continue
|
||||||
pending_icon = pending_label = None
|
pending_icon = pending_label = None
|
||||||
section = ""
|
section = ""
|
||||||
expect_section = False
|
expect_section = False
|
||||||
for ln in read_lines(pf):
|
for ln in read_lines(pf):
|
||||||
if SECTION_RE.match(ln):
|
if SECTION_RE.match(ln):
|
||||||
expect_section = True
|
expect_section = True
|
||||||
continue
|
continue
|
||||||
ml = LABEL_RE.match(ln)
|
ml = LABEL_RE.match(ln)
|
||||||
if ml:
|
if ml:
|
||||||
if expect_section:
|
if expect_section:
|
||||||
section = ml.group(1)
|
section = ml.group(1)
|
||||||
expect_section = False
|
expect_section = False
|
||||||
else:
|
else:
|
||||||
pending_label = ml.group(1)
|
pending_label = ml.group(1)
|
||||||
continue
|
continue
|
||||||
mi = ICON_RE.match(ln)
|
mi = ICON_RE.match(ln)
|
||||||
if mi:
|
if mi:
|
||||||
pending_icon = mi.group(1)
|
pending_icon = mi.group(1)
|
||||||
mo = re.search(r"openSubPage\((\d+)\)", ln)
|
mo = re.search(r"openSubPage\((\d+)\)", ln)
|
||||||
if mo:
|
if mo:
|
||||||
pos = int(mo.group(1))
|
pos = int(mo.group(1))
|
||||||
nav_children.setdefault(name, {})[pos] = (
|
nav_children.setdefault(name, {})[pos] = (
|
||||||
pending_icon or "tune", pending_label or "", section)
|
pending_icon or "tune",
|
||||||
pending_icon = pending_label = None
|
pending_label or "",
|
||||||
|
section,
|
||||||
|
)
|
||||||
|
pending_icon = pending_label = None
|
||||||
|
|
||||||
nav: dict[str, dict] = {}
|
nav: dict[str, dict] = {}
|
||||||
for top_idx, names in enumerate(comps):
|
for top_idx, names in enumerate(comps):
|
||||||
if not names:
|
if not names:
|
||||||
continue
|
continue
|
||||||
main = names[0]
|
main = names[0]
|
||||||
main_icon, main_label = top_meta.get(top_idx, ("tune", main))
|
main_icon, main_label = top_meta.get(top_idx, ("tune", main))
|
||||||
nav[main] = {"pageIdx": top_idx, "subPath": [],
|
nav[main] = {
|
||||||
"crumbIcons": [main_icon], "crumbLabels": [main_label]}
|
"pageIdx": top_idx,
|
||||||
children = dict(nav_children.get(main, {}))
|
"subPath": [],
|
||||||
opened_via_subpage = set()
|
"crumbIcons": [main_icon],
|
||||||
for owner, kids in nav_children.items():
|
"crumbLabels": [main_label],
|
||||||
owner_group = next((ns for ns in comps if owner in ns), None)
|
}
|
||||||
if not owner_group:
|
children = dict(nav_children.get(main, {}))
|
||||||
continue
|
opened_via_subpage = set()
|
||||||
for kpos in kids:
|
for owner, kids in nav_children.items():
|
||||||
if kpos < len(owner_group):
|
owner_group = next((ns for ns in comps if owner in ns), None)
|
||||||
opened_via_subpage.add(owner_group[kpos])
|
if not owner_group:
|
||||||
for pos in range(1, len(names)):
|
continue
|
||||||
if pos not in children and names[pos] not in opened_via_subpage:
|
for kpos in kids:
|
||||||
label = re.sub(r"(Detail)?Page$", "", names[pos])
|
if kpos < len(owner_group):
|
||||||
label = re.sub(r"(?<!^)(?=[A-Z])", " ", label)
|
opened_via_subpage.add(owner_group[kpos])
|
||||||
children[pos] = (main_icon, label, "")
|
for pos in range(1, len(names)):
|
||||||
for pos, (icon, label, section) in children.items():
|
if pos not in children and names[pos] not in opened_via_subpage:
|
||||||
if pos >= len(names):
|
label = re.sub(r"(Detail)?Page$", "", names[pos])
|
||||||
continue
|
label = re.sub(r"(?<!^)(?=[A-Z])", " ", label)
|
||||||
child = names[pos]
|
children[pos] = (main_icon, label, "")
|
||||||
labels = [main_label] + ([section] if section else []) + [label]
|
for pos, (icon, label, section) in children.items():
|
||||||
icons = [main_icon] + ([icon] if section else []) + [icon]
|
if pos >= len(names):
|
||||||
labels, icons = dedup_crumbs(labels, icons)
|
continue
|
||||||
nav[child] = {"pageIdx": top_idx, "subPath": [pos],
|
child = names[pos]
|
||||||
"crumbIcons": icons,
|
labels = [main_label] + ([section] if section else []) + [label]
|
||||||
"crumbLabels": labels}
|
icons = [main_icon] + ([icon] if section else []) + [icon]
|
||||||
for gpos, (gicon, glabel, gsection) in nav_children.get(child, {}).items():
|
labels, icons = dedup_crumbs(labels, icons)
|
||||||
if gpos >= len(names):
|
nav[child] = {
|
||||||
continue
|
"pageIdx": top_idx,
|
||||||
glabels = labels + ([gsection] if gsection else []) + [glabel]
|
"subPath": [pos],
|
||||||
gicons = icons + ([gicon] if gsection else []) + [gicon]
|
"crumbIcons": icons,
|
||||||
glabels, gicons = dedup_crumbs(glabels, gicons)
|
"crumbLabels": labels,
|
||||||
nav[names[gpos]] = {
|
}
|
||||||
"pageIdx": top_idx, "subPath": [pos, gpos],
|
for gpos, (gicon, glabel, gsection) in nav_children.get(
|
||||||
"crumbIcons": gicons,
|
child, {}
|
||||||
"crumbLabels": glabels}
|
).items():
|
||||||
return nav
|
if gpos >= len(names):
|
||||||
|
continue
|
||||||
|
glabels = labels + ([gsection] if gsection else []) + [glabel]
|
||||||
|
gicons = icons + ([gicon] if gsection else []) + [gicon]
|
||||||
|
glabels, gicons = dedup_crumbs(glabels, gicons)
|
||||||
|
nav[names[gpos]] = {
|
||||||
|
"pageIdx": top_idx,
|
||||||
|
"subPath": [pos, gpos],
|
||||||
|
"crumbIcons": gicons,
|
||||||
|
"crumbLabels": glabels,
|
||||||
|
}
|
||||||
|
return nav
|
||||||
|
|
||||||
|
|
||||||
def tokenize(text: str) -> list[str]:
|
def tokenize(text: str) -> list[str]:
|
||||||
toks: list[str] = []
|
toks: list[str] = []
|
||||||
for word in text.lower().split():
|
for word in text.lower().split():
|
||||||
parts = [p for p in re.split(r"[^a-z0-9]+", word) if p]
|
parts = [p for p in re.split(r"[^a-z0-9]+", word) if p]
|
||||||
for p in parts:
|
for p in parts:
|
||||||
if p not in STOPWORDS and p not in toks:
|
if p not in STOPWORDS and p not in toks:
|
||||||
toks.append(p)
|
toks.append(p)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
joined = "".join(parts)
|
joined = "".join(parts)
|
||||||
if joined not in toks:
|
if joined not in toks:
|
||||||
toks.append(joined)
|
toks.append(joined)
|
||||||
return toks
|
return toks
|
||||||
|
|
||||||
|
|
||||||
SUBTEXT_RE = re.compile(r'^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)')
|
SUBTEXT_RE = re.compile(r'^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)')
|
||||||
SECTION_RE = re.compile(r'^\s*SectionHeader\s*\{')
|
SECTION_RE = re.compile(r"^\s*SectionHeader\s*\{")
|
||||||
|
|
||||||
|
|
||||||
def extract_settings(files: dict[str, Path], nav: dict[str, dict]) -> list[dict]:
|
def extract_settings(
|
||||||
entries: list[dict] = []
|
files: dict[str, Path], nav: dict[str, dict]
|
||||||
for comp, meta in nav.items():
|
) -> list[dict]:
|
||||||
pf = files.get(comp)
|
entries: list[dict] = []
|
||||||
if not pf:
|
for comp, meta in nav.items():
|
||||||
continue
|
pf = files.get(comp)
|
||||||
lines = read_lines(pf)
|
if not pf:
|
||||||
section = ""
|
continue
|
||||||
i = 0
|
lines = read_lines(pf)
|
||||||
while i < len(lines):
|
section = ""
|
||||||
if SECTION_RE.match(lines[i]):
|
i = 0
|
||||||
for j in range(i + 1, min(i + 4, len(lines))):
|
while i < len(lines):
|
||||||
m = LABEL_RE.match(lines[j])
|
if SECTION_RE.match(lines[i]):
|
||||||
if m:
|
for j in range(i + 1, min(i + 4, len(lines))):
|
||||||
section = m.group(1)
|
m = LABEL_RE.match(lines[j])
|
||||||
break
|
if m:
|
||||||
row_match = ROW_RE.match(lines[i])
|
section = m.group(1)
|
||||||
if row_match:
|
break
|
||||||
row_type = row_match.group(1)
|
row_match = ROW_RE.match(lines[i])
|
||||||
label = anchor = subtext = None
|
if row_match:
|
||||||
checked_path = toggled_path = None
|
row_type = row_match.group(1)
|
||||||
for j in range(i + 1, min(i + 12, len(lines))):
|
label = anchor = subtext = None
|
||||||
if label is None:
|
checked_path = toggled_path = None
|
||||||
m = LABEL_RE.match(lines[j])
|
for j in range(i + 1, min(i + 12, len(lines))):
|
||||||
if m:
|
if label is None:
|
||||||
label = m.group(1)
|
m = LABEL_RE.match(lines[j])
|
||||||
if anchor is None:
|
if m:
|
||||||
a = ANCHOR_RE.match(lines[j])
|
label = m.group(1)
|
||||||
if a:
|
if anchor is None:
|
||||||
anchor = a.group(1)
|
a = ANCHOR_RE.match(lines[j])
|
||||||
if subtext is None:
|
if a:
|
||||||
st = SUBTEXT_RE.match(lines[j])
|
anchor = a.group(1)
|
||||||
if st:
|
if subtext is None:
|
||||||
subtext = st.group(1)
|
st = SUBTEXT_RE.match(lines[j])
|
||||||
if checked_path is None:
|
if st:
|
||||||
ch = CHECKED_RE.match(lines[j])
|
subtext = st.group(1)
|
||||||
if ch:
|
if checked_path is None:
|
||||||
checked_path = ch.group(1)
|
ch = CHECKED_RE.match(lines[j])
|
||||||
if toggled_path is None:
|
if ch:
|
||||||
tg = ONTOGGLED_RE.match(lines[j])
|
checked_path = ch.group(1)
|
||||||
if tg:
|
if toggled_path is None:
|
||||||
toggled_path = tg.group(1)
|
tg = ONTOGGLED_RE.match(lines[j])
|
||||||
toggle_path = (
|
if tg:
|
||||||
checked_path
|
toggled_path = tg.group(1)
|
||||||
if row_type == "ToggleRow" and checked_path and checked_path == toggled_path
|
toggle_path = (
|
||||||
else ""
|
checked_path
|
||||||
)
|
if row_type == "ToggleRow"
|
||||||
if label and label not in SKIP_LABELS and anchor:
|
and checked_path
|
||||||
extra = " ".join(meta["crumbLabels"]) + \
|
and checked_path == toggled_path
|
||||||
" " + section + " " + (subtext or "")
|
else ""
|
||||||
entries.append({
|
)
|
||||||
"pageIdx": meta["pageIdx"], "subPath": meta["subPath"],
|
if label and label not in SKIP_LABELS and anchor:
|
||||||
"crumbIcons": meta["crumbIcons"],
|
extra = (
|
||||||
"crumbLabels": meta["crumbLabels"],
|
" ".join(meta["crumbLabels"])
|
||||||
"title": label, "anchor": anchor,
|
+ " "
|
||||||
"section": section,
|
+ section
|
||||||
"subtext": subtext or "",
|
+ " "
|
||||||
"togglePath": toggle_path,
|
+ (subtext or "")
|
||||||
"keywords": " ".join(sorted(set(tokenize(label + " " + extra)))),
|
)
|
||||||
})
|
entries.append(
|
||||||
i += 1
|
{
|
||||||
return entries
|
"pageIdx": meta["pageIdx"],
|
||||||
|
"subPath": meta["subPath"],
|
||||||
|
"crumbIcons": meta["crumbIcons"],
|
||||||
|
"crumbLabels": meta["crumbLabels"],
|
||||||
|
"title": label,
|
||||||
|
"anchor": anchor,
|
||||||
|
"section": section,
|
||||||
|
"subtext": subtext or "",
|
||||||
|
"togglePath": toggle_path,
|
||||||
|
"keywords": " ".join(
|
||||||
|
sorted(set(tokenize(label + " " + extra)))
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def build_inverted_and_ranking(entries: list[dict]):
|
def build_inverted_and_ranking(entries: list[dict]):
|
||||||
inverted: dict[str, list[int]] = defaultdict(list)
|
inverted: dict[str, list[int]] = defaultdict(list)
|
||||||
ranking: dict[str, dict[int, float]] = defaultdict(dict)
|
ranking: dict[str, dict[int, float]] = defaultdict(dict)
|
||||||
for idx, e in enumerate(entries):
|
for idx, e in enumerate(entries):
|
||||||
fields = {"title": e["title"], "keywords": e["keywords"]}
|
fields = {"title": e["title"], "keywords": e["keywords"]}
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
for field, text in fields.items():
|
for field, text in fields.items():
|
||||||
weight = FIELD_WEIGHT.get(field, 0.2)
|
weight = FIELD_WEIGHT.get(field, 0.2)
|
||||||
for tok in tokenize(text):
|
for tok in tokenize(text):
|
||||||
if idx not in inverted[tok]:
|
if idx not in inverted[tok]:
|
||||||
inverted[tok].append(idx)
|
inverted[tok].append(idx)
|
||||||
ranking[tok][idx] = max(ranking[tok].get(idx, 0.0), weight)
|
ranking[tok][idx] = max(ranking[tok].get(idx, 0.0), weight)
|
||||||
seen.add(tok)
|
seen.add(tok)
|
||||||
for tok, ids in inverted.items():
|
for tok, ids in inverted.items():
|
||||||
ids.sort(key=lambda i: ranking[tok][i], reverse=True)
|
ids.sort(key=lambda i: ranking[tok][i], reverse=True)
|
||||||
return inverted, {t: {str(k): v for k, v in d.items()} for t, d in ranking.items()}
|
return inverted, {
|
||||||
|
t: {str(k): v for k, v in d.items()} for t, d in ranking.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print(__doc__)
|
print(__doc__)
|
||||||
return 1
|
return 1
|
||||||
settings = Path(sys.argv[1])
|
settings = Path(sys.argv[1])
|
||||||
out = Path(sys.argv[2])
|
out = Path(sys.argv[2])
|
||||||
files = discover_files(settings)
|
files = discover_files(settings)
|
||||||
nav = build_nav_map(settings, files)
|
nav = build_nav_map(settings, files)
|
||||||
entries = extract_settings(files, nav)
|
entries = extract_settings(files, nav)
|
||||||
inverted, ranking = build_inverted_and_ranking(entries)
|
inverted, ranking = build_inverted_and_ranking(entries)
|
||||||
for e in entries:
|
for e in entries:
|
||||||
e.pop("keywords", None)
|
e.pop("keywords", None)
|
||||||
out.write_text(json.dumps({
|
out.write_text(
|
||||||
"version": 2,
|
json.dumps(
|
||||||
"entries": entries,
|
{
|
||||||
"inverted": inverted,
|
"version": 2,
|
||||||
"ranking": ranking,
|
"entries": entries,
|
||||||
}, ensure_ascii=False, indent=2))
|
"inverted": inverted,
|
||||||
print(f"settings index: {len(entries)} entries, "
|
"ranking": ranking,
|
||||||
f"{len(inverted)} tokens -> {out}")
|
},
|
||||||
print("files:", len(files))
|
ensure_ascii=False,
|
||||||
print("comps:", len(parse_page_comps(settings)))
|
indent=2,
|
||||||
print("registry:", len(parse_page_registry(settings)))
|
)
|
||||||
print("nav:", len(nav))
|
)
|
||||||
print("entries:", len(entries))
|
print(
|
||||||
return 0
|
f"settings index: {len(entries)} entries, "
|
||||||
|
f"{len(inverted)} tokens -> {out}"
|
||||||
|
)
|
||||||
|
print("files:", len(files))
|
||||||
|
print("comps:", len(parse_page_comps(settings)))
|
||||||
|
print("registry:", len(parse_page_registry(settings)))
|
||||||
|
print("nav:", len(nav))
|
||||||
|
print("entries:", len(entries))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user