Merge branch 'main' into 135-issue-pr-templates
C++ / fmt (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 20s
Python / fmt (pull_request) Successful in 31s
Python / lint (pull_request) Successful in 29s
Python / test (pull_request) Successful in 1m0s
C++ / build (pull_request) Successful in 3m25s
Rust / fmt (pull_request) Successful in 1m12s
Python / buildcheck (pull_request) Successful in 2m51s
Rust / build (pull_request) Successful in 2m14s
Rust / clippy (pull_request) Successful in 1m52s
C++ / clang-tidy (pull_request) Successful in 6m7s
C++ / fmt (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 20s
Python / fmt (pull_request) Successful in 31s
Python / lint (pull_request) Successful in 29s
Python / test (pull_request) Successful in 1m0s
C++ / build (pull_request) Successful in 3m25s
Rust / fmt (pull_request) Successful in 1m12s
Python / buildcheck (pull_request) Successful in 2m51s
Rust / build (pull_request) Successful in 2m14s
Rust / clippy (pull_request) Successful in 1m52s
C++ / clang-tidy (pull_request) Successful in 6m7s
This commit is contained in:
@@ -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
|
||||
@@ -51,7 +51,9 @@ add_compile_options(
|
||||
-Wunreachable-code
|
||||
)
|
||||
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Build settings index
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
|
||||
execute_process(
|
||||
@@ -64,6 +66,86 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to build settings search index")
|
||||
endif()
|
||||
|
||||
# Nuitka compilation
|
||||
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
||||
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)
|
||||
|
||||
if(NUITKA_EXECUTABLE)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_SRC}/*.py"
|
||||
)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_SRC}/assets/*"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}"
|
||||
|
||||
COMMAND
|
||||
${NUITKA_EXECUTABLE}
|
||||
--standalone
|
||||
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
||||
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
||||
--output-filename=zshell-cli
|
||||
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
||||
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
|
||||
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||
)
|
||||
|
||||
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli")
|
||||
|
||||
install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli")
|
||||
install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE)
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in"
|
||||
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake"
|
||||
@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)
|
||||
|
||||
@@ -6,7 +6,7 @@ ListView {
|
||||
|
||||
property bool doneFakeFlick
|
||||
|
||||
interactive: !Visibilities.getForActive().isDrawing
|
||||
interactive: !Visibilities.getForActive()?.isDrawing
|
||||
maximumFlickVelocity: 3000
|
||||
|
||||
rebound: Transition {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -196,7 +196,7 @@ Item {
|
||||
if (!root.visibilities.bar && Config.bar.autoHide && y < root.bar.implicitHeight)
|
||||
root.bar.isHovered = true;
|
||||
|
||||
if (root.panels.sidebar.width === 0) {
|
||||
if (root.panels.sidebar.offsetScale === 1) {
|
||||
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
|
||||
|
||||
if (showOsd) {
|
||||
@@ -204,7 +204,7 @@ Item {
|
||||
root.panels.osd.hovered = true;
|
||||
}
|
||||
} else {
|
||||
const outOfSidebar = x < root.width - root.panels.sidebar.width;
|
||||
const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale);
|
||||
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
|
||||
|
||||
if (!root.osdShortcutActive) {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -184,18 +184,14 @@ Item {
|
||||
Item {
|
||||
id: settingsWrapper
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
implicitHeight: settings.implicitHeight
|
||||
implicitWidth: settings.implicitWidth
|
||||
x: (root.width - settings.implicitWidth) / 2
|
||||
y: (settings.implicitHeight + (root.height - root.bar.implicitHeight - settings.implicitHeight) / 2) * (1 - settings.offsetScale) - settings.implicitHeight - 5
|
||||
|
||||
Settings.Wrapper {
|
||||
id: settings
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
// anchors.centerIn: parent
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
|
||||
panels: root
|
||||
screen: root.screen
|
||||
visibilities: root.visibilities
|
||||
|
||||
+1
-2
@@ -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 {
|
||||
@@ -60,7 +59,7 @@ Region {
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.settingsWrapper
|
||||
panel: root.panels.settings
|
||||
}
|
||||
|
||||
R {
|
||||
|
||||
+3
-17
@@ -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
|
||||
@@ -306,15 +298,9 @@ CustomWindow {
|
||||
PanelBg {
|
||||
id: settingsBg
|
||||
|
||||
property real extraHeight: 0
|
||||
|
||||
deformAmount: 0.03
|
||||
implicitHeight: panels.settings.height * (1 + extraHeight)
|
||||
implicitWidth: panels.settings.width
|
||||
panel: panels.settingsWrapper
|
||||
panel: panels.settings
|
||||
radius: Appearance.rounding.large + Appearance.padding.normal
|
||||
x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness
|
||||
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
@@ -422,7 +408,7 @@ CustomWindow {
|
||||
resources.transform: Matrix4x4 {
|
||||
matrix: resourcesBg.deformMatrix
|
||||
}
|
||||
settingsWrapper.transform: Matrix4x4 {
|
||||
settings.transform: Matrix4x4 {
|
||||
matrix: settingsBg.deformMatrix
|
||||
}
|
||||
sidebar.transform: Matrix4x4 {
|
||||
|
||||
+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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,36 +149,71 @@ WlSessionLockSurface {
|
||||
Image {
|
||||
id: background
|
||||
|
||||
anchors.bottomMargin: -8 - lockContent.positions[lockContent.positionIndex].y
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -8 + lockContent.positions[lockContent.positionIndex].x
|
||||
anchors.rightMargin: -8 - lockContent.positions[lockContent.positionIndex].x
|
||||
anchors.topMargin: -8 + lockContent.positions[lockContent.positionIndex].y
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: WallpaperPath.lockscreenBg
|
||||
|
||||
Behavior on anchors.bottomMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.rightMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.topMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: lockContent
|
||||
|
||||
property int positionIndex: 0
|
||||
readonly property var positions: [Qt.point(0, 0), Qt.point(4, 0), Qt.point(4, 4), Qt.point(0, 4), Qt.point(-4, 4), Qt.point(-4, 0), Qt.point(-4, -4), Qt.point(0, -4), Qt.point(4, -4),]
|
||||
readonly property int radius: size / 4 * Appearance.rounding.scale
|
||||
readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: positions[positionIndex].x
|
||||
anchors.verticalCenterOffset: positions[positionIndex].y
|
||||
implicitHeight: size
|
||||
implicitWidth: size
|
||||
scale: 0
|
||||
|
||||
// MultiEffect {
|
||||
// anchors.fill: lockBg
|
||||
// autoPaddingEnabled: false
|
||||
// blur: 1
|
||||
// blurEnabled: true
|
||||
// blurMax: 64
|
||||
// maskEnabled: true
|
||||
// maskSource: lockBg
|
||||
//
|
||||
// source: ShaderEffectSource {
|
||||
// sourceItem: background
|
||||
// sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height)
|
||||
// }
|
||||
// }
|
||||
Behavior on anchors.horizontalCenterOffset {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 120000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: {
|
||||
lockContent.positionIndex = (lockContent.positionIndex + 1) % lockContent.positions.length;
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: lockBg
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -267,8 +267,6 @@ Item {
|
||||
function restoreFromData() {
|
||||
let data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||
|
||||
console.log(data.x, data.y);
|
||||
|
||||
if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) {
|
||||
zoom = data.zoom > 0 ? data.zoom : 1.0;
|
||||
x = imageX + (data.x * scaledImg.paintedWidth);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -249,7 +298,7 @@ VerticalFadeFlickable {
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: SettingsSearcher.highlight(result.modelData.title, root.search, DynamicColors.palette.m3primary)
|
||||
textFormat: Text.StyledText
|
||||
textFormat: text.includes("<font") ? Text.StyledText : Text.PlainText
|
||||
}
|
||||
|
||||
CustomText {
|
||||
@@ -258,7 +307,7 @@ VerticalFadeFlickable {
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: SettingsSearcher.highlight(result.modelData.subtext, root.search, DynamicColors.palette.m3primary)
|
||||
textFormat: Text.StyledText
|
||||
textFormat: text.includes("<font") ? Text.StyledText : Text.PlainText
|
||||
visible: result.modelData.subtext.length > 0
|
||||
}
|
||||
}
|
||||
@@ -292,9 +341,6 @@ VerticalFadeFlickable {
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
objectProp: "pageIdx"
|
||||
values: root.groups
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,39 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property var fzfFinder: null
|
||||
readonly property var highlightCache: ({
|
||||
"search": "",
|
||||
"pattern": null
|
||||
})
|
||||
property var inverted: ({})
|
||||
property var ranking: ({})
|
||||
|
||||
function highlight(text: string, search: string, colour: color): string {
|
||||
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const tokens = tokenize(search);
|
||||
if (tokens.length === 0)
|
||||
if (search.length === 0)
|
||||
return escaped;
|
||||
|
||||
const cache = root.highlightCache;
|
||||
if (search !== cache.search) {
|
||||
const tokens = tokenize(search);
|
||||
cache.search = search;
|
||||
if (tokens.length === 0)
|
||||
cache.pattern = null;
|
||||
else {
|
||||
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||
const pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
|
||||
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
|
||||
}
|
||||
}
|
||||
|
||||
const pattern = cache.pattern;
|
||||
if (!pattern)
|
||||
return escaped;
|
||||
|
||||
pattern.lastIndex = 0;
|
||||
if (!pattern.test(escaped))
|
||||
return escaped;
|
||||
|
||||
pattern.lastIndex = 0;
|
||||
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ Item {
|
||||
sState.animatingContainer: content.opacity < 1
|
||||
sState.currentPageIdx: ["wallpaper"][0]
|
||||
sState.screen: root.screen
|
||||
|
||||
onClose: console.log("shouldclose")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,12 @@
|
||||
set(ZSHELL_CLI_BIN_DIR "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/usr/bin")
|
||||
file(MAKE_DIRECTORY "${ZSHELL_CLI_BIN_DIR}")
|
||||
|
||||
file(RELATIVE_PATH ZSHELL_CLI_TARGET
|
||||
"${ZSHELL_CLI_BIN_DIR}"
|
||||
"${CMAKE_INSTALL_PREFIX}/@INSTALL_LIBDIR@/zshell-cli/zshell-cli"
|
||||
)
|
||||
|
||||
file(CREATE_LINK
|
||||
"${ZSHELL_CLI_TARGET}"
|
||||
"${ZSHELL_CLI_BIN_DIR}/zshell-cli" SYMBOLIC
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname $0)/../src" || exit
|
||||
|
||||
python3 -m zshell "$@"
|
||||
@@ -1,32 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["hatchling >= 1.26"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "zshell"
|
||||
requires-python = ">=3.13"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"typer",
|
||||
"pillow",
|
||||
"jinja2",
|
||||
"materialyoucolor"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
zshell-cli = "zshell:main"
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
only-include = [
|
||||
"src",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
@@ -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,4 +1,4 @@
|
||||
from . import main
|
||||
from zshell import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -73,7 +80,9 @@ def _complete_accent(ctx, incomplete):
|
||||
|
||||
@app.command()
|
||||
def list_presets(
|
||||
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
||||
json_format: bool = typer.Option(
|
||||
False, "--json", help="Output in JSON format"
|
||||
),
|
||||
):
|
||||
schemes = list_schemes()
|
||||
if json_format:
|
||||
@@ -106,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,
|
||||
@@ -139,7 +148,7 @@ def generate(
|
||||
HOME = str(os.getenv("HOME"))
|
||||
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
||||
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
||||
THUMB_PATH = Path(HOME + "/.cache/zshell/imagecache/thumbnail.jpg")
|
||||
THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails")
|
||||
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
||||
|
||||
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
||||
@@ -147,20 +156,28 @@ def generate(
|
||||
CONFIG = Path(HOME + "/.config/zshell/config.json")
|
||||
|
||||
if preset is not None and image_path is not None:
|
||||
raise typer.BadParameter("Use either --image-path or --preset, not both.")
|
||||
raise typer.BadParameter(
|
||||
"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":
|
||||
@@ -168,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":
|
||||
@@ -176,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":
|
||||
@@ -188,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
|
||||
|
||||
@@ -273,7 +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)
|
||||
@@ -307,17 +331,32 @@ def generate(
|
||||
|
||||
return out
|
||||
|
||||
def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)):
|
||||
thumbnail_file = Path(thumbnail_path)
|
||||
def thumbnail_cache_path(image_path: Path, thumb_dir: Path) -> Path:
|
||||
stat = image_path.stat()
|
||||
key = f"{image_path.stem}_{stat.st_size}_{int(stat.st_mtime)}"
|
||||
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:
|
||||
thumb_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_path = thumbnail_cache_path(image_path, thumb_dir)
|
||||
|
||||
if cache_path.exists():
|
||||
return cache_path
|
||||
|
||||
image = Image.open(image_path)
|
||||
image.draft("RGB", size)
|
||||
image = image.convert("RGB")
|
||||
image.thumbnail(size, Image.Resampling.NEAREST)
|
||||
image.save(cache_path, "JPEG")
|
||||
|
||||
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(thumbnail_path, "JPEG")
|
||||
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")
|
||||
|
||||
@@ -361,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",
|
||||
@@ -374,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()
|
||||
@@ -399,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(
|
||||
*,
|
||||
@@ -466,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
|
||||
@@ -478,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, ""
|
||||
@@ -506,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):
|
||||
@@ -523,14 +560,14 @@ def generate(
|
||||
template = env.from_string(body)
|
||||
text = template.render(**context)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Template render failed for '{rel}': {e}") from e
|
||||
raise RuntimeError(
|
||||
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)
|
||||
|
||||
@@ -547,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()
|
||||
@@ -563,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:
|
||||
@@ -586,7 +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}"
|
||||
)
|
||||
@@ -596,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(
|
||||
@@ -623,13 +669,13 @@ def generate(
|
||||
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
||||
else:
|
||||
image_path = image_path or Path(WALL_PATH)
|
||||
generate_thumbnail(image_path, str(THUMB_PATH))
|
||||
seed = seed_from_image(THUMB_PATH)
|
||||
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
|
||||
seed = seed_from_image(thumb_path)
|
||||
name = "dynamic"
|
||||
flavor = "default"
|
||||
|
||||
if smart:
|
||||
effective_mode = smart_mode(THUMB_PATH)
|
||||
effective_mode = smart_mode(thumb_path)
|
||||
elif mode is not None:
|
||||
effective_mode = mode
|
||||
else:
|
||||
@@ -675,7 +721,9 @@ def generate(
|
||||
print(f"rendered: {p}")
|
||||
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT, "w") as f:
|
||||
tmp_output = OUTPUT.with_suffix(".json.tmp")
|
||||
with open(tmp_output, "w") as f:
|
||||
json.dump(output_dict, f, indent=4)
|
||||
os.replace(tmp_output, OUTPUT)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
ASSETS = Path(__file__).resolve().parent.parent / "assets" / "schemes"
|
||||
ASSETS: Traversable = files("zshell") / "assets" / "schemes"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -30,7 +32,7 @@ class Palette:
|
||||
accent: str | None = None
|
||||
|
||||
|
||||
def _parse_txt(path: Path) -> dict[str, str]:
|
||||
def _parse_txt(path: Traversable) -> dict[str, str]:
|
||||
colors: dict[str, str] = {}
|
||||
for line in path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
@@ -46,7 +48,7 @@ def _parse_txt(path: Path) -> dict[str, str]:
|
||||
def _discover_schemes() -> dict[str, SchemeMeta]:
|
||||
schemes: dict[str, SchemeMeta] = {}
|
||||
|
||||
for scheme_dir in sorted(ASSETS.iterdir()):
|
||||
for scheme_dir in sorted(ASSETS.iterdir(), key=lambda p: p.name):
|
||||
if not scheme_dir.is_dir() or scheme_dir.name.startswith("."):
|
||||
continue
|
||||
|
||||
@@ -54,7 +56,7 @@ def _discover_schemes() -> dict[str, SchemeMeta]:
|
||||
display_name = sid.capitalize()
|
||||
|
||||
variants: list[SchemeVariant] = []
|
||||
for var_dir in sorted(scheme_dir.iterdir()):
|
||||
for var_dir in sorted(scheme_dir.iterdir(), key=lambda p: p.name):
|
||||
if not var_dir.is_dir() or var_dir.name.startswith("."):
|
||||
continue
|
||||
|
||||
@@ -62,9 +64,10 @@ def _discover_schemes() -> dict[str, SchemeMeta]:
|
||||
accents: set[str] = set()
|
||||
|
||||
for f in var_dir.iterdir():
|
||||
if f.suffix != ".txt":
|
||||
name = PurePosixPath(f.name)
|
||||
if name.suffix != ".txt":
|
||||
continue
|
||||
stem = f.stem
|
||||
stem = name.stem
|
||||
if "-" in stem:
|
||||
maybe_accent, maybe_mode = stem.rsplit("-", 1)
|
||||
if maybe_mode in ("dark", "light"):
|
||||
@@ -99,25 +102,28 @@ 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)}")
|
||||
raise KeyError(
|
||||
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))}")
|
||||
raise KeyError(
|
||||
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.exists():
|
||||
if not txt_path.is_file():
|
||||
txt_path = ASSETS / scheme / variant / f"{mode}.txt"
|
||||
|
||||
if not txt_path.exists():
|
||||
if not txt_path.is_file():
|
||||
var_info = next(v for v in meta.variants if v.id == variant)
|
||||
raise FileNotFoundError(
|
||||
f"No {mode} palette for '{scheme}:{variant}'. Available modes: {sorted(var_info.modes)}"
|
||||
@@ -125,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")
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
[build-system]
|
||||
requires = ["hatchling >= 1.26"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "zshell"
|
||||
requires-python = ">=3.13"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"typer",
|
||||
"pillow",
|
||||
"jinja2",
|
||||
"materialyoucolor"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
zshell-cli = "zshell:main"
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
[tool.hatch.build]
|
||||
include = [
|
||||
"cli/src/zshell/assets/**",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["cli/src/zshell"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
only-include = [
|
||||
"cli/src",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
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 = ["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