18 Commits
Author SHA1 Message Date
zach 0fc1a123fa mute cmake install output 2026-07-08 23:08:42 +02:00
zach 672cb07547 feat: import M3Shapes, M3 loading indicator and weather + resources + media revamp in lockscreen 2026-07-08 22:53:19 +02:00
zach e3b2d33a11 fix: fallback to python wheel if Nuitka is not available 2026-07-08 18:32:21 +02:00
zach 2a40696292 Merge pull request 'chore(ci-image): fix tld in ci-image workflow + additional workflow improvements' (#137) from update-cibuild-tld into main
Reviewed-on: #137
Reviewed-by: zach <zach@brohn.se>
2026-07-08 18:25:10 +02:00
zach 80ee714e91 Merge branch 'main' into update-cibuild-tld
C++ / fmt (pull_request) Successful in 4s
Python / fmt (pull_request) Successful in 58s
JS/TS / lint (pull_request) Successful in 1m14s
JS/TS / fmt (pull_request) Failing after 1m32s
Python / lint (pull_request) Failing after 1m55s
C++ / clang-tidy (pull_request) Successful in 2m55s
C++ / build (pull_request) Successful in 2m58s
Rust / fmt (pull_request) Successful in 1m21s
Python / test (pull_request) Successful in 2m25s
Rust / clippy (pull_request) Successful in 1m31s
Rust / build (pull_request) Successful in 3m5s
Python / buildcheck (pull_request) Successful in 3m25s
2026-07-08 17:50:24 +02:00
zach 2c572c3250 feat: animations in settings search results 2026-07-08 17:49:26 +02:00
AramJonghu 0912a8e40c hotfix(): Forgot to change ruff package as dep
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Successful in 34s
Python / lint (pull_request) Successful in 33s
Python / test (pull_request) Successful in 59s
C++ / build (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 1m12s
Rust / build (pull_request) Successful in 1m58s
Python / buildcheck (pull_request) Successful in 2m44s
Rust / clippy (pull_request) Successful in 1m57s
C++ / clang-tidy (pull_request) Successful in 3m37s
2026-07-08 17:30:46 +02:00
AramJonghu 0d17621180 chore(py formatter): Revert from autopep8 -> ruff after config update
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 20s
Python / fmt (pull_request) Failing after 26s
Python / lint (pull_request) Successful in 29s
Python / test (pull_request) Successful in 1m3s
Rust / fmt (pull_request) Successful in 1m11s
C++ / build (pull_request) Successful in 1m53s
Rust / build (pull_request) Successful in 1m56s
Rust / clippy (pull_request) Successful in 1m45s
Python / buildcheck (pull_request) Successful in 2m44s
C++ / clang-tidy (pull_request) Successful in 3m49s
2026-07-08 17:28:25 +02:00
zach 0ae6d6fc55 chore: format test files
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / lint (pull_request) Successful in 32s
Python / fmt (pull_request) Failing after 37s
Python / test (pull_request) Successful in 54s
C++ / build (pull_request) Successful in 1m57s
Rust / fmt (pull_request) Successful in 1m16s
Rust / build (pull_request) Successful in 1m56s
Python / buildcheck (pull_request) Successful in 2m44s
Rust / clippy (pull_request) Successful in 1m56s
C++ / clang-tidy (pull_request) Successful in 3m46s
2026-07-08 17:01:25 +02:00
zach 9300da258e chore: python code formatted with ruff, ruff formatter + linter, pyproject.toml now in root
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 18s
Python / lint (pull_request) Failing after 32s
Python / fmt (pull_request) Failing after 37s
Python / test (pull_request) Successful in 52s
C++ / build (pull_request) Successful in 1m56s
Rust / fmt (pull_request) Successful in 1m22s
Rust / build (pull_request) Successful in 2m5s
Python / buildcheck (pull_request) Successful in 2m39s
Rust / clippy (pull_request) Successful in 2m1s
C++ / clang-tidy (pull_request) Successful in 3m46s
2026-07-08 16:54:41 +02:00
zach 298cbbf424 Merge branch 'main' into update-cibuild-tld
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Failing after 30s
Python / lint (pull_request) Successful in 33s
Python / test (pull_request) Successful in 54s
C++ / build (pull_request) Successful in 1m47s
Rust / fmt (pull_request) Successful in 1m16s
Rust / build (pull_request) Successful in 2m0s
Python / buildcheck (pull_request) Successful in 2m41s
Rust / clippy (pull_request) Successful in 1m54s
C++ / clang-tidy (pull_request) Successful in 3m41s
2026-07-08 16:18:15 +02:00
zach b19b630a77 better ddcutil handling, revert to using onMoved brightness slider, fix osd region 2026-07-08 16:17:37 +02:00
AramJonghu 75f0f2071d fix(ci): include zshell python package for buildcheck
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 18s
Python / fmt (pull_request) Failing after 32s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 52s
Rust / fmt (pull_request) Successful in 1m11s
C++ / build (pull_request) Successful in 1m57s
Rust / build (pull_request) Successful in 1m49s
Python / buildcheck (pull_request) Successful in 2m39s
Rust / clippy (pull_request) Successful in 1m45s
C++ / clang-tidy (pull_request) Successful in 3m43s
2026-07-08 14:52:42 +02:00
AramJonghu bfea718547 fix(ci): clang-format pass echo, nuitka full package compile
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Failing after 34s
Python / lint (pull_request) Successful in 33s
Python / test (pull_request) Successful in 51s
C++ / build (pull_request) Successful in 1m37s
Rust / fmt (pull_request) Successful in 1m8s
Rust / build (pull_request) Successful in 1m52s
Python / buildcheck (pull_request) Successful in 2m37s
Rust / clippy (pull_request) Successful in 1m55s
C++ / clang-tidy (pull_request) Successful in 3m37s
2026-07-08 14:49:02 +02:00
AramJonghu 5505ac19ba refactor(ci): add python workflow, split cpp jobs, add clang-format
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 18s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Failing after 29s
Python / lint (pull_request) Successful in 35s
Python / test (pull_request) Successful in 56s
C++ / build (pull_request) Successful in 1m49s
Rust / fmt (pull_request) Successful in 1m9s
Rust / build (pull_request) Successful in 1m51s
Python / buildcheck (pull_request) Successful in 2m31s
Rust / clippy (pull_request) Successful in 1m47s
C++ / clang-tidy (pull_request) Successful in 3m29s
2026-07-08 14:37:35 +02:00
AramJonghu 9d2dc281ec refactor(ci): restructure workflows to multi-job per language
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 14s
Rust / fmt (pull_request) Successful in 1m30s
Rust / build (pull_request) Successful in 1m53s
Rust / clippy (pull_request) Successful in 2m20s
C++ / build (pull_request) Successful in 2m54s
2026-07-08 14:28:51 +02:00
AramJonghu 05c8851143 Merge branch 'main' into update-cibuild-tld
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 22s
Python / lint-format (pull_request) Successful in 27s
Python / test (pull_request) Successful in 50s
C++ / build (pull_request) Successful in 2m31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m34s
2026-07-08 13:51:16 +02:00
AramJonghu 71b25bff49 chore(ci-image): fix tld in ci-image workflow
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 45s
Python / lint-format (pull_request) Successful in 52s
Python / test (pull_request) Successful in 44s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m50s
C++ / build (pull_request) Successful in 3m40s
2026-07-08 13:21:19 +02:00
43 changed files with 3052 additions and 2288 deletions
+3 -3
View File
@@ -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 .
+34 -1
View File
@@ -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
-85
View File
@@ -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/
+152
View File
@@ -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
+60 -3
View File
@@ -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,8 +74,9 @@ 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)
if(NUITKA_EXECUTABLE)
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
"${ZSHELL_CLI_SRC}/*.py" "${ZSHELL_CLI_SRC}/*.py"
) )
@@ -109,11 +112,46 @@ if("shell" IN_LIST ENABLE_MODULES)
@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()
+1 -1
View File
@@ -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 {
+114
View File
@@ -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();
}
}
}
+1 -1
View File
@@ -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 {
+48 -18
View File
@@ -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
} }
+46
View File
@@ -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;
}
}
}
}
-11
View File
@@ -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
} }
} }
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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()
} }
} }
+31 -3
View File
@@ -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
View File
@@ -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 geo = JSON.parse(text).features?.[0]?.properties.geocoding; const fallbackToBigDataCloud = () => {
if (geo) { const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
const geoCity = geo.type === "city" ? geo.name : geo.city; Requests.get(fallbackUrl, text => {
city = geoCity; const geo = JSON.parse(text);
cachedCities.set(coords, geoCity); const geoCity = geo.city || geo.locality;
if (geoCity) {
city = fixCityName(geoCity);
cachedCities.set(coords, city);
} else { } else {
city = "Unknown City"; 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;
if (geo) {
const geoCity = geo.type === "city" ? geo.name : geo.city;
if (geoCity) {
city = fixCityName(geoCity);
cachedCities.set(coords, 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
} }
} }
+14 -24
View File
@@ -4,57 +4,46 @@ 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
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
radius: Appearance.rounding.large
RowLayout {
id: layout
anchors.fill: parent
spacing: Appearance.spacing.large * 2 spacing: Appearance.spacing.large * 2
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
CustomRect {
Layout.fillWidth: true
color: DynamicColors.tPalette.m3surfaceContainer
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
}
}
CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: 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
@@ -78,3 +67,4 @@ RowLayout {
} }
} }
} }
}
+54 -145
View File
@@ -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 anchors.fill: parent
layer.enabled: true color: DynamicColors.palette.m3surface
visible: false opacity: 0.7
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
active: Players.active?.isPlaying ?? false shapeMorph: true
animate: true
icon: active ? "pause" : "play_arrow" onClicked: Players.active?.togglePlaying()
level: active ? 2 : 1
set_color: "Primary"
}
PlayerControl {
function onClicked(): void {
if (Players.active?.canGoNext)
Players.active.next();
} }
IconButton {
enabled: Players.active?.canGoNext
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 {
}
} }
} }
} }
+39 -38
View File
@@ -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
} }
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.large
Resource { Resource {
Layout.bottomMargin: Appearance.padding.large id: cpu
Layout.topMargin: Appearance.padding.large
fgColor: DynamicColors.palette.m3primary fgColor: DynamicColors.palette.m3primary
icon: "memory" icon: "memory"
value: Cpu.percentage value: Cpu.percentage
} }
Resource { Resource {
Layout.bottomMargin: Appearance.padding.large fgColor: DynamicColors.palette.m3tertiary
Layout.topMargin: Appearance.padding.large
fgColor: DynamicColors.palette.m3secondary
icon: "memory_alt" icon: "memory_alt"
value: Memory.percentage value: Memory.percentage
} }
component Resource: CustomRect { Resource {
fgColor: DynamicColors.palette.m3secondary
icon: "hard_disk"
value: Storage.percentage
}
}
component Resource: CircularProgress {
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
} }
} }
+62
View File
@@ -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
}
}
+103
View File
@@ -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
View File
@@ -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 {
}
}
} }
+1 -3
View File
@@ -102,8 +102,7 @@ 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);
@@ -116,7 +115,6 @@ Item {
} }
} }
} }
}
component WrappedLoader: Loader { component WrappedLoader: Loader {
required property bool shouldBeActive required property bool shouldBeActive
+3 -3
View File
@@ -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 {
+25
View File
@@ -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
+58 -12
View File
@@ -148,16 +148,39 @@ 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 {
Anim {
from: 0
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
move: Transition {
Anim {
properties: "x,y"
}
Anim {
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
Repeater {
model: ScriptModel {
objectProp: "pageIdx"
values: root.groups
}
ColumnLayout {
id: group id: group
required property int index required property int index
@@ -173,25 +196,51 @@ VerticalFadeFlickable {
MaterialIcon { MaterialIcon {
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3primary
fill: 1
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
text: group.modelData.icon text: group.modelData.icon
} }
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3secondary
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
text: group.modelData.page text: group.modelData.page
} }
} }
ColumnLayout { Column {
id: cardList
Layout.fillWidth: true Layout.fillWidth: true
spacing: Appearance.spacing.extraSmall / 2 spacing: Appearance.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
move: Transition {
Anim {
properties: "x,y"
}
Anim {
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
Repeater { Repeater {
model: group.modelData.entries model: ScriptModel {
objectProp: "anchor"
values: group.modelData.entries
}
CustomRect { CustomRect {
id: result id: result
@@ -201,7 +250,6 @@ VerticalFadeFlickable {
readonly property bool isLast: index === group.modelData.entries.length - 1 readonly property bool isLast: index === group.modelData.entries.length - 1
required property var modelData required property var modelData
Layout.fillWidth: true
bottomLeftRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall bottomLeftRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
bottomRightRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall bottomRightRadius: layer.pressed ? Appearance.rounding.medium : isLast ? Appearance.rounding.large : Appearance.rounding.extraSmall
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
@@ -211,6 +259,7 @@ VerticalFadeFlickable {
} }
topLeftRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall 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 topRightRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
width: cardList.width
RadiusBehavior on bottomLeftRadius { RadiusBehavior on bottomLeftRadius {
} }
@@ -292,9 +341,6 @@ VerticalFadeFlickable {
} }
} }
} }
model: ScriptModel {
objectProp: "pageIdx"
values: root.groups
} }
} }
+10 -1
View File
@@ -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
}
} }
} }
+4 -4
View File
@@ -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
+19
View File
@@ -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()
+26 -8
View File
@@ -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)
@@ -23,9 +25,17 @@ def _completion_installed() -> bool:
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 (
Path.home() / ".bash_completions" / "zshell-cli.sh"
).exists()
case "fish": case "fish":
return (Path.home() / ".config" / "fish" / "completions" / "zshell-cli.fish").exists() return (
Path.home()
/ ".config"
/ "fish"
/ "completions"
/ "zshell-cli.fish"
).exists()
return False return False
@@ -40,10 +50,15 @@ def _install_completion() -> None:
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(
"zshell-cli: Restart your shell or source the file to enable tab-completion."
)
except Exception as e: except Exception as e:
print(f"zshell-cli: Failed to install shell completion: {e}", file=sys.stderr) print(
raise typer.Exit(code=1) 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:
@@ -53,5 +68,8 @@ def main() -> None:
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(
"zshell-cli: Tip: run with --install-autocomplete for tab completion.",
file=sys.stderr,
)
app() app()
+87 -27
View File
@@ -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,7 +18,9 @@ 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]:
@@ -32,34 +34,54 @@ def _read_extra_args() -> list[str]:
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(
summary: str,
body: str = "",
actions: list | None = None,
timeout: int = 5000,
) -> int | None:
args = ["notify-send", summary, body, "-t", str(timeout), "-p"] args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
if actions: if actions:
for action in actions: for action in actions:
args.extend(["-A", action]) args.extend(["-A", action])
try: try:
proc = subprocess.run(args, capture_output=True, text=True) proc = subprocess.run(args, capture_output=True, text=True)
return int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None return (
int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
)
except Exception: except Exception:
return None 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(
["hyprctl", "monitors", "-j"], capture_output=True, text=True
)
return json.loads(res.stdout) return json.loads(res.stdout)
except Exception: except Exception:
return [] 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"]
@@ -71,7 +93,12 @@ def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
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 (
region[2] <= mx
or region[0] >= mx + mw
or region[3] <= my
or region[1] >= my + mh
):
intersecting.append(m) intersecting.append(m)
return intersecting return intersecting
@@ -80,23 +107,30 @@ 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(
["slurp", "-f", "%wx%h+%x+%y"], text=True
).strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return None 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 (
int(match.group(3)),
int(match.group(4)),
int(match.group(1)),
int(match.group(2)),
)
return None 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()
@@ -118,7 +152,9 @@ def start_recording(region: Optional[str], sound: bool):
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()
@@ -137,7 +173,12 @@ def start_recording(region: Optional[str], sound: bool):
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:
@@ -145,12 +186,20 @@ def start_recording(region: Optional[str], sound: bool):
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(
"Recording failed",
"Check gpu-screen-recorder output.",
timeout=5000,
)
raise typer.Exit(code=1) 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():
@@ -166,10 +215,8 @@ def stop_recording(clipboard: bool):
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:
pass
NOTIF_ID_FILE.unlink() NOTIF_ID_FILE.unlink()
if clipboard: if clipboard:
@@ -183,21 +230,34 @@ def stop_recording(clipboard: bool):
def toggle_pause(): def toggle_pause():
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(
["pkill", "-USR2", "-f", RECORDER],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
typer.echo("Toggled pause.") 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:
+77 -47
View File
@@ -1,26 +1,33 @@
import typer import contextlib
import json import json
import shutil
import os import os
import sys
import re import re
import shutil
import subprocess import subprocess
import sys
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined
from typing import Any, Optional, Tuple
from zshell.utils.schemepalettes import get_palette, list_schemes, resolve_preset
from pathlib import Path from pathlib import Path
from PIL import Image from typing import Any
import typer
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined
from materialyoucolor.dynamiccolor.material_dynamic_colors import (
MaterialDynamicColors,
)
from materialyoucolor.hct.hct import Hct
from materialyoucolor.quantize import QuantizeCelebi from materialyoucolor.quantize import QuantizeCelebi
from materialyoucolor.score.score import Score from materialyoucolor.score.score import Score
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
from materialyoucolor.hct.hct import Hct
from materialyoucolor.utils.color_utils import argb_from_rgb from materialyoucolor.utils.color_utils import argb_from_rgb
from materialyoucolor.utils.math_utils import ( from materialyoucolor.utils.math_utils import (
difference_degrees, difference_degrees,
rotation_direction, rotation_direction,
sanitize_degrees_double, sanitize_degrees_double,
) )
from PIL import Image
from zshell.utils.schemepalettes import (
get_palette,
list_schemes,
resolve_preset,
)
app = typer.Typer() app = typer.Typer()
@@ -74,7 +81,8 @@ def _complete_accent(ctx, incomplete):
@app.command() @app.command()
def list_presets( def list_presets(
json_format: bool = typer.Option( json_format: bool = typer.Option(
False, "--json", help="Output in JSON format"), False, "--json", help="Output in JSON format"
),
): ):
schemes = list_schemes() schemes = list_schemes()
if json_format: if json_format:
@@ -107,25 +115,25 @@ def list_presets(
@app.command() @app.command()
def generate( def generate(
image_path: Optional[Path] = typer.Option( image_path: Path | None = typer.Option(
None, help="Path to source image. Required for image mode." None, help="Path to source image. Required for image mode."
), ),
scheme: Optional[str] = typer.Option( scheme: str | None = typer.Option(
None, None,
help="Color scheme algorithm to use for image mode. Ignored in preset mode.", help="Color scheme algorithm to use for image mode. Ignored in preset mode.",
autocompletion=_complete_scheme_name, autocompletion=_complete_scheme_name,
), ),
preset: Optional[str] = typer.Option( preset: str | None = typer.Option(
None, None,
help="Name of a premade scheme in this format: <scheme>:<variant>", help="Name of a premade scheme in this format: <scheme>:<variant>",
autocompletion=_complete_preset, autocompletion=_complete_preset,
), ),
mode: Optional[str] = typer.Option( mode: str | None = typer.Option(
None, None,
help="Mode of the preset scheme (dark or light).", help="Mode of the preset scheme (dark or light).",
autocompletion=_complete_mode, autocompletion=_complete_mode,
), ),
accent: Optional[str] = typer.Option( accent: str | None = typer.Option(
None, None,
help="Accent for schemes that support it (e.g. mauve).", help="Accent for schemes that support it (e.g. mauve).",
autocompletion=_complete_accent, autocompletion=_complete_accent,
@@ -149,20 +157,27 @@ def generate(
if preset is not None and image_path is not None: if preset is not None and image_path is not None:
raise typer.BadParameter( raise typer.BadParameter(
"Use either --image-path or --preset, not both.") "Use either --image-path or --preset, not both."
)
def get_scheme_class(scheme_name: str): def get_scheme_class(scheme_name: str):
match scheme_name: match scheme_name:
case "fruit-salad": case "fruit-salad":
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad from materialyoucolor.scheme.scheme_fruit_salad import (
SchemeFruitSalad,
)
return SchemeFruitSalad return SchemeFruitSalad
case "expressive": case "expressive":
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive from materialyoucolor.scheme.scheme_expressive import (
SchemeExpressive,
)
return SchemeExpressive return SchemeExpressive
case "monochrome": case "monochrome":
from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome from materialyoucolor.scheme.scheme_monochrome import (
SchemeMonochrome,
)
return SchemeMonochrome return SchemeMonochrome
case "rainbow": case "rainbow":
@@ -170,7 +185,9 @@ def generate(
return SchemeRainbow return SchemeRainbow
case "tonal-spot": case "tonal-spot":
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot from materialyoucolor.scheme.scheme_tonal_spot import (
SchemeTonalSpot,
)
return SchemeTonalSpot return SchemeTonalSpot
case "neutral": case "neutral":
@@ -178,7 +195,9 @@ def generate(
return SchemeNeutral return SchemeNeutral
case "fidelity": case "fidelity":
from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity from materialyoucolor.scheme.scheme_fidelity import (
SchemeFidelity,
)
return SchemeFidelity return SchemeFidelity
case "content": case "content":
@@ -190,7 +209,9 @@ def generate(
return SchemeVibrant return SchemeVibrant
case _: case _:
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad from materialyoucolor.scheme.scheme_fruit_salad import (
SchemeFruitSalad,
)
return SchemeFruitSalad return SchemeFruitSalad
@@ -275,8 +296,8 @@ def generate(
diff = difference_degrees(from_hct.hue, to_hct.hue) diff = difference_degrees(from_hct.hue, to_hct.hue)
rotation = min(diff * 0.8, 100) rotation = min(diff * 0.8, 100)
output_hue = sanitize_degrees_double( output_hue = sanitize_degrees_double(
from_hct.hue + rotation * from_hct.hue
rotation_direction(from_hct.hue, to_hct.hue) + rotation * rotation_direction(from_hct.hue, to_hct.hue)
) )
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost))) tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
return Hct.from_hct(output_hue, from_hct.chroma, tone) return Hct.from_hct(output_hue, from_hct.chroma, tone)
@@ -316,7 +337,9 @@ def generate(
safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key) safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
return thumb_dir / f"{safe_key}_thumbnail.jpg" return thumb_dir / f"{safe_key}_thumbnail.jpg"
def generate_thumbnail(image_path: Path, thumb_dir: Path, size=(128, 128)) -> Path: def generate_thumbnail(
image_path: Path, thumb_dir: Path, size=(128, 128)
) -> Path:
thumb_dir.mkdir(parents=True, exist_ok=True) thumb_dir.mkdir(parents=True, exist_ok=True)
cache_path = thumbnail_cache_path(image_path, thumb_dir) cache_path = thumbnail_cache_path(image_path, thumb_dir)
@@ -331,7 +354,9 @@ def generate(
return cache_path return cache_path
def apply_terms(sequences: str, sequences_tmux: str, state_path: Path) -> None: def apply_terms(
sequences: str, sequences_tmux: str, state_path: Path
) -> None:
state_path.parent.mkdir(parents=True, exist_ok=True) state_path.parent.mkdir(parents=True, exist_ok=True)
state_path.write_text(sequences, encoding="utf-8") state_path.write_text(sequences, encoding="utf-8")
@@ -375,7 +400,7 @@ def generate(
mode = mode.lower() mode = mode.lower()
preference = "prefer-dark" if mode == "dark" else "prefer-light" preference = "prefer-dark" if mode == "dark" else "prefer-light"
try: with contextlib.suppress(FileNotFoundError):
subprocess.run( subprocess.run(
[ [
"gsettings", "gsettings",
@@ -388,8 +413,6 @@ def generate(
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
except FileNotFoundError:
pass
def apply_qt_mode(mode: str, home: str) -> None: def apply_qt_mode(mode: str, home: str) -> None:
mode = mode.lower() mode = mode.lower()
@@ -413,10 +436,8 @@ def generate(
) )
if count > 0 and new_text != text: if count > 0 and new_text != text:
try: with contextlib.suppress(OSError):
qt_conf.write_text(new_text, encoding="utf-8") qt_conf.write_text(new_text, encoding="utf-8")
except OSError:
pass
def build_template_context( def build_template_context(
*, *,
@@ -480,7 +501,7 @@ def generate(
ESC = "\x1b" ESC = "\x1b"
return f"{ESC}Ptmux;{seq.replace(ESC, ESC + ESC)}{ESC}\\" return f"{ESC}Ptmux;{seq.replace(ESC, ESC + ESC)}{ESC}\\"
def parse_output_directive(first_line: str) -> Optional[Path]: def parse_output_directive(first_line: str) -> Path | None:
s = first_line.strip() s = first_line.strip()
if not s.startswith("#") or s.startswith("#!"): if not s.startswith("#") or s.startswith("#!"):
return None return None
@@ -492,7 +513,7 @@ def generate(
expanded = os.path.expandvars(os.path.expanduser(target)) expanded = os.path.expandvars(os.path.expanduser(target))
return Path(expanded) return Path(expanded)
def split_directive_and_body(text: str) -> Tuple[Optional[Path], str]: def split_directive_and_body(text: str) -> tuple[Path | None, str]:
lines = text.splitlines(keepends=True) lines = text.splitlines(keepends=True)
if not lines: if not lines:
return None, "" return None, ""
@@ -520,7 +541,9 @@ def generate(
rendered_outputs: list[Path] = [] rendered_outputs: list[Path] = []
for tpl_path in sorted(p for p in templates_dir.rglob("*") if p.is_file()): for tpl_path in sorted(
p for p in templates_dir.rglob("*") if p.is_file()
):
rel = tpl_path.relative_to(templates_dir) rel = tpl_path.relative_to(templates_dir)
if any(part.startswith(".") for part in rel.parts): if any(part.startswith(".") for part in rel.parts):
@@ -538,14 +561,13 @@ def generate(
text = template.render(**context) text = template.render(**context)
except Exception as e: except Exception as e:
raise RuntimeError( raise RuntimeError(
f"Template render failed for '{rel}': {e}") from e f"Template render failed for '{rel}': {e}"
) from e
out_path.write_text(text, encoding="utf-8") out_path.write_text(text, encoding="utf-8")
try: with contextlib.suppress(OSError):
shutil.copymode(tpl_path, out_path) shutil.copymode(tpl_path, out_path)
except OSError:
pass
rendered_outputs.append(out_path) rendered_outputs.append(out_path)
@@ -562,14 +584,16 @@ def generate(
result = QuantizeCelebi(pixel_array, 128) result = QuantizeCelebi(pixel_array, 128)
return Hct.from_int(Score.score(result)[0]) return Hct.from_int(Score.score(result)[0])
def generate_color_scheme(seed: Hct, mode: str, scheme_class) -> dict[str, str]: def generate_color_scheme(
seed: Hct, mode: str, scheme_class
) -> dict[str, str]:
is_dark = mode.lower() == "dark" is_dark = mode.lower() == "dark"
scheme = scheme_class(seed, is_dark, 0.0) scheme = scheme_class(seed, is_dark, 0.0)
color_dict = {} color_dict = {}
for color in vars(MaterialDynamicColors).keys(): for color in vars(MaterialDynamicColors):
color_name = getattr(MaterialDynamicColors, color) color_name = getattr(MaterialDynamicColors, color)
if hasattr(color_name, "get_hct"): if hasattr(color_name, "get_hct"):
color_int = color_name.get_hct(scheme).to_int() color_int = color_name.get_hct(scheme).to_int()
@@ -578,7 +602,7 @@ def generate(
return color_dict return color_dict
def int_to_hex(argb_int): def int_to_hex(argb_int):
return "#{:06X}".format(argb_int & 0xFFFFFF) return f"#{argb_int & 0xFFFFFF:06X}"
try: try:
with CONFIG.open() as f: with CONFIG.open() as f:
@@ -601,8 +625,9 @@ def generate(
(v.accents for v in meta.variants if v.id == p_variant), () (v.accents for v in meta.variants if v.id == p_variant), ()
) )
if accent not in var_accents: if accent not in var_accents:
available = ", ".join( available = (
var_accents) if var_accents else "none" ", ".join(var_accents) if var_accents else "none"
)
raise typer.BadParameter( raise typer.BadParameter(
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}" f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
) )
@@ -612,9 +637,14 @@ def generate(
if p_scheme in schemes: if p_scheme in schemes:
meta = schemes[p_scheme] meta = schemes[p_scheme]
variant = next( variant = next(
(vari for vari in meta.variants if vari.id == p_variant), None (vari for vari in meta.variants if vari.id == p_variant),
None,
) )
if variant and requested_mode not in variant.modes and variant.modes: if (
variant
and requested_mode not in variant.modes
and variant.modes
):
resolved_mode = sorted(variant.modes)[0] resolved_mode = sorted(variant.modes)[0]
palette_obj = get_palette( palette_obj = get_palette(
+3 -2
View File
@@ -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)
+19 -11
View File
@@ -11,7 +11,7 @@ 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)
@@ -19,10 +19,11 @@ def kill():
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(
args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True
)
stdout = result.stdout.decode().strip() stdout = result.stdout.decode().strip()
if stdout: if stdout and "already running" in stdout.lower():
if "already running" in stdout.lower():
sys.stderr.write(stdout + "\n") sys.stderr.write(stdout + "\n")
sys.exit(1) sys.exit(1)
if result.returncode != 0: if result.returncode != 0:
@@ -38,10 +39,10 @@ def start(no_daemon: bool = False):
@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)
@@ -50,7 +51,7 @@ def restart(no_daemon: bool = False):
@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)
@@ -60,7 +61,7 @@ def show():
@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)
@@ -70,7 +71,9 @@ def log():
@app.command() @app.command()
def lock(): def lock():
result = subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], capture_output=True) result = subprocess.run(
[*args, "ipc", "call", "lock", "lock"], 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)
@@ -78,8 +81,13 @@ def lock():
@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)
):
result = subprocess.run(
args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []),
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)
+8 -5
View File
@@ -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,7 +12,10 @@ 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()
+11 -8
View File
@@ -102,21 +102,22 @@ def _discover_schemes() -> dict[str, SchemeMeta]:
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(
scheme: str, variant: str, mode: str, accent: str | None = None
) -> Palette:
if scheme not in SCHEMES: if scheme not in SCHEMES:
raise KeyError( raise KeyError(
f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}") 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():
@@ -130,7 +131,9 @@ def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None)
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]:
+42 -14
View File
@@ -1,7 +1,8 @@
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
@@ -12,8 +13,12 @@ def tmp_schemes(tmp_path: Path) -> Path:
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)
@@ -21,14 +26,24 @@ def tmp_schemes(tmp_path: Path) -> Path:
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)
@@ -81,13 +96,17 @@ class TestDiscoverSchemes:
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(
v for v in schemes["gruvbox"].variants if v.id == "medium"
)
assert "dark" in gmedium.modes assert "dark" in gmedium.modes
assert "light" 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(
v for v in schemes["catppuccin"].variants if v.id == "mocha"
)
assert "mauve" in mocha.accents assert "mauve" in mocha.accents
assert "green" in mocha.accents assert "green" in mocha.accents
assert "rosewater" in mocha.accents assert "rosewater" in mocha.accents
@@ -95,7 +114,9 @@ class TestDiscoverSchemes:
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(
v for v in schemes["gruvbox"].variants if v.id == "medium"
)
assert gmedium.accents == () assert gmedium.accents == ()
@@ -124,11 +145,15 @@ class TestGetPalette:
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(
KeyError, match="Unknown variant 'bogus' for scheme 'gruvbox'"
):
sp.get_palette("gruvbox", "bogus", "dark") 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(
"catppuccin", "mocha", "dark", accent="nonexistent"
)
assert pal.accent == "nonexistent" assert pal.accent == "nonexistent"
assert pal.colors["primary"] is not None assert pal.colors["primary"] is not None
@@ -164,4 +189,7 @@ class TestResolvePreset:
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 ",
)
+56 -15
View File
@@ -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
@@ -21,11 +21,15 @@ class TestKill:
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(
[], 255, b"", b"No running instances\n"
)
result = runner.invoke(app, ["kill"]) result = runner.invoke(app, ["kill"])
assert result.exit_code != 0 assert result.exit_code != 0
assert "No running instance to kill" in result.output assert "No running instance to kill" in result.output
@@ -34,19 +38,32 @@ class TestKill:
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(
[], 0, b"", b"Launching config\n"
)
invoke("start") invoke("start")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], capture_output=True) 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(
[], 0, b"", b"Launching config\n"
)
invoke("start", "--no-daemon") invoke("start", "--no-daemon")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], capture_output=True) 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(
[],
0,
b"An instance of this configuration is already running.\n",
b"",
)
result = runner.invoke(app, ["start"]) result = runner.invoke(app, ["start"])
assert result.exit_code != 0 assert result.exit_code != 0
assert "already running" in result.output assert "already running" in result.output
@@ -62,10 +79,14 @@ class TestStart:
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(
[], 0, b"target visibilities\n", b""
)
result = invoke("show") result = invoke("show")
assert "target visibilities" in result.output assert "target visibilities" in result.output
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True) mock_run.assert_called_once_with(
["qs", "-c", "zshell", "ipc", "show"], capture_output=True
)
class TestLog: class TestLog:
@@ -73,7 +94,9 @@ class TestLog:
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:
@@ -81,7 +104,10 @@ class TestLock:
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:
@@ -89,14 +115,27 @@ class TestCall:
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"], [
"qs",
"-c",
"zshell",
"ipc",
"call",
"target",
"method",
"arg1",
"arg2",
],
capture_output=True, capture_output=True,
) )
@@ -106,7 +145,9 @@ class TestRestart:
@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(
[], 0, b"", b"Killed abc\n"
), # first kill (captured)
CompletedProcess([], 255, b"", b""), # poll → no instance CompletedProcess([], 255, b"", b""), # poll → no instance
] ]
invoke("restart") invoke("restart")
+27 -5
View File
@@ -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"]
+81 -36
View File
@@ -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}
@@ -45,8 +47,7 @@ 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]] = []
@@ -92,14 +93,16 @@ def parse_page_registry(settings: Path) -> list[tuple[str, str]]:
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(
lines: list[str], i: int
) -> tuple[str, list[tuple[str, list]], int]:
line = _strip_comment(lines[i]).strip() line = _strip_comment(lines[i]).strip()
m = BLOCK_RE.match(line) m = BLOCK_RE.match(line)
if not m: if not m:
@@ -152,8 +155,9 @@ 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]] = []
@@ -180,10 +184,12 @@ def parse_page_comps(settings: Path) -> list[list[str]]:
return comps return comps
def dedup_crumbs(labels: list[str], icons: list[str]) -> tuple[list[str], list[str]]: def dedup_crumbs(
labels: list[str], icons: list[str]
) -> tuple[list[str], list[str]]:
out_labels: list[str] = [] out_labels: list[str] = []
out_icons: list[str] = [] out_icons: list[str] = []
for lbl, ico in zip(labels, icons): for lbl, ico in zip(labels, icons, strict=False):
if out_labels and out_labels[-1] == lbl: if out_labels and out_labels[-1] == lbl:
continue continue
out_labels.append(lbl) out_labels.append(lbl)
@@ -227,7 +233,10 @@ def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
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_label or "",
section,
)
pending_icon = pending_label = None pending_icon = pending_label = None
nav: dict[str, dict] = {} nav: dict[str, dict] = {}
@@ -236,8 +245,12 @@ def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
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,
"subPath": [],
"crumbIcons": [main_icon],
"crumbLabels": [main_label],
}
children = dict(nav_children.get(main, {})) children = dict(nav_children.get(main, {}))
opened_via_subpage = set() opened_via_subpage = set()
for owner, kids in nav_children.items(): for owner, kids in nav_children.items():
@@ -259,19 +272,26 @@ def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
labels = [main_label] + ([section] if section else []) + [label] labels = [main_label] + ([section] if section else []) + [label]
icons = [main_icon] + ([icon] if section else []) + [icon] icons = [main_icon] + ([icon] if section else []) + [icon]
labels, icons = dedup_crumbs(labels, icons) labels, icons = dedup_crumbs(labels, icons)
nav[child] = {"pageIdx": top_idx, "subPath": [pos], nav[child] = {
"pageIdx": top_idx,
"subPath": [pos],
"crumbIcons": icons, "crumbIcons": icons,
"crumbLabels": labels} "crumbLabels": labels,
for gpos, (gicon, glabel, gsection) in nav_children.get(child, {}).items(): }
for gpos, (gicon, glabel, gsection) in nav_children.get(
child, {}
).items():
if gpos >= len(names): if gpos >= len(names):
continue continue
glabels = labels + ([gsection] if gsection else []) + [glabel] glabels = labels + ([gsection] if gsection else []) + [glabel]
gicons = icons + ([gicon] if gsection else []) + [gicon] gicons = icons + ([gicon] if gsection else []) + [gicon]
glabels, gicons = dedup_crumbs(glabels, gicons) glabels, gicons = dedup_crumbs(glabels, gicons)
nav[names[gpos]] = { nav[names[gpos]] = {
"pageIdx": top_idx, "subPath": [pos, gpos], "pageIdx": top_idx,
"subPath": [pos, gpos],
"crumbIcons": gicons, "crumbIcons": gicons,
"crumbLabels": glabels} "crumbLabels": glabels,
}
return nav return nav
@@ -290,10 +310,12 @@ def tokenize(text: str) -> list[str]:
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(
files: dict[str, Path], nav: dict[str, dict]
) -> list[dict]:
entries: list[dict] = [] entries: list[dict] = []
for comp, meta in nav.items(): for comp, meta in nav.items():
pf = files.get(comp) pf = files.get(comp)
@@ -337,22 +359,35 @@ def extract_settings(files: dict[str, Path], nav: dict[str, dict]) -> list[dict]
toggled_path = tg.group(1) toggled_path = tg.group(1)
toggle_path = ( toggle_path = (
checked_path checked_path
if row_type == "ToggleRow" and checked_path and checked_path == toggled_path if row_type == "ToggleRow"
and checked_path
and checked_path == toggled_path
else "" else ""
) )
if label and label not in SKIP_LABELS and anchor: if label and label not in SKIP_LABELS and anchor:
extra = " ".join(meta["crumbLabels"]) + \ extra = (
" " + section + " " + (subtext or "") " ".join(meta["crumbLabels"])
entries.append({ + " "
"pageIdx": meta["pageIdx"], "subPath": meta["subPath"], + section
+ " "
+ (subtext or "")
)
entries.append(
{
"pageIdx": meta["pageIdx"],
"subPath": meta["subPath"],
"crumbIcons": meta["crumbIcons"], "crumbIcons": meta["crumbIcons"],
"crumbLabels": meta["crumbLabels"], "crumbLabels": meta["crumbLabels"],
"title": label, "anchor": anchor, "title": label,
"anchor": anchor,
"section": section, "section": section,
"subtext": subtext or "", "subtext": subtext or "",
"togglePath": toggle_path, "togglePath": toggle_path,
"keywords": " ".join(sorted(set(tokenize(label + " " + extra)))), "keywords": " ".join(
}) sorted(set(tokenize(label + " " + extra)))
),
}
)
i += 1 i += 1
return entries return entries
@@ -372,7 +407,9 @@ def build_inverted_and_ranking(entries: list[dict]):
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:
@@ -387,14 +424,22 @@ def main() -> int:
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(
json.dumps(
{
"version": 2, "version": 2,
"entries": entries, "entries": entries,
"inverted": inverted, "inverted": inverted,
"ranking": ranking, "ranking": ranking,
}, ensure_ascii=False, indent=2)) },
print(f"settings index: {len(entries)} entries, " ensure_ascii=False,
f"{len(inverted)} tokens -> {out}") indent=2,
)
)
print(
f"settings index: {len(entries)} entries, "
f"{len(inverted)} tokens -> {out}"
)
print("files:", len(files)) print("files:", len(files))
print("comps:", len(parse_page_comps(settings))) print("comps:", len(parse_page_comps(settings)))
print("registry:", len(parse_page_registry(settings))) print("registry:", len(parse_page_registry(settings)))