Compare commits
18
Commits
1cbe9a9f8a
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc1a123fa | ||
|
|
672cb07547 | ||
|
|
e3b2d33a11 | ||
|
|
2a40696292 | ||
|
|
80ee714e91 | ||
|
|
2c572c3250 | ||
|
|
0912a8e40c | ||
|
|
0d17621180 | ||
|
|
0ae6d6fc55 | ||
|
|
9300da258e | ||
|
|
298cbbf424 | ||
|
|
b19b630a77 | ||
|
|
75f0f2071d | ||
|
|
bfea718547 | ||
|
|
5505ac19ba | ||
|
|
9d2dc281ec | ||
|
|
05c8851143 | ||
|
|
71b25bff49 |
@@ -2,7 +2,7 @@ name: Rebuild CI Image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 6 * * 1'
|
- cron: "0 6 * * 1"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: node:26-alpine
|
image: node:26-alpine
|
||||||
env:
|
env:
|
||||||
IMAGE: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
IMAGE: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run: apk add --no-cache docker-cli
|
run: apk add --no-cache docker-cli
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.nl --username aramjonghu --password-stdin
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
||||||
|
|||||||
@@ -4,10 +4,43 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
fmt:
|
||||||
|
runs-on: alpine
|
||||||
|
container:
|
||||||
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
|
||||||
|
-not -path './build/*' \
|
||||||
|
-exec clang-format -i --style=file {} +
|
||||||
|
git diff --exit-code && echo "clang-format: passed"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container:
|
container:
|
||||||
image: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure
|
||||||
|
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ninja -C build
|
||||||
|
|
||||||
|
clang-tidy:
|
||||||
|
runs-on: alpine
|
||||||
|
container:
|
||||||
|
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Lint & Format (JS/TS)
|
name: JS/TS
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-format:
|
fmt:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ jobs:
|
|||||||
git
|
git
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||||
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
|
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
|
||||||
@@ -26,6 +25,19 @@ jobs:
|
|||||||
echo "No JS/TS files found"
|
echo "No JS/TS files found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git
|
||||||
|
|
||||||
- name: ESLint
|
- name: ESLint
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
name: Lint & Format (Rust)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint-format:
|
|
||||||
runs-on: alpine
|
|
||||||
container: node:26-alpine
|
|
||||||
env:
|
|
||||||
CARGO_HOME: ${{ github.workspace }}/.cargo
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Cache cargo packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
env:
|
|
||||||
cache-name: cache-cargo-packages
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.cargo/registry
|
|
||||||
.cargo/git
|
|
||||||
target
|
|
||||||
key: rust-${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
rust-${{ runner.os }}-build-
|
|
||||||
rust-
|
|
||||||
|
|
||||||
- name: Install tools
|
|
||||||
run: |
|
|
||||||
apk add --no-cache \
|
|
||||||
git \
|
|
||||||
cargo \
|
|
||||||
rust \
|
|
||||||
rustfmt \
|
|
||||||
rust-clippy
|
|
||||||
|
|
||||||
- id: format-check
|
|
||||||
name: Format check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
|
||||||
status=0
|
|
||||||
for manifest in $(find . -name "Cargo.toml"); do
|
|
||||||
cargo fmt --manifest-path "$manifest" --check && \
|
|
||||||
echo "$manifest: formatting OK" || \
|
|
||||||
{ echo "$manifest: needs formatting"; status=1; }
|
|
||||||
done
|
|
||||||
exit $status
|
|
||||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
|
||||||
echo "Rust files found but no Cargo.toml"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "No Rust project found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- id: clippy
|
|
||||||
name: Clippy
|
|
||||||
run: |
|
|
||||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
|
||||||
status=0
|
|
||||||
for manifest in $(find . -name "Cargo.toml"); do
|
|
||||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
|
||||||
echo "$manifest: Clippy passed" || \
|
|
||||||
{ echo "$manifest: Clippy failed"; status=1; }
|
|
||||||
done
|
|
||||||
exit $status
|
|
||||||
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
|
||||||
echo "Rust files found but no Cargo.toml"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "No Rust project found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check results
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.format-check.outcome }}" = "failure" ] || [ "${{ steps.clippy.outcome }}" = "failure" ]; then
|
|
||||||
echo "One or more checks failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "All checks passed"
|
|
||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-format:
|
fmt:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|
||||||
@@ -23,11 +23,28 @@ jobs:
|
|||||||
pip install --no-cache-dir ruff
|
pip install --no-cache-dir ruff
|
||||||
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
ruff format --check .
|
ruff format --check .
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
py3-pip
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install --no-cache-dir ruff
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
@@ -63,3 +80,30 @@ jobs:
|
|||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
cd cli
|
cd cli
|
||||||
python -m pytest tests/ -v
|
python -m pytest tests/ -v
|
||||||
|
|
||||||
|
buildcheck:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
py3-pip \
|
||||||
|
build-base \
|
||||||
|
python3-dev \
|
||||||
|
gcc \
|
||||||
|
g++
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install --no-cache-dir nuitka
|
||||||
|
|
||||||
|
- name: Nuitka module check
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
|
nuitka --module --include-package=zshell cli/src/zshell/
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-build-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust
|
||||||
|
|
||||||
|
- name: Cargo check
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo check --manifest-path "$manifest" && \
|
||||||
|
echo "$manifest: check passed" || \
|
||||||
|
{ echo "$manifest: check failed"; exit 1; }
|
||||||
|
done
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust \
|
||||||
|
rustfmt
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
status=0
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo fmt --manifest-path "$manifest" --check && \
|
||||||
|
echo "$manifest: formatting OK" || \
|
||||||
|
{ echo "$manifest: needs formatting"; status=1; }
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
env:
|
||||||
|
CARGO_HOME: ${{ github.workspace }}/.cargo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-cargo-packages
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.cargo/registry
|
||||||
|
.cargo/git
|
||||||
|
target
|
||||||
|
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
rust-${{ runner.os }}-build-
|
||||||
|
rust-
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
cargo \
|
||||||
|
rust \
|
||||||
|
rust-clippy
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
run: |
|
||||||
|
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||||
|
status=0
|
||||||
|
for manifest in $(find . -name "Cargo.toml"); do
|
||||||
|
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||||
|
echo "$manifest: Clippy passed" || \
|
||||||
|
{ echo "$manifest: Clippy failed"; status=1; }
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
|
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
|
||||||
|
echo "Rust files found but no Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No Rust project found"
|
||||||
|
fi
|
||||||
+60
-3
@@ -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()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ListView {
|
|||||||
|
|
||||||
property bool doneFakeFlick
|
property bool doneFakeFlick
|
||||||
|
|
||||||
interactive: !Visibilities.getForActive().isDrawing
|
interactive: !Visibilities.getForActive()?.isDrawing
|
||||||
maximumFlickVelocity: 3000
|
maximumFlickVelocity: 3000
|
||||||
|
|
||||||
rebound: Transition {
|
rebound: Transition {
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import M3Shapes
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
MaterialShape {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool animated: true
|
||||||
|
property real cRotation
|
||||||
|
property bool containsIcon
|
||||||
|
property real dampingRatio: 0.6
|
||||||
|
property real lRotation
|
||||||
|
property int morphAnimRotation: 60
|
||||||
|
property real morphScale: 0.14
|
||||||
|
property alias rotateAnimDuration: rotateAnim.duration
|
||||||
|
property int shapeIndex
|
||||||
|
property list<int> shapes: {
|
||||||
|
if (containsIcon)
|
||||||
|
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||||
|
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||||
|
}
|
||||||
|
readonly property real springDuration: {
|
||||||
|
const wn = Math.sqrt(stiffness);
|
||||||
|
const r = -dampingRatio * wn;
|
||||||
|
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||||
|
return Math.log(visibilityThreshold / c) / r;
|
||||||
|
}
|
||||||
|
readonly property real springMaxVelocity: {
|
||||||
|
const wn = Math.sqrt(stiffness);
|
||||||
|
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
||||||
|
return wn * factor;
|
||||||
|
}
|
||||||
|
property bool springSettled: true
|
||||||
|
property real stiffness: 180
|
||||||
|
property real thisLRotation
|
||||||
|
property real visibilityThreshold: 0.075
|
||||||
|
|
||||||
|
function spring(t: real): var {
|
||||||
|
const wn = Math.sqrt(stiffness);
|
||||||
|
const za = dampingRatio * wn;
|
||||||
|
|
||||||
|
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||||
|
const r = za / wd;
|
||||||
|
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
||||||
|
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
||||||
|
|
||||||
|
return [pos, vel];
|
||||||
|
}
|
||||||
|
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
implicitSize: 38
|
||||||
|
toShape: shapes[0]
|
||||||
|
|
||||||
|
RotationAnimation on cRotation {
|
||||||
|
id: rotateAnim
|
||||||
|
|
||||||
|
duration: 4666
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
from: 0
|
||||||
|
loops: Animation.Infinite
|
||||||
|
running: root.animated
|
||||||
|
to: 360
|
||||||
|
}
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElapsedTimer {
|
||||||
|
id: timer
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameAnimation {
|
||||||
|
running: root.animated && !root.springSettled
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
const t = timer.elapsed();
|
||||||
|
|
||||||
|
if (t >= root.springDuration) {
|
||||||
|
root.springSettled = true;
|
||||||
|
} else {
|
||||||
|
const [pos, vel] = root.spring(t);
|
||||||
|
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
||||||
|
root.thisLRotation = pos * root.morphAnimRotation;
|
||||||
|
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 650
|
||||||
|
repeat: true
|
||||||
|
running: root.animated
|
||||||
|
triggeredOnStart: true
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.beginBatchUpdate();
|
||||||
|
root.fromShape = root.toShape;
|
||||||
|
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
||||||
|
root.toShape = root.shapes[root.shapeIndex];
|
||||||
|
root.morphProgress = 0;
|
||||||
|
|
||||||
|
root.rotation = root.rotation;
|
||||||
|
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
||||||
|
root.thisLRotation = 0;
|
||||||
|
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
||||||
|
|
||||||
|
root.springSettled = false;
|
||||||
|
timer.restart();
|
||||||
|
root.endBatchUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ MouseArea {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||||
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
|
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
Behavior on stateOpacity {
|
Behavior on stateOpacity {
|
||||||
|
|||||||
@@ -6,25 +6,55 @@ import qs.Effects
|
|||||||
CustomListView {
|
CustomListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||||
property real fadeAmount: 0.1
|
property real fadeAmount: 0.1
|
||||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||||
|
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||||
|
|
||||||
function fadeShouldBeActive(isStart: bool): bool {
|
function contentSize(): real {
|
||||||
// When content is smaller than flickable size, hide fade when rebound starts
|
return horizontal ? contentWidth : contentHeight;
|
||||||
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (isStart)
|
|
||||||
return visibleArea.yPosition > 0;
|
|
||||||
return visibleArea.yPosition + visibleArea.heightRatio < 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
function fadeShouldBeActive(isStart: bool): bool {
|
||||||
layer.enabled: true
|
// When content is smaller than flickable size, hide fade when rebound starts.
|
||||||
orientation: ListView.Vertical
|
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on bottomFadeOpacity {
|
if (isStart)
|
||||||
|
return visibleStart() > 0;
|
||||||
|
|
||||||
|
return visibleStart() + visibleRatio() < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginEnd(): real {
|
||||||
|
return horizontal ? rightMargin : bottomMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginStart(): real {
|
||||||
|
return horizontal ? leftMargin : topMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function overshootStart(): real {
|
||||||
|
return horizontal ? horizontalOvershoot : verticalOvershoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewportSize(): real {
|
||||||
|
return horizontal ? width : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleRatio(): real {
|
||||||
|
return horizontal ? visibleArea.widthRatio : visibleArea.heightRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleStart(): real {
|
||||||
|
return horizontal ? visibleArea.xPosition : visibleArea.yPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
flickableDirection: horizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
|
||||||
|
layer.enabled: true
|
||||||
|
|
||||||
|
Behavior on endFadeOpacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
@@ -40,10 +70,10 @@ CustomListView {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
orientation: Gradient.Vertical
|
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
||||||
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
||||||
position: 0
|
position: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +88,13 @@ CustomListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
||||||
position: 1
|
position: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on topFadeOpacity {
|
Behavior on startFadeOpacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property real amplitude: 3
|
||||||
|
property color color: DynamicColors.palette.m3surfaceContainer
|
||||||
|
readonly property real waveHeight: amplitude * 2
|
||||||
|
property int waves: 4
|
||||||
|
|
||||||
|
asynchronous: true
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
fillColor: root.color
|
||||||
|
strokeColor: "transparent"
|
||||||
|
strokeWidth: 0
|
||||||
|
|
||||||
|
Behavior on fillColor {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathSvg {
|
||||||
|
path: {
|
||||||
|
const w = root.width;
|
||||||
|
const h = root.height;
|
||||||
|
const a = root.amplitude;
|
||||||
|
const n = Math.max(1, root.waves);
|
||||||
|
const wl = w / n;
|
||||||
|
const half = wl / 2;
|
||||||
|
|
||||||
|
let d = `M 0,${a} `;
|
||||||
|
for (let i = 0; i < n; ++i) {
|
||||||
|
const x = i * wl;
|
||||||
|
d += `Q ${x + half / 2},${-a} ${x + half},${a} `;
|
||||||
|
d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `;
|
||||||
|
}
|
||||||
|
d += `L ${w},${h} L 0,${h} Z`;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -311,17 +311,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUtilitiesChanged() {
|
|
||||||
if (root.visibilities.utilities) {
|
|
||||||
const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);
|
|
||||||
if (!inUtilitiesArea) {
|
|
||||||
root.utilitiesShortcutActive = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.utilitiesShortcutActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root.visibilities
|
target: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ Region {
|
|||||||
R {
|
R {
|
||||||
panel: root.panels.osdWrapper
|
panel: root.panels.osdWrapper
|
||||||
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
||||||
x: root.win.width - width
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R {
|
R {
|
||||||
|
|||||||
+1
-9
@@ -26,7 +26,7 @@ CustomWindow {
|
|||||||
if (focusGrab.active)
|
if (focusGrab.active)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
|
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace?.lastIpcObject.windows > 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return 100;
|
return 100;
|
||||||
@@ -162,14 +162,6 @@ CustomWindow {
|
|||||||
Component.onCompleted: Visibilities.load(root.screen, this)
|
Component.onCompleted: Visibilities.load(root.screen, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function toggleLauncher(fix: string): void {
|
|
||||||
visibilities.launcher = !visibilities.launcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "visibilities"
|
|
||||||
}
|
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
property: "bar"
|
property: "bar"
|
||||||
target: visibilities
|
target: visibilities
|
||||||
|
|||||||
+15
-42
@@ -11,8 +11,13 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool appleDisplayPresent: false
|
property bool appleDisplayPresent: false
|
||||||
|
readonly property var ddcMonitorMap: {
|
||||||
|
const map = {};
|
||||||
|
for (const m of ddcMonitors)
|
||||||
|
map[m.connector] = m;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
property list<var> ddcMonitors: []
|
property list<var> ddcMonitors: []
|
||||||
property list<var> ddcServiceMon: []
|
|
||||||
readonly property list<Monitor> monitors: variants.instances
|
readonly property list<Monitor> monitors: variants.instances
|
||||||
|
|
||||||
function decreaseBrightness(): void {
|
function decreaseBrightness(): void {
|
||||||
@@ -56,8 +61,6 @@ Singleton {
|
|||||||
|
|
||||||
onMonitorsChanged: {
|
onMonitorsChanged: {
|
||||||
ddcMonitors = [];
|
ddcMonitors = [];
|
||||||
ddcServiceMon = [];
|
|
||||||
ddcServiceProc.running = true;
|
|
||||||
ddcProc.running = true;
|
ddcProc.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,26 +95,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: ddcServiceProc
|
|
||||||
|
|
||||||
command: ["ddcutil-client", "detect"]
|
|
||||||
|
|
||||||
// running: true
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const t = text.replace(/\r\n/g, "\n").trim();
|
|
||||||
|
|
||||||
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
|
|
||||||
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
|
|
||||||
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
|
|
||||||
})).filter(d => d.display > 0);
|
|
||||||
root.ddcServiceMon = output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Increase brightness"
|
description: "Increase brightness"
|
||||||
name: "brightnessUp"
|
name: "brightnessUp"
|
||||||
@@ -183,16 +166,12 @@ Singleton {
|
|||||||
id: monitor
|
id: monitor
|
||||||
|
|
||||||
property real brightness
|
property real brightness
|
||||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
readonly property string busNum: ddcInfo?.busNum ?? ""
|
||||||
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
|
readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null
|
||||||
readonly property Process initProc: Process {
|
readonly property Process initProc: Process {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (monitor.isDdcService) {
|
if (monitor.isAppleDisplay) {
|
||||||
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
|
|
||||||
const val = parseInt(output.trim());
|
|
||||||
monitor.brightness = val / 100;
|
|
||||||
} else if (monitor.isAppleDisplay) {
|
|
||||||
const val = parseInt(text.trim());
|
const val = parseInt(text.trim());
|
||||||
monitor.brightness = val / 101;
|
monitor.brightness = val / 101;
|
||||||
} else {
|
} else {
|
||||||
@@ -203,12 +182,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
|
readonly property bool isDdc: ddcInfo !== null
|
||||||
readonly property bool isDdcService: Config.services.ddcutilService
|
|
||||||
required property ShellScreen modelData
|
required property ShellScreen modelData
|
||||||
property real queuedBrightness: NaN
|
property real queuedBrightness: NaN
|
||||||
readonly property Timer timer: Timer {
|
readonly property Timer timer: Timer {
|
||||||
interval: 500
|
interval: 400
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!isNaN(monitor.queuedBrightness)) {
|
if (!isNaN(monitor.queuedBrightness)) {
|
||||||
@@ -219,9 +197,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initBrightness(): void {
|
function initBrightness(): void {
|
||||||
if (isDdcService)
|
if (isAppleDisplay)
|
||||||
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
|
|
||||||
else if (isAppleDisplay)
|
|
||||||
initProc.command = ["asdbctl", "get"];
|
initProc.command = ["asdbctl", "get"];
|
||||||
else if (isDdc)
|
else if (isDdc)
|
||||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||||
@@ -237,28 +213,25 @@ Singleton {
|
|||||||
if (Math.round(brightness * 100) === rounded)
|
if (Math.round(brightness * 100) === rounded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ((isDdc || isDdcService) && timer.running) {
|
if (isDdc && timer.running) {
|
||||||
queuedBrightness = value;
|
queuedBrightness = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
brightness = value;
|
brightness = value;
|
||||||
|
|
||||||
if (isDdcService)
|
if (isAppleDisplay)
|
||||||
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
|
|
||||||
else if (isAppleDisplay)
|
|
||||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||||
else if (isDdc)
|
else if (isDdc)
|
||||||
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
||||||
else
|
else
|
||||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||||
|
|
||||||
if (isDdc || isDdcService)
|
if (isDdc)
|
||||||
timer.restart();
|
timer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: initBrightness()
|
Component.onCompleted: initBrightness()
|
||||||
onBusNumChanged: initBrightness()
|
onBusNumChanged: initBrightness()
|
||||||
onDisplayNumChanged: initBrightness()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-3
@@ -1,12 +1,12 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQml
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import QtQml
|
|
||||||
import ZShell
|
import ZShell
|
||||||
import qs.Config
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -15,7 +15,24 @@ Singleton {
|
|||||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||||
property alias manualActive: props.manualActive
|
property alias manualActive: props.manualActive
|
||||||
|
|
||||||
|
function getArtUrl(player: MprisPlayer): string {
|
||||||
|
if (!player)
|
||||||
|
return "";
|
||||||
|
if (player.trackArtUrl)
|
||||||
|
return player.trackArtUrl;
|
||||||
|
|
||||||
|
const url = player.metadata["xesam:url"] ?? "";
|
||||||
|
if (url.startsWith("https://www.youtube.com/watch")) {
|
||||||
|
// Fallback for youtube
|
||||||
|
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
|
||||||
|
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function getIdentity(player: MprisPlayer): string {
|
function getIdentity(player: MprisPlayer): string {
|
||||||
|
if (!player)
|
||||||
|
return "";
|
||||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||||
return alias?.to ?? player.identity;
|
return alias?.to ?? player.identity;
|
||||||
}
|
}
|
||||||
@@ -25,9 +42,12 @@ Singleton {
|
|||||||
if (!Config.utilities.toasts.nowPlaying) {
|
if (!Config.utilities.toasts.nowPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
|
||||||
|
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: active
|
target: root.active
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistentProperties {
|
PersistentProperties {
|
||||||
@@ -38,8 +58,10 @@ Singleton {
|
|||||||
reloadableId: "players"
|
reloadableId: "players"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Toggle media playback"
|
description: "Toggle media playback"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaToggle"
|
name: "mediaToggle"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -49,8 +71,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Previous track"
|
description: "Previous track"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaPrev"
|
name: "mediaPrev"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -60,8 +84,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Next track"
|
description: "Next track"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaNext"
|
name: "mediaNext"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -71,8 +97,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qmllint disable unresolved-type
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Stop media playback"
|
description: "Stop media playback"
|
||||||
|
// qmllint enable unresolved-type
|
||||||
name: "mediaStop"
|
name: "mediaStop"
|
||||||
|
|
||||||
onPressed: root.active?.stop()
|
onPressed: root.active?.stop()
|
||||||
|
|||||||
+107
-32
@@ -1,7 +1,7 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import ZShell
|
import ZShell
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
@@ -12,15 +12,15 @@ Singleton {
|
|||||||
property var cc
|
property var cc
|
||||||
property string city
|
property string city
|
||||||
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
||||||
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
|
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
|
||||||
property list<var> forecast
|
property list<var> forecast
|
||||||
property list<var> hourlyForecast
|
property list<var> hourlyForecast
|
||||||
readonly property int humidity: cc?.humidity ?? 0
|
readonly property int humidity: cc?.humidity ?? 0
|
||||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||||
property string loc
|
property string loc
|
||||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||||
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
readonly property string temp: formatTemp(cc?.tempC)
|
||||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||||
|
|
||||||
function fetchCityFromCoords(coords: string): void {
|
function fetchCityFromCoords(coords: string): void {
|
||||||
@@ -29,29 +29,48 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lat, lon] = coords.split(",");
|
const [lat, lon] = coords.split(",").map(s => s.trim());
|
||||||
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||||
Requests.get(url, text => {
|
|
||||||
const 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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property int rootHeight
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: Weather.description
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: temp
|
||||||
|
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: Weather.temp
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
text: Weather.icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: qsTr("Feels like %1").arg(Weather.temp)
|
||||||
|
visible: root.rootHeight > 550
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: {
|
||||||
|
const today = Weather.forecast[0];
|
||||||
|
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||||
|
}
|
||||||
|
visible: root.rootHeight > 550
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import M3Shapes
|
||||||
|
import ZShell
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||||
|
implicitHeight: header.anchors.margins + header.implicitHeight + Appearance.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.largeIncreased
|
||||||
|
anchors.top: parent.top
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: "schedule"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: title
|
||||||
|
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: qsTr("Hourly forecast")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalFadeListView {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: Appearance.padding.largeIncreased
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitHeight: contentItem.childrenRect.height
|
||||||
|
model: Weather.hourlyForecast
|
||||||
|
orientation: VerticalFadeListView.Horizontal
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
id: hour
|
||||||
|
|
||||||
|
readonly property var cond: modelData
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
MaterialShape {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: Qt.alpha(DynamicColors.palette.m3primary, hour.index === 0 ? 1 : 0)
|
||||||
|
implicitSize: temp.implicitHeight + Appearance.padding.normal * 2
|
||||||
|
shape: MaterialShape.Cookie4Sided
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: temp
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: hour.index === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
|
font.pointSize: Appearance.font.size.medium
|
||||||
|
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge
|
||||||
|
text: hour.cond.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
text: hour.cond.precipChance + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: Appearance.spacing.extraSmall
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-150
@@ -1,162 +1,23 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import qs.Modules.Lock.Weather
|
||||||
|
import qs.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Config
|
|
||||||
|
|
||||||
ColumnLayout {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property int rootHeight
|
required property int rootHeight
|
||||||
|
readonly property bool showForecast: rootHeight >= 700
|
||||||
|
|
||||||
anchors.left: parent.left
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
anchors.margins: Appearance.padding.large * 2
|
implicitHeight: {
|
||||||
anchors.right: parent.right
|
const base = brief.implicitHeight + brief.anchors.topMargin;
|
||||||
spacing: Appearance.spacing.small
|
if (showForecast)
|
||||||
|
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
||||||
Loader {
|
return base + brief.anchors.topMargin;
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.bottomMargin: -Appearance.padding.large
|
|
||||||
Layout.topMargin: Appearance.padding.large * 2
|
|
||||||
active: root.rootHeight > 610
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: CustomText {
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge
|
|
||||||
font.weight: 500
|
|
||||||
text: qsTr("Weather")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.large
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge * 2.5
|
|
||||||
text: Weather.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
font.weight: 500
|
|
||||||
text: Weather.description
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pointSize: Appearance.font.size.normal
|
|
||||||
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
Layout.rightMargin: Appearance.padding.smaller
|
|
||||||
active: root.width > 400
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: ColumnLayout {
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
elide: Text.ElideLeft
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge
|
|
||||||
font.weight: 500
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: Weather.temp
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3outline
|
|
||||||
elide: Text.ElideLeft
|
|
||||||
font.pointSize: Appearance.font.size.smaller
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: forecastLoader
|
|
||||||
|
|
||||||
Layout.bottomMargin: Appearance.padding.large * 2
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: Appearance.spacing.smaller
|
|
||||||
active: root.rootHeight > 820
|
|
||||||
visible: active
|
|
||||||
|
|
||||||
sourceComponent: RowLayout {
|
|
||||||
spacing: Appearance.spacing.large
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
const forecast = Weather.hourlyForecast;
|
|
||||||
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
|
||||||
if (!forecast)
|
|
||||||
return Array.from({
|
|
||||||
length: count
|
|
||||||
}, () => null);
|
|
||||||
|
|
||||||
return forecast.slice(0, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: forecastHour
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: DynamicColors.palette.m3outline
|
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: {
|
|
||||||
const hour = forecastHour.modelData?.hour ?? 0;
|
|
||||||
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge * 1.5
|
|
||||||
font.weight: 500
|
|
||||||
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 900000 // 15 minutes
|
interval: 900000 // 15 minutes
|
||||||
@@ -166,4 +27,27 @@ ColumnLayout {
|
|||||||
|
|
||||||
onTriggered: Weather.reload()
|
onTriggered: Weather.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BriefInfo {
|
||||||
|
id: brief
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Appearance.padding.extraLarge
|
||||||
|
rootHeight: root.rootHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: forecast
|
||||||
|
|
||||||
|
active: root.showForecast
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
anchors.right: parent.right
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
sourceComponent: Forecast {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ Scope {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
function onShouldShowChanged(): void {
|
||||||
|
|
||||||
onShouldShowChanged: {
|
|
||||||
if (root.shouldShow) {
|
if (root.shouldShow) {
|
||||||
panelWindow.visible = true;
|
panelWindow.visible = true;
|
||||||
openAnim.start();
|
openAnim.start();
|
||||||
@@ -38,6 +36,8 @@ Scope {
|
|||||||
closeAnim.start();
|
closeAnim.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: root
|
||||||
}
|
}
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
|
|||||||
@@ -32,6 +32,31 @@ Item {
|
|||||||
implicitHeight: width
|
implicitHeight: width
|
||||||
radius: Appearance.rounding.large
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: img.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
color: DynamicColors.palette.m3primaryContainer
|
||||||
|
implicitHeight: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||||
|
implicitWidth: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
LoadingIndicator {
|
||||||
|
id: loadingIndicator
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
containsIcon: true
|
||||||
|
implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: img
|
id: img
|
||||||
|
|
||||||
|
|||||||
@@ -148,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool completed
|
property bool completed
|
||||||
property real cropHeight: displayData.height ?? 1.0
|
property real cropHeight: displayData?.height ?? 1.0
|
||||||
property real cropWidth: displayData.width ?? 1.0
|
property real cropWidth: displayData?.width ?? 1.0
|
||||||
property real cropX: displayData.x ?? 0.0
|
property real cropX: displayData?.x ?? 0.0
|
||||||
property real cropY: displayData.y ?? 0.0
|
property real cropY: displayData?.y ?? 0.0
|
||||||
property WallpaperImage current
|
property WallpaperImage current
|
||||||
readonly property var displayData: Wallpapers.getCrop(screen.name)
|
readonly property var displayData: Wallpapers.getCrop(screen.name)
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
file(GLOB ZSHELL_CLI_WHEEL "@ZSHELL_CLI_DIST_DIR@/*.whl")
|
||||||
|
if(NOT ZSHELL_CLI_WHEEL)
|
||||||
|
message(FATAL_ERROR "No zshell-cli wheel found in @ZSHELL_CLI_DIST_DIR@")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_zshell_installer_args "--prefix=@CMAKE_INSTALL_PREFIX@")
|
||||||
|
if(DEFINED ENV{DESTDIR} AND NOT "$ENV{DESTDIR}" STREQUAL "")
|
||||||
|
list(APPEND _zshell_installer_args "--destdir=$ENV{DESTDIR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND "@Python3_EXECUTABLE@" -m installer
|
||||||
|
${_zshell_installer_args}
|
||||||
|
${ZSHELL_CLI_WHEEL}
|
||||||
|
RESULT_VARIABLE ZSHELL_CLI_INSTALL_RESULT
|
||||||
|
)
|
||||||
|
if(NOT ZSHELL_CLI_INSTALL_RESULT EQUAL 0)
|
||||||
|
message(FATAL_ERROR "zshell-cli wheel install failed")
|
||||||
|
endif()
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
args = ["qs", "-c", "zshell"]
|
args = ["qs", "-c", "zshell"]
|
||||||
@@ -8,9 +9,9 @@ app = typer.Typer()
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def start():
|
def start():
|
||||||
subprocess.run(args + ["ipc"] + ["call"] + ["picker"] + ["open"], check=True)
|
subprocess.run([*args, "ipc", "call", "picker", "open"], check=True)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def start_freeze():
|
def start_freeze():
|
||||||
subprocess.run(args + ["ipc"] + ["call"] + ["picker"] + ["openFreeze"], check=True)
|
subprocess.run([*args, "ipc", "call", "picker", "openFreeze"], check=True)
|
||||||
|
|||||||
@@ -11,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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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)))
|
||||||
|
|||||||
Reference in New Issue
Block a user