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:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1'
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
container:
|
||||
image: node:26-alpine
|
||||
env:
|
||||
IMAGE: git.aramjonghu.nl/aramjonghu/zshell-ci:latest
|
||||
IMAGE: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: apk add --no-cache docker-cli
|
||||
|
||||
- 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
|
||||
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
||||
|
||||
@@ -4,10 +4,43 @@ on:
|
||||
pull_request:
|
||||
|
||||
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:
|
||||
runs-on: alpine
|
||||
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:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Lint & Format (JS/TS)
|
||||
name: JS/TS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
@@ -18,7 +18,6 @@ jobs:
|
||||
git
|
||||
|
||||
- name: Prettier
|
||||
continue-on-error: true
|
||||
run: |
|
||||
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
|
||||
@@ -26,6 +25,19 @@ jobs:
|
||||
echo "No JS/TS files found"
|
||||
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
|
||||
run: |
|
||||
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:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
@@ -23,11 +23,28 @@ jobs:
|
||||
pip install --no-cache-dir ruff
|
||||
|
||||
- name: Format check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
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
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
@@ -63,3 +80,30 @@ jobs:
|
||||
. .venv/bin/activate
|
||||
cd cli
|
||||
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_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
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_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_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
||||
|
||||
set(CMAKE_INSTALL_MESSAGE NEVER)
|
||||
|
||||
add_compile_options(
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||
-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_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
|
||||
"${ZSHELL_CLI_SRC}/*.py"
|
||||
)
|
||||
@@ -109,11 +112,46 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
@ONLY
|
||||
)
|
||||
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()
|
||||
|
||||
if("plugin" IN_LIST ENABLE_MODULES)
|
||||
add_subdirectory(Plugins)
|
||||
|
||||
endif()
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
@@ -130,3 +168,22 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Greeter
|
||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||
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
|
||||
|
||||
interactive: !Visibilities.getForActive().isDrawing
|
||||
interactive: !Visibilities.getForActive()?.isDrawing
|
||||
maximumFlickVelocity: 3000
|
||||
|
||||
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
|
||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
|
||||
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
|
||||
hoverEnabled: true
|
||||
|
||||
Behavior on stateOpacity {
|
||||
|
||||
@@ -6,25 +6,55 @@ import qs.Effects
|
||||
CustomListView {
|
||||
id: root
|
||||
|
||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 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 {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts
|
||||
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;
|
||||
function contentSize(): real {
|
||||
return horizontal ? contentWidth : contentHeight;
|
||||
}
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
orientation: ListView.Vertical
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts.
|
||||
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 {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
@@ -40,10 +70,10 @@ CustomListView {
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
||||
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
||||
position: 0
|
||||
}
|
||||
|
||||
@@ -58,13 +88,13 @@ CustomListView {
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
||||
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on topFadeOpacity {
|
||||
Behavior on startFadeOpacity {
|
||||
Anim {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ Region {
|
||||
R {
|
||||
panel: root.panels.osdWrapper
|
||||
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
||||
x: root.win.width - width
|
||||
}
|
||||
|
||||
R {
|
||||
|
||||
+1
-9
@@ -26,7 +26,7 @@ CustomWindow {
|
||||
if (focusGrab.active)
|
||||
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 100;
|
||||
@@ -162,14 +162,6 @@ CustomWindow {
|
||||
Component.onCompleted: Visibilities.load(root.screen, this)
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggleLauncher(fix: string): void {
|
||||
visibilities.launcher = !visibilities.launcher;
|
||||
}
|
||||
|
||||
target: "visibilities"
|
||||
}
|
||||
|
||||
Binding {
|
||||
property: "bar"
|
||||
target: visibilities
|
||||
|
||||
+15
-42
@@ -11,8 +11,13 @@ Singleton {
|
||||
id: root
|
||||
|
||||
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> ddcServiceMon: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
@@ -56,8 +61,6 @@ Singleton {
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
ddcServiceMon = [];
|
||||
ddcServiceProc.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 {
|
||||
description: "Increase brightness"
|
||||
name: "brightnessUp"
|
||||
@@ -183,16 +166,12 @@ Singleton {
|
||||
id: monitor
|
||||
|
||||
property real brightness
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
||||
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
|
||||
readonly property string busNum: ddcInfo?.busNum ?? ""
|
||||
readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (monitor.isDdcService) {
|
||||
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) {
|
||||
if (monitor.isAppleDisplay) {
|
||||
const val = parseInt(text.trim());
|
||||
monitor.brightness = val / 101;
|
||||
} else {
|
||||
@@ -203,12 +182,11 @@ Singleton {
|
||||
}
|
||||
}
|
||||
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 isDdcService: Config.services.ddcutilService
|
||||
readonly property bool isDdc: ddcInfo !== null
|
||||
required property ShellScreen modelData
|
||||
property real queuedBrightness: NaN
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 500
|
||||
interval: 400
|
||||
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
@@ -219,9 +197,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
if (isDdcService)
|
||||
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
|
||||
else if (isAppleDisplay)
|
||||
if (isAppleDisplay)
|
||||
initProc.command = ["asdbctl", "get"];
|
||||
else if (isDdc)
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
@@ -237,28 +213,25 @@ Singleton {
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
if ((isDdc || isDdcService) && timer.running) {
|
||||
if (isDdc && timer.running) {
|
||||
queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
brightness = value;
|
||||
|
||||
if (isDdcService)
|
||||
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
|
||||
else if (isAppleDisplay)
|
||||
if (isAppleDisplay)
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||
else if (isDdc)
|
||||
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
||||
else
|
||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||
|
||||
if (isDdc || isDdcService)
|
||||
if (isDdc)
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
Component.onCompleted: initBrightness()
|
||||
onBusNumChanged: initBrightness()
|
||||
onDisplayNumChanged: initBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
+31
-3
@@ -1,12 +1,12 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQml
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQml
|
||||
import ZShell
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -15,7 +15,24 @@ Singleton {
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
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 {
|
||||
if (!player)
|
||||
return "";
|
||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||
return alias?.to ?? player.identity;
|
||||
}
|
||||
@@ -25,9 +42,12 @@ Singleton {
|
||||
if (!Config.utilities.toasts.nowPlaying) {
|
||||
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 {
|
||||
@@ -38,8 +58,10 @@ Singleton {
|
||||
reloadableId: "players"
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Toggle media playback"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaToggle"
|
||||
|
||||
onPressed: {
|
||||
@@ -49,8 +71,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Previous track"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaPrev"
|
||||
|
||||
onPressed: {
|
||||
@@ -60,8 +84,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Next track"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaNext"
|
||||
|
||||
onPressed: {
|
||||
@@ -71,8 +97,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Stop media playback"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaStop"
|
||||
|
||||
onPressed: root.active?.stop()
|
||||
|
||||
+107
-32
@@ -1,7 +1,7 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell
|
||||
import qs.Config
|
||||
|
||||
@@ -12,15 +12,15 @@ Singleton {
|
||||
property var cc
|
||||
property string city
|
||||
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> hourlyForecast
|
||||
readonly property int humidity: cc?.humidity ?? 0
|
||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||
property string loc
|
||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
||||
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
||||
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), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||
readonly property string temp: formatTemp(cc?.tempC)
|
||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||
|
||||
function fetchCityFromCoords(coords: string): void {
|
||||
@@ -29,29 +29,48 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lat, lon] = coords.split(",");
|
||||
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
||||
Requests.get(url, text => {
|
||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||
if (geo) {
|
||||
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
||||
city = geoCity;
|
||||
cachedCities.set(coords, geoCity);
|
||||
const [lat, lon] = coords.split(",").map(s => s.trim());
|
||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||
|
||||
const fallbackToBigDataCloud = () => {
|
||||
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
|
||||
Requests.get(fallbackUrl, text => {
|
||||
const geo = JSON.parse(text);
|
||||
const geoCity = geo.city || geo.locality;
|
||||
if (geoCity) {
|
||||
city = fixCityName(geoCity);
|
||||
cachedCities.set(coords, city);
|
||||
} else {
|
||||
city = "Unknown City";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
|
||||
Requests.get(nominatimUrl, text => {
|
||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||
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 {
|
||||
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 => {
|
||||
const json = JSON.parse(text);
|
||||
if (json.results && json.results.length > 0) {
|
||||
const result = json.results[0];
|
||||
loc = result.latitude + "," + result.longitude;
|
||||
city = result.name;
|
||||
city = fixCityName(result.name);
|
||||
} else {
|
||||
loc = "";
|
||||
reload();
|
||||
@@ -72,25 +91,21 @@ Singleton {
|
||||
cc = {
|
||||
weatherCode: json.current.weather_code,
|
||||
weatherDesc: getWeatherCondition(json.current.weather_code),
|
||||
tempC: Math.round(json.current.temperature_2m),
|
||||
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
|
||||
feelsLikeC: Math.round(json.current.apparent_temperature),
|
||||
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
|
||||
tempC: json.current.temperature_2m,
|
||||
feelsLikeC: json.current.apparent_temperature,
|
||||
humidity: json.current.relative_humidity_2m,
|
||||
windSpeed: json.current.wind_speed_10m,
|
||||
isDay: json.current.is_day,
|
||||
sunrise: json.daily.sunrise[0],
|
||||
sunset: json.daily.sunset[0]
|
||||
sunrise: json.daily.sunrise[0].replace("T", " "),
|
||||
sunset: json.daily.sunset[0].replace("T", " ")
|
||||
};
|
||||
|
||||
const forecastList = [];
|
||||
for (let i = 0; i < json.daily.time.length; i++)
|
||||
forecastList.push({
|
||||
date: json.daily.time[i],
|
||||
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
||||
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
||||
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
||||
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
|
||||
date: json.daily.time[i].replace(/-/g, "/"),
|
||||
maxTempC: json.daily.temperature_2m_max[i],
|
||||
minTempC: json.daily.temperature_2m_min[i],
|
||||
weatherCode: json.daily.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
||||
});
|
||||
@@ -99,7 +114,8 @@ Singleton {
|
||||
const hourlyList = [];
|
||||
const now = new Date();
|
||||
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)
|
||||
continue;
|
||||
|
||||
@@ -107,7 +123,7 @@ Singleton {
|
||||
timestamp: json.hourly.time[i],
|
||||
hour: time.getHours(),
|
||||
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],
|
||||
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 {
|
||||
const conditions = {
|
||||
"0": "Clear",
|
||||
@@ -154,9 +223,9 @@ Singleton {
|
||||
if (!loc || loc.indexOf(",") === -1)
|
||||
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 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("&");
|
||||
}
|
||||
@@ -189,7 +258,14 @@ Singleton {
|
||||
|
||||
onLocChanged: fetchWeatherData()
|
||||
|
||||
// Refresh current location hourly
|
||||
Connections {
|
||||
function onWeatherLocationChanged(): void {
|
||||
root.reload();
|
||||
}
|
||||
|
||||
target: Config.services
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 3600000 // 1 hour
|
||||
repeat: true
|
||||
@@ -200,6 +276,5 @@ Singleton {
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+14
-24
@@ -4,57 +4,46 @@ import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
RowLayout {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
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
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: weather.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
topLeftRadius: Appearance.rounding.large
|
||||
|
||||
WeatherInfo {
|
||||
id: weather
|
||||
|
||||
Layout.fillWidth: true
|
||||
rootHeight: root.height
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: resources.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Resources {
|
||||
id: resources
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomLeftRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
|
||||
Media {
|
||||
id: media
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Center {
|
||||
lock: root.lock
|
||||
@@ -77,4 +66,5 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-145
@@ -1,201 +1,110 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
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
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: mask
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
}
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.2)
|
||||
position: 0.4
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 0.8
|
||||
}
|
||||
color: DynamicColors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
CustomText {
|
||||
Layout.bottomMargin: Appearance.spacing.larger
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
text: qsTr("Now playing")
|
||||
}
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.extraLarge
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackArtist ?? qsTr("No media")
|
||||
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.pointSize: Appearance.font.size.small
|
||||
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.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.spacing.large * 1.2
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoPrevious)
|
||||
Players.active.previous();
|
||||
}
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoPrevious
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: Players.active?.previous()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canTogglePlaying)
|
||||
Players.active.togglePlaying();
|
||||
}
|
||||
|
||||
active: Players.active?.isPlaying ?? false
|
||||
animate: true
|
||||
icon: active ? "pause" : "play_arrow"
|
||||
level: active ? 2 : 1
|
||||
set_color: "Primary"
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoNext)
|
||||
Players.active.next();
|
||||
IconButton {
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
onClicked: Players.active?.togglePlaying()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoNext
|
||||
icon: "skip_next"
|
||||
}
|
||||
}
|
||||
}
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
component PlayerControl: CustomRect {
|
||||
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 {
|
||||
}
|
||||
onClicked: Players.active?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-38
@@ -1,81 +1,82 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Effects
|
||||
|
||||
GridLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
columnSpacing: Appearance.spacing.large
|
||||
columns: 2
|
||||
rowSpacing: Appearance.spacing.large
|
||||
rows: 1
|
||||
readonly property real fontScale: {
|
||||
const diff = width / 391 - 1; // 391 is the width at 1080 height screen
|
||||
return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
|
||||
}
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
service: Storage
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
id: cpu
|
||||
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
fgColor: DynamicColors.palette.m3tertiary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
component Resource: CustomRect {
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "hard_disk"
|
||||
value: Storage.percentage
|
||||
}
|
||||
}
|
||||
|
||||
component Resource: CircularProgress {
|
||||
id: res
|
||||
|
||||
required property color fgColor
|
||||
required property string icon
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
implicitSize: width
|
||||
|
||||
Behavior on value {
|
||||
Behavior on clampedVal {
|
||||
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 {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: res.fgColor
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
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.Layouts
|
||||
import qs.Modules.Lock.Weather
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
readonly property bool showForecast: rootHeight >= 700
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
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`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: {
|
||||
const base = brief.implicitHeight + brief.anchors.topMargin;
|
||||
if (showForecast)
|
||||
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
||||
return base + brief.anchors.topMargin;
|
||||
}
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
@@ -166,4 +27,27 @@ ColumnLayout {
|
||||
|
||||
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
|
||||
value: root.brightness
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
onMoved: {
|
||||
if (Config.osd.allMonBrightness) {
|
||||
for (const mon of Brightness.monitors) {
|
||||
mon.setBrightness(value);
|
||||
@@ -116,7 +115,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
required property bool shouldBeActive
|
||||
|
||||
@@ -28,9 +28,7 @@ Scope {
|
||||
visible: false
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
onShouldShowChanged: {
|
||||
function onShouldShowChanged(): void {
|
||||
if (root.shouldShow) {
|
||||
panelWindow.visible = true;
|
||||
openAnim.start();
|
||||
@@ -38,6 +36,8 @@ Scope {
|
||||
closeAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
Anim {
|
||||
|
||||
@@ -32,6 +32,31 @@ Item {
|
||||
implicitHeight: width
|
||||
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 {
|
||||
id: img
|
||||
|
||||
|
||||
@@ -148,16 +148,39 @@ VerticalFadeFlickable {
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
Column {
|
||||
id: resultList
|
||||
|
||||
Layout.fillWidth: true
|
||||
cacheBuffer: 10000
|
||||
implicitHeight: contentHeight
|
||||
interactive: false
|
||||
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
|
||||
|
||||
required property int index
|
||||
@@ -173,25 +196,51 @@ VerticalFadeFlickable {
|
||||
|
||||
MaterialIcon {
|
||||
color: DynamicColors.palette.m3primary
|
||||
fill: 1
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: group.modelData.icon
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
color: DynamicColors.palette.m3secondary
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: group.modelData.page
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Column {
|
||||
id: cardList
|
||||
|
||||
Layout.fillWidth: true
|
||||
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 {
|
||||
model: group.modelData.entries
|
||||
model: ScriptModel {
|
||||
objectProp: "anchor"
|
||||
values: group.modelData.entries
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: result
|
||||
@@ -201,7 +250,6 @@ VerticalFadeFlickable {
|
||||
readonly property bool isLast: index === group.modelData.entries.length - 1
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
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
|
||||
topRightRadius: layer.pressed ? Appearance.rounding.medium : isFirst ? Appearance.rounding.large : Appearance.rounding.extraSmall
|
||||
width: cardList.width
|
||||
|
||||
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
|
||||
active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
|
||||
first: true
|
||||
last: true
|
||||
menuItems: root.clockFormats
|
||||
settingAnchor: "bar-clock-format"
|
||||
subtext: qsTr("Change how time is displayed in the widget")
|
||||
@@ -58,5 +57,15 @@ PageBase {
|
||||
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
|
||||
|
||||
property bool completed
|
||||
property real cropHeight: displayData.height ?? 1.0
|
||||
property real cropWidth: displayData.width ?? 1.0
|
||||
property real cropX: displayData.x ?? 0.0
|
||||
property real cropY: displayData.y ?? 0.0
|
||||
property real cropHeight: displayData?.height ?? 1.0
|
||||
property real cropWidth: displayData?.width ?? 1.0
|
||||
property real cropX: displayData?.x ?? 0.0
|
||||
property real cropY: displayData?.y ?? 0.0
|
||||
property WallpaperImage current
|
||||
readonly property var displayData: Wallpapers.getCrop(screen.name)
|
||||
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
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from typer._completion_shared import install, _get_shell_name
|
||||
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)
|
||||
|
||||
@@ -23,9 +25,17 @@ def _completion_installed() -> bool:
|
||||
case "zsh":
|
||||
return (Path.home() / ".zfunc" / "_zshell-cli").exists()
|
||||
case "bash":
|
||||
return (Path.home() / ".bash_completions" / "zshell-cli.sh").exists()
|
||||
return (
|
||||
Path.home() / ".bash_completions" / "zshell-cli.sh"
|
||||
).exists()
|
||||
case "fish":
|
||||
return (Path.home() / ".config" / "fish" / "completions" / "zshell-cli.fish").exists()
|
||||
return (
|
||||
Path.home()
|
||||
/ ".config"
|
||||
/ "fish"
|
||||
/ "completions"
|
||||
/ "zshell-cli.fish"
|
||||
).exists()
|
||||
return False
|
||||
|
||||
|
||||
@@ -40,10 +50,15 @@ def _install_completion() -> None:
|
||||
try:
|
||||
_, path = install(prog_name="zshell-cli")
|
||||
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:
|
||||
print(f"zshell-cli: Failed to install shell completion: {e}", file=sys.stderr)
|
||||
raise typer.Exit(code=1)
|
||||
print(
|
||||
f"zshell-cli: Failed to install shell completion: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise typer.Exit(code=1) from None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -53,5 +68,8 @@ def main() -> None:
|
||||
if "_ZSHELL_CLI_COMPLETE" in os.environ:
|
||||
completion_init()
|
||||
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()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
@@ -18,7 +18,9 @@ TEMP_RECORDING = STATE_DIR / "recording.mp4"
|
||||
REPLAY_RECORDING = STATE_DIR / "replay.mp4"
|
||||
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]:
|
||||
@@ -32,34 +34,54 @@ def _read_extra_args() -> list[str]:
|
||||
|
||||
|
||||
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"]
|
||||
if actions:
|
||||
for action in actions:
|
||||
args.extend(["-A", action])
|
||||
try:
|
||||
proc = subprocess.run(args, capture_output=True, text=True)
|
||||
return int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
|
||||
return (
|
||||
int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
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]:
|
||||
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)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _focused_monitor_name() -> Optional[str]:
|
||||
def _focused_monitor_name() -> str | None:
|
||||
for m in _get_monitors():
|
||||
if m.get("focused"):
|
||||
return m["name"]
|
||||
@@ -71,7 +93,12 @@ def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
||||
intersecting = []
|
||||
for m in _get_monitors():
|
||||
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)
|
||||
return intersecting
|
||||
|
||||
@@ -80,23 +107,30 @@ def _highest_refresh(monitors: list[dict]) -> float:
|
||||
return max((m["refreshRate"] for m in monitors), default=60.0)
|
||||
|
||||
|
||||
def _slurp_region() -> Optional[str]:
|
||||
def _slurp_region() -> str | None:
|
||||
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:
|
||||
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
|
||||
|
||||
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
|
||||
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
|
||||
|
||||
|
||||
def start_recording(region: Optional[str], sound: bool):
|
||||
def start_recording(region: str | None, sound: bool):
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cmd = [RECORDER]
|
||||
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)
|
||||
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:
|
||||
monitor_name = _focused_monitor_name()
|
||||
@@ -137,7 +173,12 @@ def start_recording(region: Optional[str], sound: bool):
|
||||
cmd.extend(extra_args)
|
||||
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}")
|
||||
if notif_id is not None:
|
||||
@@ -145,12 +186,20 @@ def start_recording(region: Optional[str], sound: bool):
|
||||
|
||||
time.sleep(1)
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
if not _is_recording():
|
||||
@@ -166,10 +215,8 @@ def stop_recording(clipboard: bool):
|
||||
TEMP_RECORDING.rename(final_path)
|
||||
|
||||
if NOTIF_ID_FILE.is_file():
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
|
||||
except Exception:
|
||||
pass
|
||||
NOTIF_ID_FILE.unlink()
|
||||
|
||||
if clipboard:
|
||||
@@ -183,21 +230,34 @@ def stop_recording(clipboard: bool):
|
||||
|
||||
|
||||
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.")
|
||||
|
||||
|
||||
@app.command()
|
||||
def record(
|
||||
region: Optional[str] = typer.Option(
|
||||
region: str | None = typer.Option(
|
||||
None,
|
||||
"--region",
|
||||
"-r",
|
||||
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."),
|
||||
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."),
|
||||
sound: bool = typer.Option(
|
||||
False, "--sound", "-s", help="Record audio from default output."
|
||||
),
|
||||
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."""
|
||||
if pause:
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import typer
|
||||
import contextlib
|
||||
import json
|
||||
import shutil
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined
|
||||
from typing import Any, Optional, Tuple
|
||||
from zshell.utils.schemepalettes import get_palette, list_schemes, resolve_preset
|
||||
import sys
|
||||
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.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.math_utils import (
|
||||
difference_degrees,
|
||||
rotation_direction,
|
||||
sanitize_degrees_double,
|
||||
)
|
||||
from PIL import Image
|
||||
from zshell.utils.schemepalettes import (
|
||||
get_palette,
|
||||
list_schemes,
|
||||
resolve_preset,
|
||||
)
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
@@ -74,7 +81,8 @@ def _complete_accent(ctx, incomplete):
|
||||
@app.command()
|
||||
def list_presets(
|
||||
json_format: bool = typer.Option(
|
||||
False, "--json", help="Output in JSON format"),
|
||||
False, "--json", help="Output in JSON format"
|
||||
),
|
||||
):
|
||||
schemes = list_schemes()
|
||||
if json_format:
|
||||
@@ -107,25 +115,25 @@ def list_presets(
|
||||
|
||||
@app.command()
|
||||
def generate(
|
||||
image_path: Optional[Path] = typer.Option(
|
||||
image_path: Path | None = typer.Option(
|
||||
None, help="Path to source image. Required for image mode."
|
||||
),
|
||||
scheme: Optional[str] = typer.Option(
|
||||
scheme: str | None = typer.Option(
|
||||
None,
|
||||
help="Color scheme algorithm to use for image mode. Ignored in preset mode.",
|
||||
autocompletion=_complete_scheme_name,
|
||||
),
|
||||
preset: Optional[str] = typer.Option(
|
||||
preset: str | None = typer.Option(
|
||||
None,
|
||||
help="Name of a premade scheme in this format: <scheme>:<variant>",
|
||||
autocompletion=_complete_preset,
|
||||
),
|
||||
mode: Optional[str] = typer.Option(
|
||||
mode: str | None = typer.Option(
|
||||
None,
|
||||
help="Mode of the preset scheme (dark or light).",
|
||||
autocompletion=_complete_mode,
|
||||
),
|
||||
accent: Optional[str] = typer.Option(
|
||||
accent: str | None = typer.Option(
|
||||
None,
|
||||
help="Accent for schemes that support it (e.g. mauve).",
|
||||
autocompletion=_complete_accent,
|
||||
@@ -149,20 +157,27 @@ def generate(
|
||||
|
||||
if preset is not None and image_path is not None:
|
||||
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):
|
||||
match scheme_name:
|
||||
case "fruit-salad":
|
||||
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad
|
||||
from materialyoucolor.scheme.scheme_fruit_salad import (
|
||||
SchemeFruitSalad,
|
||||
)
|
||||
|
||||
return SchemeFruitSalad
|
||||
case "expressive":
|
||||
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
|
||||
from materialyoucolor.scheme.scheme_expressive import (
|
||||
SchemeExpressive,
|
||||
)
|
||||
|
||||
return SchemeExpressive
|
||||
case "monochrome":
|
||||
from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome
|
||||
from materialyoucolor.scheme.scheme_monochrome import (
|
||||
SchemeMonochrome,
|
||||
)
|
||||
|
||||
return SchemeMonochrome
|
||||
case "rainbow":
|
||||
@@ -170,7 +185,9 @@ def generate(
|
||||
|
||||
return SchemeRainbow
|
||||
case "tonal-spot":
|
||||
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
|
||||
from materialyoucolor.scheme.scheme_tonal_spot import (
|
||||
SchemeTonalSpot,
|
||||
)
|
||||
|
||||
return SchemeTonalSpot
|
||||
case "neutral":
|
||||
@@ -178,7 +195,9 @@ def generate(
|
||||
|
||||
return SchemeNeutral
|
||||
case "fidelity":
|
||||
from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity
|
||||
from materialyoucolor.scheme.scheme_fidelity import (
|
||||
SchemeFidelity,
|
||||
)
|
||||
|
||||
return SchemeFidelity
|
||||
case "content":
|
||||
@@ -190,7 +209,9 @@ def generate(
|
||||
|
||||
return SchemeVibrant
|
||||
case _:
|
||||
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad
|
||||
from materialyoucolor.scheme.scheme_fruit_salad import (
|
||||
SchemeFruitSalad,
|
||||
)
|
||||
|
||||
return SchemeFruitSalad
|
||||
|
||||
@@ -275,8 +296,8 @@ def generate(
|
||||
diff = difference_degrees(from_hct.hue, to_hct.hue)
|
||||
rotation = min(diff * 0.8, 100)
|
||||
output_hue = sanitize_degrees_double(
|
||||
from_hct.hue + rotation *
|
||||
rotation_direction(from_hct.hue, to_hct.hue)
|
||||
from_hct.hue
|
||||
+ rotation * rotation_direction(from_hct.hue, to_hct.hue)
|
||||
)
|
||||
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
|
||||
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)
|
||||
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)
|
||||
cache_path = thumbnail_cache_path(image_path, thumb_dir)
|
||||
|
||||
@@ -331,7 +354,9 @@ def generate(
|
||||
|
||||
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.write_text(sequences, encoding="utf-8")
|
||||
|
||||
@@ -375,7 +400,7 @@ def generate(
|
||||
mode = mode.lower()
|
||||
preference = "prefer-dark" if mode == "dark" else "prefer-light"
|
||||
|
||||
try:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
subprocess.run(
|
||||
[
|
||||
"gsettings",
|
||||
@@ -388,8 +413,6 @@ def generate(
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def apply_qt_mode(mode: str, home: str) -> None:
|
||||
mode = mode.lower()
|
||||
@@ -413,10 +436,8 @@ def generate(
|
||||
)
|
||||
|
||||
if count > 0 and new_text != text:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
qt_conf.write_text(new_text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def build_template_context(
|
||||
*,
|
||||
@@ -480,7 +501,7 @@ def generate(
|
||||
ESC = "\x1b"
|
||||
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()
|
||||
if not s.startswith("#") or s.startswith("#!"):
|
||||
return None
|
||||
@@ -492,7 +513,7 @@ def generate(
|
||||
expanded = os.path.expandvars(os.path.expanduser(target))
|
||||
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)
|
||||
if not lines:
|
||||
return None, ""
|
||||
@@ -520,7 +541,9 @@ def generate(
|
||||
|
||||
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)
|
||||
|
||||
if any(part.startswith(".") for part in rel.parts):
|
||||
@@ -538,14 +561,13 @@ def generate(
|
||||
text = template.render(**context)
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
shutil.copymode(tpl_path, out_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
rendered_outputs.append(out_path)
|
||||
|
||||
@@ -562,14 +584,16 @@ def generate(
|
||||
result = QuantizeCelebi(pixel_array, 128)
|
||||
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"
|
||||
|
||||
scheme = scheme_class(seed, is_dark, 0.0)
|
||||
|
||||
color_dict = {}
|
||||
for color in vars(MaterialDynamicColors).keys():
|
||||
for color in vars(MaterialDynamicColors):
|
||||
color_name = getattr(MaterialDynamicColors, color)
|
||||
if hasattr(color_name, "get_hct"):
|
||||
color_int = color_name.get_hct(scheme).to_int()
|
||||
@@ -578,7 +602,7 @@ def generate(
|
||||
return color_dict
|
||||
|
||||
def int_to_hex(argb_int):
|
||||
return "#{:06X}".format(argb_int & 0xFFFFFF)
|
||||
return f"#{argb_int & 0xFFFFFF:06X}"
|
||||
|
||||
try:
|
||||
with CONFIG.open() as f:
|
||||
@@ -601,8 +625,9 @@ def generate(
|
||||
(v.accents for v in meta.variants if v.id == p_variant), ()
|
||||
)
|
||||
if accent not in var_accents:
|
||||
available = ", ".join(
|
||||
var_accents) if var_accents else "none"
|
||||
available = (
|
||||
", ".join(var_accents) if var_accents else "none"
|
||||
)
|
||||
raise typer.BadParameter(
|
||||
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
|
||||
)
|
||||
@@ -612,9 +637,14 @@ def generate(
|
||||
if p_scheme in schemes:
|
||||
meta = schemes[p_scheme]
|
||||
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]
|
||||
|
||||
palette_obj = get_palette(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import subprocess
|
||||
|
||||
import typer
|
||||
|
||||
args = ["qs", "-c", "zshell"]
|
||||
@@ -8,9 +9,9 @@ app = typer.Typer()
|
||||
|
||||
@app.command()
|
||||
def start():
|
||||
subprocess.run(args + ["ipc"] + ["call"] + ["picker"] + ["open"], check=True)
|
||||
subprocess.run([*args, "ipc", "call", "picker", "open"], check=True)
|
||||
|
||||
|
||||
@app.command()
|
||||
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()
|
||||
def kill():
|
||||
result = subprocess.run(args + ["kill"], capture_output=True)
|
||||
result = subprocess.run([*args, "kill"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write("No running instance to kill.\n")
|
||||
sys.exit(1)
|
||||
@@ -19,10 +19,11 @@ def kill():
|
||||
|
||||
|
||||
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()
|
||||
if stdout:
|
||||
if "already running" in stdout.lower():
|
||||
if stdout and "already running" in stdout.lower():
|
||||
sys.stderr.write(stdout + "\n")
|
||||
sys.exit(1)
|
||||
if result.returncode != 0:
|
||||
@@ -38,10 +39,10 @@ def start(no_daemon: bool = False):
|
||||
|
||||
@app.command()
|
||||
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
|
||||
while time.monotonic() < deadline:
|
||||
result = subprocess.run(args + ["kill"], capture_output=True)
|
||||
result = subprocess.run([*args, "kill"], capture_output=True)
|
||||
if result.returncode == 255:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
@@ -50,7 +51,7 @@ def restart(no_daemon: bool = False):
|
||||
|
||||
@app.command()
|
||||
def show():
|
||||
result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True)
|
||||
result = subprocess.run([*args, "ipc", "show"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stderr.decode())
|
||||
sys.exit(1)
|
||||
@@ -60,7 +61,7 @@ def show():
|
||||
|
||||
@app.command()
|
||||
def log():
|
||||
result = subprocess.run(args + ["log"], capture_output=True)
|
||||
result = subprocess.run([*args, "log"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stderr.decode())
|
||||
sys.exit(1)
|
||||
@@ -70,7 +71,9 @@ def log():
|
||||
|
||||
@app.command()
|
||||
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:
|
||||
sys.stderr.write(result.stderr.decode())
|
||||
sys.exit(1)
|
||||
@@ -78,8 +81,13 @@ def lock():
|
||||
|
||||
|
||||
@app.command()
|
||||
def call(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)
|
||||
def call(
|
||||
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:
|
||||
sys.stderr.write(result.stderr.decode())
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import subprocess
|
||||
import typer
|
||||
|
||||
from typing import Annotated
|
||||
from PIL import Image, ImageFilter
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
args = ["qs", "-c", "zshell"]
|
||||
|
||||
@@ -12,7 +12,10 @@ app = typer.Typer()
|
||||
|
||||
@app.command()
|
||||
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()
|
||||
|
||||
@@ -102,21 +102,22 @@ def _discover_schemes() -> dict[str, SchemeMeta]:
|
||||
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:
|
||||
raise KeyError(
|
||||
f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}")
|
||||
f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}"
|
||||
)
|
||||
|
||||
meta = SCHEMES[scheme]
|
||||
var_ids = {v.id for v in meta.variants}
|
||||
if variant not in var_ids:
|
||||
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"
|
||||
else:
|
||||
filename = f"{mode}.txt"
|
||||
filename = f"{accent}-{mode}.txt" if accent else f"{mode}.txt"
|
||||
|
||||
txt_path = ASSETS / scheme / variant / filename
|
||||
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)
|
||||
|
||||
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]:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from zshell.utils import schemepalettes as sp
|
||||
|
||||
|
||||
@@ -12,8 +13,12 @@ def tmp_schemes(tmp_path: Path) -> Path:
|
||||
|
||||
gmedium = schemes / "gruvbox" / "medium"
|
||||
gmedium.mkdir(parents=True)
|
||||
(gmedium / "dark.txt").write_text("background 101415\nonBackground e0e3e4\nprimary 81d3e0\nsurface 1c2021\n")
|
||||
(gmedium / "light.txt").write_text("background fbf1c7\nonBackground 3c3836\nprimary 6b5f10\nsurface fbf1c7\n")
|
||||
(gmedium / "dark.txt").write_text(
|
||||
"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.mkdir(parents=True)
|
||||
@@ -21,14 +26,24 @@ def tmp_schemes(tmp_path: Path) -> Path:
|
||||
|
||||
cmocha = schemes / "catppuccin" / "mocha"
|
||||
cmocha.mkdir(parents=True)
|
||||
(cmocha / "dark.txt").write_text("background 1e1e2e\nprimary cba6f7\nsecondary 756294\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")
|
||||
(cmocha / "dark.txt").write_text(
|
||||
"background 1e1e2e\nprimary cba6f7\nsecondary 756294\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.mkdir(parents=True)
|
||||
(clatte / "light.txt").write_text("background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n")
|
||||
(clatte / "mauve-light.txt").write_text("background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n")
|
||||
(clatte / "light.txt").write_text(
|
||||
"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.mkdir(parents=True)
|
||||
@@ -81,13 +96,17 @@ class TestDiscoverSchemes:
|
||||
|
||||
def test_variant_has_modes(self):
|
||||
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 "light" in gmedium.modes
|
||||
|
||||
def test_catppuccin_has_accents(self):
|
||||
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 "green" in mocha.accents
|
||||
assert "rosewater" in mocha.accents
|
||||
@@ -95,7 +114,9 @@ class TestDiscoverSchemes:
|
||||
|
||||
def test_non_accent_scheme_has_no_accents(self):
|
||||
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 == ()
|
||||
|
||||
|
||||
@@ -124,11 +145,15 @@ class TestGetPalette:
|
||||
sp.get_palette("nope", "medium", "dark")
|
||||
|
||||
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")
|
||||
|
||||
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.colors["primary"] is not None
|
||||
|
||||
@@ -164,4 +189,7 @@ class TestResolvePreset:
|
||||
assert sp.resolve_preset("default") == ("default", "default")
|
||||
|
||||
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 subprocess import CompletedProcess
|
||||
from unittest.mock import patch, call
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from typer.testing import CliRunner
|
||||
from zshell.subcommands.shell import app
|
||||
@@ -21,11 +21,15 @@ class TestKill:
|
||||
def test_kill_runs_qs_kill_success(self, mock_run):
|
||||
mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n")
|
||||
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")
|
||||
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"])
|
||||
assert result.exit_code != 0
|
||||
assert "No running instance to kill" in result.output
|
||||
@@ -34,19 +38,32 @@ class TestKill:
|
||||
class TestStart:
|
||||
@patch("zshell.subcommands.shell.subprocess.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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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"])
|
||||
assert result.exit_code != 0
|
||||
assert "already running" in result.output
|
||||
@@ -62,10 +79,14 @@ class TestStart:
|
||||
class TestShow:
|
||||
@patch("zshell.subcommands.shell.subprocess.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")
|
||||
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:
|
||||
@@ -73,7 +94,9 @@ class TestLog:
|
||||
def test_log_runs_qs_log(self, mock_run):
|
||||
mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"")
|
||||
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:
|
||||
@@ -81,7 +104,10 @@ class TestLock:
|
||||
def test_lock_runs_ipc_call_lock(self, mock_run):
|
||||
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
||||
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:
|
||||
@@ -89,14 +115,27 @@ class TestCall:
|
||||
def test_call_no_args(self, mock_run):
|
||||
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
||||
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")
|
||||
def test_call_with_args(self, mock_run):
|
||||
mock_run.return_value = CompletedProcess([], 0, b"", b"")
|
||||
invoke("call", "target", "method", "arg1", "arg2")
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -106,7 +145,9 @@ class TestRestart:
|
||||
@patch("zshell.subcommands.shell.subprocess.run")
|
||||
def test_restart_kills_then_starts(self, mock_run, mock_start):
|
||||
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
|
||||
]
|
||||
invoke("restart")
|
||||
|
||||
@@ -21,17 +21,39 @@ source = "vcs"
|
||||
|
||||
[tool.hatch.build]
|
||||
include = [
|
||||
"src/zshell/assets/**",
|
||||
"cli/src/zshell/assets/**",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["cli/src/zshell"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
only-include = [
|
||||
"src",
|
||||
"cli/src",
|
||||
]
|
||||
|
||||
[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]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
testpaths = ["cli/tests"]
|
||||
pythonpath = ["cli/src"]
|
||||
@@ -4,22 +4,24 @@ import json
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
@cache
|
||||
def read_lines(path: Path) -> tuple[str, ...]:
|
||||
return tuple(path.read_text().splitlines())
|
||||
|
||||
|
||||
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\("([^"]+)"\)')
|
||||
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(
|
||||
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*"([^"]+)"')
|
||||
SKIP_LABELS = {"Muted", "None"}
|
||||
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()
|
||||
|
||||
start = next(
|
||||
i for i, line in enumerate(text)
|
||||
if re.search(r'\bpages\s*:\s*\[', line)
|
||||
i for i, line in enumerate(text) if re.search(r"\bpages\s*:\s*\[", line)
|
||||
)
|
||||
|
||||
out: list[tuple[str, str]] = []
|
||||
@@ -92,14 +93,16 @@ def parse_page_registry(settings: Path) -> list[tuple[str, str]]:
|
||||
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:
|
||||
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()
|
||||
m = BLOCK_RE.match(line)
|
||||
if not m:
|
||||
@@ -152,8 +155,9 @@ def parse_page_comps(settings: Path) -> list[list[str]]:
|
||||
text = (settings / "PageCompRegistry.qml").read_text().splitlines()
|
||||
|
||||
start = next(
|
||||
i for i, line in enumerate(text)
|
||||
if re.search(r'\bpageComps\s*:\s*\[', _strip_comment(line))
|
||||
i
|
||||
for i, line in enumerate(text)
|
||||
if re.search(r"\bpageComps\s*:\s*\[", _strip_comment(line))
|
||||
)
|
||||
|
||||
comps: list[list[str]] = []
|
||||
@@ -180,10 +184,12 @@ def parse_page_comps(settings: Path) -> list[list[str]]:
|
||||
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_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:
|
||||
continue
|
||||
out_labels.append(lbl)
|
||||
@@ -227,7 +233,10 @@ def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
|
||||
if mo:
|
||||
pos = int(mo.group(1))
|
||||
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
|
||||
|
||||
nav: dict[str, dict] = {}
|
||||
@@ -236,8 +245,12 @@ def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
|
||||
continue
|
||||
main = names[0]
|
||||
main_icon, main_label = top_meta.get(top_idx, ("tune", main))
|
||||
nav[main] = {"pageIdx": top_idx, "subPath": [],
|
||||
"crumbIcons": [main_icon], "crumbLabels": [main_label]}
|
||||
nav[main] = {
|
||||
"pageIdx": top_idx,
|
||||
"subPath": [],
|
||||
"crumbIcons": [main_icon],
|
||||
"crumbLabels": [main_label],
|
||||
}
|
||||
children = dict(nav_children.get(main, {}))
|
||||
opened_via_subpage = set()
|
||||
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]
|
||||
icons = [main_icon] + ([icon] if section else []) + [icon]
|
||||
labels, icons = dedup_crumbs(labels, icons)
|
||||
nav[child] = {"pageIdx": top_idx, "subPath": [pos],
|
||||
nav[child] = {
|
||||
"pageIdx": top_idx,
|
||||
"subPath": [pos],
|
||||
"crumbIcons": icons,
|
||||
"crumbLabels": labels}
|
||||
for gpos, (gicon, glabel, gsection) in nav_children.get(child, {}).items():
|
||||
"crumbLabels": labels,
|
||||
}
|
||||
for gpos, (gicon, glabel, gsection) in nav_children.get(
|
||||
child, {}
|
||||
).items():
|
||||
if gpos >= len(names):
|
||||
continue
|
||||
glabels = labels + ([gsection] if gsection else []) + [glabel]
|
||||
gicons = icons + ([gicon] if gsection else []) + [gicon]
|
||||
glabels, gicons = dedup_crumbs(glabels, gicons)
|
||||
nav[names[gpos]] = {
|
||||
"pageIdx": top_idx, "subPath": [pos, gpos],
|
||||
"pageIdx": top_idx,
|
||||
"subPath": [pos, gpos],
|
||||
"crumbIcons": gicons,
|
||||
"crumbLabels": glabels}
|
||||
"crumbLabels": glabels,
|
||||
}
|
||||
return nav
|
||||
|
||||
|
||||
@@ -290,10 +310,12 @@ def tokenize(text: str) -> list[str]:
|
||||
|
||||
|
||||
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] = []
|
||||
for comp, meta in nav.items():
|
||||
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)
|
||||
toggle_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 ""
|
||||
)
|
||||
if label and label not in SKIP_LABELS and anchor:
|
||||
extra = " ".join(meta["crumbLabels"]) + \
|
||||
" " + section + " " + (subtext or "")
|
||||
entries.append({
|
||||
"pageIdx": meta["pageIdx"], "subPath": meta["subPath"],
|
||||
extra = (
|
||||
" ".join(meta["crumbLabels"])
|
||||
+ " "
|
||||
+ section
|
||||
+ " "
|
||||
+ (subtext or "")
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
"pageIdx": meta["pageIdx"],
|
||||
"subPath": meta["subPath"],
|
||||
"crumbIcons": meta["crumbIcons"],
|
||||
"crumbLabels": meta["crumbLabels"],
|
||||
"title": label, "anchor": anchor,
|
||||
"title": label,
|
||||
"anchor": anchor,
|
||||
"section": section,
|
||||
"subtext": subtext or "",
|
||||
"togglePath": toggle_path,
|
||||
"keywords": " ".join(sorted(set(tokenize(label + " " + extra)))),
|
||||
})
|
||||
"keywords": " ".join(
|
||||
sorted(set(tokenize(label + " " + extra)))
|
||||
),
|
||||
}
|
||||
)
|
||||
i += 1
|
||||
return entries
|
||||
|
||||
@@ -372,7 +407,9 @@ def build_inverted_and_ranking(entries: list[dict]):
|
||||
seen.add(tok)
|
||||
for tok, ids in inverted.items():
|
||||
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:
|
||||
@@ -387,14 +424,22 @@ def main() -> int:
|
||||
inverted, ranking = build_inverted_and_ranking(entries)
|
||||
for e in entries:
|
||||
e.pop("keywords", None)
|
||||
out.write_text(json.dumps({
|
||||
out.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 2,
|
||||
"entries": entries,
|
||||
"inverted": inverted,
|
||||
"ranking": ranking,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
print(f"settings index: {len(entries)} entries, "
|
||||
f"{len(inverted)} tokens -> {out}")
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"settings index: {len(entries)} entries, "
|
||||
f"{len(inverted)} tokens -> {out}"
|
||||
)
|
||||
print("files:", len(files))
|
||||
print("comps:", len(parse_page_comps(settings)))
|
||||
print("registry:", len(parse_page_registry(settings)))
|
||||
|
||||
Reference in New Issue
Block a user