mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
183 Commits
e6d289d48c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80025804ab | ||
|
|
028d3b4e61 | ||
|
|
9cce5ccfe6 | ||
|
|
a260b8060e | ||
|
|
f945307232 | ||
|
|
8f44d52cb2 | ||
|
|
3413cb7b89 | ||
|
|
4e3b24ffbb | ||
|
|
03cfa55e0b | ||
|
|
a887e60f40 | ||
|
|
816819bf9f | ||
|
|
78f3bb3812 | ||
|
|
01d7ed5dd8 | ||
|
|
50311db280 | ||
|
|
01b1a276c5 | ||
|
|
6d4c31492c | ||
|
|
f8c5f07e9f | ||
|
|
11e23feb0e | ||
|
|
b4ba2dac37 | ||
|
|
d013c3b718 | ||
|
|
b3ea28c5c4 | ||
|
|
775b381987 | ||
|
|
3a41f2f1ed | ||
|
|
972fc534a4 | ||
|
|
808ee66e11 | ||
|
|
3936a516f8 | ||
|
|
15dc91f779 | ||
|
|
dd3d2908a2 | ||
|
|
0857023dba | ||
|
|
1edc8f468e | ||
|
|
2681fe87bb | ||
|
|
3f0d0f4d95 | ||
|
|
f24ecf1b99 | ||
|
|
acdd1d2ec4 | ||
|
|
d08496f237 | ||
|
|
27b4e0221b | ||
|
|
496ace0cd4 | ||
|
|
f61ed8b8a6 | ||
|
|
41ee88a3cf | ||
|
|
6bf1438ef1 | ||
|
|
b819306ab6 | ||
|
|
b140afca8e | ||
|
|
6735989455 | ||
|
|
db37ac24c7 | ||
|
|
0231270f9e | ||
|
|
b5194aa9e1 | ||
|
|
ea0ffaacb0 | ||
|
|
3b1f084a13 | ||
|
|
39a9e3a89f | ||
|
|
7a7af775c2 | ||
|
|
6ac2a305f7 | ||
|
|
3507c6cec3 | ||
|
|
3ff00768ac | ||
|
|
556d253ea8 | ||
|
|
3922070488 | ||
|
|
eebb4827c4 | ||
|
|
fd2c6a0784 | ||
|
|
417bf37515 | ||
|
|
132e799265 | ||
|
|
bdc864781b | ||
|
|
a343bc7562 | ||
|
|
1f2e231386 | ||
|
|
0e7f628c4a | ||
|
|
553f5257b3 | ||
|
|
80ce6aa19c | ||
|
|
2b2977de4a | ||
|
|
1d5d876e16 | ||
|
|
3c39162016 | ||
|
|
d38767fb5a | ||
|
|
f2be6cfeb1 | ||
|
|
65486ed3cf | ||
|
|
cc30e2a9e4 | ||
|
|
ac68451cdf | ||
|
|
0f6ae11c3d | ||
|
|
7cb39f00ad | ||
|
|
f313d03348 | ||
|
|
1adbf3937b | ||
|
|
a685d9da52 | ||
|
|
13dededcc9 | ||
|
|
3bed2d9feb | ||
|
|
7241877995 | ||
|
|
340d79000c | ||
|
|
162ec909da | ||
|
|
53f5240d41 | ||
|
|
27f0df07af | ||
|
|
ad940b5884 | ||
|
|
ec8ab47462 | ||
|
|
35cbfeb008 | ||
|
|
7036362b9b | ||
|
|
2bcb33e85c | ||
|
|
76ac036f85 | ||
|
|
581073394a | ||
|
|
d7b7086b21 | ||
|
|
59be179821 | ||
|
|
1cf2f6b946 | ||
|
|
a57a9c2121 | ||
|
|
67568c3746 | ||
|
|
afce792b80 | ||
|
|
f5c7493dbb | ||
|
|
f9b9d98638 | ||
|
|
2a97e03fa6 | ||
|
|
d6dacc2975 | ||
|
|
aab4b6765d | ||
|
|
3539aca1f7 | ||
|
|
81fbe9eaba | ||
|
|
f9dc6de485 | ||
|
|
012022d370 | ||
|
|
993216e157 | ||
|
|
c992f2b582 | ||
|
|
3243adebca | ||
|
|
baccef57d4 | ||
|
|
a823095372 | ||
|
|
172a743de4 | ||
|
|
623eec3689 | ||
|
|
53a033fe35 | ||
|
|
c490ee24f4 | ||
|
|
cc1e49294e | ||
|
|
e6fa46ae26 | ||
|
|
35fe774a1b | ||
|
|
1e6a0f9423 | ||
|
|
cc1877aadb | ||
|
|
f1eb1fa9ba | ||
|
|
bdd01e335d | ||
|
|
4b7baf82cd | ||
|
|
15c88ce1d2 | ||
|
|
8891c388d0 | ||
|
|
cd9d92d884 | ||
|
|
1b69a5e62b | ||
|
|
61d311b157 | ||
|
|
6b76b86930 | ||
|
|
dcfb947c36 | ||
|
|
59893b7f44 | ||
|
|
d2c62f5533 | ||
|
|
2bbe9a0c45 | ||
|
|
4e2ce82c0a | ||
|
|
104762186f | ||
|
|
f1233ab1e3 | ||
|
|
d6b407ec37 | ||
|
|
022b4b4bb3 | ||
|
|
49b322582d | ||
|
|
1280bd047d | ||
|
|
6f206d7523 | ||
|
|
2e58283859 | ||
|
|
99a5721fe8 | ||
|
|
5302ebd840 | ||
|
|
fa427ea1ac | ||
|
|
7027bd1646 | ||
|
|
3c38e17472 | ||
|
|
510ea5d2e4 | ||
|
|
bb2234d328 | ||
|
|
edbdeb0fb8 | ||
|
|
19541fc573 | ||
|
|
7c936cacfb | ||
|
|
c60cd3a341 | ||
|
|
e37135f80d | ||
|
|
aac937cbcc | ||
|
|
4b46d022af | ||
|
|
7f0181b310 | ||
|
|
6a109274f8 | ||
|
|
0f09cc693a | ||
|
|
af0166a553 | ||
|
|
a283017f26 | ||
|
|
5ae2cd1dfb | ||
|
|
eece811fb0 | ||
|
|
1ff1f3a7f2 | ||
|
|
a21a846bf5 | ||
|
|
f5f21e738a | ||
|
|
033e62418a | ||
|
|
3c69e8b1cc | ||
|
|
118be27796 | ||
|
|
721d35d417 | ||
|
|
7bc3d5910d | ||
|
|
ccc7047be0 | ||
|
|
a5e107c89d | ||
|
|
646d60dcbf | ||
|
|
5dc7c0d797 | ||
|
|
db1de9df38 | ||
|
|
3dd21382ba | ||
|
|
ec2b3d0d4b | ||
|
|
a205df1bd6 | ||
|
|
e822fa73da | ||
|
|
634e75b80c | ||
|
|
ec5b507efc |
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,12 +42,12 @@ body:
|
|||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: textarea
|
||||||
id: dms_version
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms version
|
label: dms doctor -v
|
||||||
description: Output of dms version command
|
description: Output of `dms doctor -v` command
|
||||||
placeholder: e.g., 1.2.3
|
placeholder: Paste the output of `dms doctor -v` here
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/support_request.yml
vendored
10
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -27,12 +27,12 @@ body:
|
|||||||
placeholder: Your Linux distribution
|
placeholder: Your Linux distribution
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: textarea
|
||||||
id: dms_version
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms version
|
label: dms doctor -v
|
||||||
description: Output of dms version command
|
description: Output of `dms doctor -v` command
|
||||||
placeholder: e.g., 1.2.3
|
placeholder: Paste the output of `dms doctor -v` here
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,13 +4,14 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to update (dms, dms-git, or all)"
|
description: "Package to update"
|
||||||
required: false
|
required: true
|
||||||
default: "all"
|
type: choice
|
||||||
tag_version:
|
options:
|
||||||
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
- dms
|
||||||
required: false
|
- dms-git
|
||||||
default: ""
|
- all
|
||||||
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||||
required: false
|
required: false
|
||||||
@@ -56,8 +57,9 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to check dms stable tag
|
# Helper function to check dms stable tag
|
||||||
|
# Sets LATEST_TAG variable in parent scope if update needed
|
||||||
check_dms_stable() {
|
check_dms_stable() {
|
||||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||||
|
|
||||||
@@ -73,8 +75,8 @@ jobs:
|
|||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
# Tag push - always update stable package
|
# Tag selected or pushed - always update stable package
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
@@ -104,7 +106,12 @@ jobs:
|
|||||||
# Check each package and build list of those needing updates
|
# Check each package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
if check_dms_stable; then
|
||||||
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -129,6 +136,9 @@ jobs:
|
|||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
@@ -161,12 +171,19 @@ jobs:
|
|||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||||
# Tag push event - use the pushed tag
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by tag: $VERSION"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
|
# Check if check-updates already determined a version (from auto-detection)
|
||||||
|
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
|
# Use version from check-updates job
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
# Scheduled run - dms-git only
|
# Scheduled run - dms-git only
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
@@ -176,22 +193,28 @@ jobs:
|
|||||||
|
|
||||||
# Determine version for dms stable
|
# Determine version for dms stable
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||||
# For explicit dms selection, require tag_version
|
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
VERSION="${{ github.event.inputs.tag_version }}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using specified tag: $VERSION"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
else
|
else
|
||||||
echo "ERROR: tag_version is required when package=dms"
|
# Auto-detect latest release for dms
|
||||||
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
exit 1
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Auto-detected latest release: $LATEST_TAG"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not auto-detect latest release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
# For "all", auto-detect if tag_version not specified
|
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
VERSION="${{ github.event.inputs.tag_version }}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using specified tag: $VERSION"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
else
|
else
|
||||||
# Auto-detect latest release for "all"
|
# Auto-detect latest release for "all"
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
@@ -206,7 +229,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||||
else
|
else
|
||||||
@@ -215,6 +238,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
- name: Update dms-git spec version
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -109,3 +109,4 @@ bin/
|
|||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
quickshell/dms-plugins
|
quickshell/dms-plugins
|
||||||
|
__pycache__
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
|
# 1.4.0
|
||||||
|
|
||||||
|
- Overhauled system monitor, graphs, styling
|
||||||
|
- dbus API for plugins, KDEConnect
|
||||||
|
- new dank16 algorithm
|
||||||
|
- launcher actions, customize env, args, name, icon
|
||||||
|
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||||
|
- dock on bar
|
||||||
|
|
||||||
# 1.2.0
|
# 1.2.0
|
||||||
|
|
||||||
- Added clipboard and clipboard history integration
|
- Added clipboard and clipboard history integration
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -43,7 +43,6 @@ install-shell:
|
|||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||||
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
|
||||||
@echo "Shell files installed"
|
@echo "Shell files installed"
|
||||||
|
|
||||||
install-completions:
|
install-completions:
|
||||||
|
|||||||
300
core/cmd/dms/commands_chroma.go
Normal file
300
core/cmd/dms/commands_chroma.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
ghtml "github.com/yuin/goldmark/renderer/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
chromaLanguage string
|
||||||
|
chromaStyle string
|
||||||
|
chromaInline bool
|
||||||
|
chromaMarkdown bool
|
||||||
|
chromaLineNumbers bool
|
||||||
|
|
||||||
|
// Caching layer for performance
|
||||||
|
lexerCache = make(map[string]chroma.Lexer)
|
||||||
|
styleCache = make(map[string]*chroma.Style)
|
||||||
|
formatterCache = make(map[string]*html.Formatter)
|
||||||
|
cacheMutex sync.RWMutex
|
||||||
|
maxFileSize = int64(5 * 1024 * 1024) // 5MB default
|
||||||
|
)
|
||||||
|
|
||||||
|
var chromaCmd = &cobra.Command{
|
||||||
|
Use: "chroma [file]",
|
||||||
|
Short: "Syntax highlight source code",
|
||||||
|
Long: `Generate syntax-highlighted HTML from source code.
|
||||||
|
|
||||||
|
Reads from file or stdin, outputs HTML with syntax highlighting.
|
||||||
|
Language is auto-detected from filename or can be specified with --language.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms chroma main.go
|
||||||
|
dms chroma --language python script.py
|
||||||
|
echo "def foo(): pass" | dms chroma -l python
|
||||||
|
cat code.rs | dms chroma -l rust --style dracula
|
||||||
|
dms chroma --markdown README.md
|
||||||
|
dms chroma --markdown --style github-dark notes.md
|
||||||
|
dms chroma list-languages
|
||||||
|
dms chroma list-styles`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runChroma,
|
||||||
|
}
|
||||||
|
|
||||||
|
var chromaListLanguagesCmd = &cobra.Command{
|
||||||
|
Use: "list-languages",
|
||||||
|
Short: "List all supported languages",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for _, name := range lexers.Names(true) {
|
||||||
|
fmt.Println(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var chromaListStylesCmd = &cobra.Command{
|
||||||
|
Use: "list-styles",
|
||||||
|
Short: "List all available color styles",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for _, name := range styles.Names() {
|
||||||
|
fmt.Println(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
|
||||||
|
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
|
||||||
|
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
|
||||||
|
chromaCmd.Flags().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output")
|
||||||
|
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
|
||||||
|
chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)")
|
||||||
|
|
||||||
|
chromaCmd.AddCommand(chromaListLanguagesCmd)
|
||||||
|
chromaCmd.AddCommand(chromaListStylesCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer {
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if lexer, ok := lexerCache[key]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
lexer := fallbackFunc()
|
||||||
|
if lexer != nil {
|
||||||
|
cacheMutex.Lock()
|
||||||
|
lexerCache[key] = lexer
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
}
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedStyle(name string) *chroma.Style {
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if style, ok := styleCache[name]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
style := styles.Get(name)
|
||||||
|
if style == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
|
||||||
|
style = styles.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheMutex.Lock()
|
||||||
|
styleCache[name] = style
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter {
|
||||||
|
key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers)
|
||||||
|
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if formatter, ok := formatterCache[key]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
var opts []html.Option
|
||||||
|
if inline {
|
||||||
|
opts = append(opts, html.WithClasses(false))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, html.WithClasses(true))
|
||||||
|
}
|
||||||
|
opts = append(opts, html.TabWidth(4))
|
||||||
|
|
||||||
|
if lineNumbers {
|
||||||
|
opts = append(opts, html.WithLineNumbers(true))
|
||||||
|
opts = append(opts, html.LineNumbersInTable(false))
|
||||||
|
opts = append(opts, html.WithLinkableLineNumbers(false, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := html.New(opts...)
|
||||||
|
|
||||||
|
cacheMutex.Lock()
|
||||||
|
formatterCache[key] = formatter
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func runChroma(cmd *cobra.Command, args []string) {
|
||||||
|
var source string
|
||||||
|
var filename string
|
||||||
|
|
||||||
|
// Read from file or stdin
|
||||||
|
if len(args) > 0 {
|
||||||
|
filename = args[0]
|
||||||
|
|
||||||
|
// Check file size before reading
|
||||||
|
fileInfo, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.Size() > maxFileSize {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n",
|
||||||
|
fileInfo.Size(), maxFileSize)
|
||||||
|
fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
source = string(content)
|
||||||
|
} else {
|
||||||
|
stat, _ := os.Stdin.Stat()
|
||||||
|
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||||
|
_ = cmd.Help()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
source = string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty input
|
||||||
|
if strings.TrimSpace(source) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Markdown rendering
|
||||||
|
if chromaMarkdown {
|
||||||
|
md := goldmark.New(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.GFM,
|
||||||
|
highlighting.NewHighlighting(
|
||||||
|
highlighting.WithStyle(chromaStyle),
|
||||||
|
highlighting.WithFormatOptions(
|
||||||
|
html.WithClasses(!chromaInline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithAutoHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
ghtml.WithHardWraps(),
|
||||||
|
ghtml.WithXHTML(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Print(buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect or use specified lexer
|
||||||
|
var lexer chroma.Lexer
|
||||||
|
if chromaLanguage != "" {
|
||||||
|
lexer = getCachedLexer(chromaLanguage, func() chroma.Lexer {
|
||||||
|
l := lexers.Get(chromaLanguage)
|
||||||
|
if l == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
})
|
||||||
|
} else if filename != "" {
|
||||||
|
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
|
||||||
|
return lexers.Match(filename)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try content analysis if no lexer found (limit to first 1KB for performance)
|
||||||
|
if lexer == nil {
|
||||||
|
analyzeContent := source
|
||||||
|
if len(source) > 1024 {
|
||||||
|
analyzeContent = source[:1024]
|
||||||
|
}
|
||||||
|
lexer = lexers.Analyse(analyzeContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to plaintext
|
||||||
|
if lexer == nil {
|
||||||
|
lexer = lexers.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
lexer = chroma.Coalesce(lexer)
|
||||||
|
|
||||||
|
// Get cached style
|
||||||
|
style := getCachedStyle(chromaStyle)
|
||||||
|
|
||||||
|
// Get cached formatter
|
||||||
|
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
|
||||||
|
|
||||||
|
// Tokenize
|
||||||
|
iterator, err := lexer.Tokenise(nil, source)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format and output
|
||||||
|
if chromaLineNumbers {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := formatter.Format(&buf, style, iterator); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Add spacing between line numbers
|
||||||
|
output := buf.String()
|
||||||
|
output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>")
|
||||||
|
fmt.Print(output)
|
||||||
|
} else {
|
||||||
|
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
|
||||||
PreRunE: findConfig,
|
PreRunE: findConfig,
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
@@ -77,6 +76,13 @@ var ipcCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
|
_ = findConfig(cmd, args)
|
||||||
|
printIPCHelp()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var debugSrvCmd = &cobra.Command{
|
var debugSrvCmd = &cobra.Command{
|
||||||
Use: "debug-srv",
|
Use: "debug-srv",
|
||||||
Short: "Start the debug server",
|
Short: "Start the debug server",
|
||||||
@@ -511,8 +517,12 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
colorCmd,
|
colorCmd,
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
|
notifyCmd,
|
||||||
|
genericNotifyActionCmd,
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
clipboardCmd,
|
||||||
|
chromaCmd,
|
||||||
doctorCmd,
|
doctorCmd,
|
||||||
|
configCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
318
core/cmd/dms/commands_config.go
Normal file
318
core/cmd/dms/commands_config.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Configuration utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolveIncludeCmd = &cobra.Command{
|
||||||
|
Use: "resolve-include <compositor> <filename>",
|
||||||
|
Short: "Check if a file is included in compositor config",
|
||||||
|
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
case 1:
|
||||||
|
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runResolveInclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configCmd.AddCommand(resolveIncludeCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncludeResult struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Included bool `json:"included"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
filename := args[1]
|
||||||
|
|
||||||
|
var result IncludeResult
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case "hyprland":
|
||||||
|
result, err = checkHyprlandInclude(filename)
|
||||||
|
case "niri":
|
||||||
|
result, err = checkNiriInclude(filename)
|
||||||
|
case "mangowc", "dwl", "mango":
|
||||||
|
result, err = checkMangoWCInclude(filename)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error checking include: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hyprlandFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.kdl")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "include") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
startQuote := strings.Index(trimmed, "\"")
|
||||||
|
if startQuote == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endQuote := strings.LastIndex(trimmed, "\"")
|
||||||
|
if endQuote <= startQuote {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := trimmed[startQuote+1 : endQuote]
|
||||||
|
if matchesTarget(includePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := includePath
|
||||||
|
if !filepath.IsAbs(includePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, includePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if niriFindInclude(fullPath, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
mainConfig = filepath.Join(configDir, "mango.conf")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mangowcFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesTarget(path, target string) bool {
|
||||||
|
path = strings.TrimPrefix(path, "./")
|
||||||
|
target = strings.TrimPrefix(target, "./")
|
||||||
|
return path == target || strings.HasSuffix(path, "/"+target)
|
||||||
|
}
|
||||||
@@ -87,6 +87,8 @@ var (
|
|||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
|
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
|
||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -448,11 +450,13 @@ func checkWindowManagers() []checkResult {
|
|||||||
versionRegex *regexp.Regexp
|
versionRegex *regexp.Regexp
|
||||||
commands []string
|
commands []string
|
||||||
}{
|
}{
|
||||||
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
{"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
||||||
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
||||||
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
||||||
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -477,7 +481,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, c.name, statusOK,
|
catCompositor, c.name, statusOK,
|
||||||
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
|
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
|
||||||
doctorDocsURL + "#compositor",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +490,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, or Wayfire",
|
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||||
doctorDocsURL + "#compositor",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,8 +502,8 @@ func checkWindowManagers() []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).Output()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil && len(output) == 0 {
|
||||||
return "installed"
|
return "installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,19 +638,14 @@ func checkI2CAvailability() checkResult {
|
|||||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectNetworkBackend() string {
|
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||||
result, err := network.DetectNetworkStack()
|
switch stackResult.Backend {
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result.Backend {
|
|
||||||
case network.BackendNetworkManager:
|
case network.BackendNetworkManager:
|
||||||
return "NetworkManager"
|
return "NetworkManager"
|
||||||
case network.BackendIwd:
|
case network.BackendIwd:
|
||||||
return "iwd"
|
return "iwd"
|
||||||
case network.BackendNetworkd:
|
case network.BackendNetworkd:
|
||||||
if result.HasIwd {
|
if stackResult.HasIwd {
|
||||||
return "iwd + systemd-networkd"
|
return "iwd + systemd-networkd"
|
||||||
}
|
}
|
||||||
return "systemd-networkd"
|
return "systemd-networkd"
|
||||||
@@ -657,75 +656,73 @@ func detectNetworkBackend() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOptionalDBusStatus(busName string) (status, string) {
|
||||||
|
if utils.IsDBusServiceAvailable(busName) {
|
||||||
|
return statusOK, "Available"
|
||||||
|
} else {
|
||||||
|
return statusWarn, "Not available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkOptionalDependencies() []checkResult {
|
func checkOptionalDependencies() []checkResult {
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
|
|
||||||
if utils.IsServiceActive("accounts-daemon", false) {
|
optionalFeaturesURL := doctorDocsURL + "#optional-features"
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.IsServiceActive("power-profiles-daemon", false) {
|
accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts")
|
||||||
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL})
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"})
|
ppdStatus, ppdMsg := getOptionalDBusStatus("org.freedesktop.UPower.PowerProfiles")
|
||||||
}
|
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", ppdStatus, ppdMsg, "Power profile management", optionalFeaturesURL})
|
||||||
|
|
||||||
|
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
|
||||||
|
|
||||||
results = append(results, checkI2CAvailability())
|
results = append(results, checkI2CAvailability())
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
networkStatus, networkMessage, networkDetails := statusOK, "Not available", "Network management"
|
||||||
|
|
||||||
|
if err == nil && networkResult.Backend != network.BackendNone {
|
||||||
|
networkMessage = detectNetworkBackend(networkResult)
|
||||||
|
if doctorVerbose {
|
||||||
|
networkDetails = networkResult.ChosenReason
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
networkStatus = statusInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL})
|
||||||
|
|
||||||
deps := []struct {
|
deps := []struct {
|
||||||
name, cmd, altCmd, desc string
|
name, cmd, desc string
|
||||||
important bool
|
important bool
|
||||||
}{
|
}{
|
||||||
{"matugen", "matugen", "", "Dynamic theming", true},
|
{"matugen", "matugen", "Dynamic theming", true},
|
||||||
{"dgop", "dgop", "", "System monitoring", true},
|
{"dgop", "dgop", "System monitoring", true},
|
||||||
{"cava", "cava", "", "Audio visualizer", true},
|
{"cava", "cava", "Audio visualizer", true},
|
||||||
{"khal", "khal", "", "Calendar events", false},
|
{"khal", "khal", "Calendar events", false},
|
||||||
{"Network", "nmcli", "iwctl", "Network management", false},
|
{"danksearch", "dsearch", "File search", false},
|
||||||
{"danksearch", "dsearch", "", "File search", false},
|
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
||||||
{"loginctl", "loginctl", "", "Session management", false},
|
|
||||||
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range deps {
|
for _, d := range deps {
|
||||||
found, foundCmd := utils.CommandExists(d.cmd), d.cmd
|
found := utils.CommandExists(d.cmd)
|
||||||
if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
|
|
||||||
found, foundCmd = true, d.altCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case found:
|
case found:
|
||||||
message := "Installed"
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL})
|
||||||
details := d.desc
|
|
||||||
if d.name == "Network" {
|
|
||||||
result, err := network.DetectNetworkStack()
|
|
||||||
if err == nil && result.Backend != network.BackendNone {
|
|
||||||
message = detectNetworkBackend() + " (active)"
|
|
||||||
if doctorVerbose {
|
|
||||||
details = result.ChosenReason
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch foundCmd {
|
|
||||||
case "nmcli":
|
|
||||||
message = "NetworkManager (installed)"
|
|
||||||
case "iwctl":
|
|
||||||
message = "iwd (installed)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
|
|
||||||
case d.important:
|
case d.important:
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL})
|
||||||
default:
|
default:
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"})
|
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,6 +890,10 @@ func printResultLine(r checkResult, styles tui.Styles) {
|
|||||||
if doctorVerbose && r.details != "" {
|
if doctorVerbose && r.details != "" {
|
||||||
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
|
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r.status == statusError || r.status == statusWarn) && r.url != "" {
|
||||||
|
fmt.Printf(" %s\n", styles.Subtle.Render("→ "+r.url))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSummary(results []checkResult, qsMissingFeatures bool) {
|
func printSummary(results []checkResult, qsMissingFeatures bool) {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func init() {
|
|||||||
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||||
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
||||||
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||||
|
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
|
||||||
|
|
||||||
keybindsCmd.AddCommand(keybindsListCmd)
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
@@ -211,6 +212,9 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
|
|||||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
options["repeat"] = false
|
options["repeat"] = false
|
||||||
}
|
}
|
||||||
|
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||||
|
options["flags"] = v
|
||||||
|
}
|
||||||
|
|
||||||
desc, _ := cmd.Flags().GetString("desc")
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
if err := writable.SetBind(key, action, desc, options); err != nil {
|
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||||
|
|||||||
68
core/cmd/dms/commands_notify.go
Normal file
68
core/cmd/dms/commands_notify.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
notifyAppName string
|
||||||
|
notifyIcon string
|
||||||
|
notifyFile string
|
||||||
|
notifyTimeout int
|
||||||
|
)
|
||||||
|
|
||||||
|
var notifyCmd = &cobra.Command{
|
||||||
|
Use: "notify <summary> [body]",
|
||||||
|
Short: "Send a desktop notification",
|
||||||
|
Long: `Send a desktop notification with optional actions.
|
||||||
|
|
||||||
|
If --file is provided, the notification will have "Open" and "Open Folder" actions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms notify "Hello" "World"
|
||||||
|
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone
|
||||||
|
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: runNotify,
|
||||||
|
}
|
||||||
|
|
||||||
|
var genericNotifyActionCmd = &cobra.Command{
|
||||||
|
Use: "notify-action-generic",
|
||||||
|
Hidden: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
notify.RunActionListener(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
notifyCmd.Flags().StringVar(¬ifyAppName, "app", "DMS", "Application name")
|
||||||
|
notifyCmd.Flags().StringVar(¬ifyIcon, "icon", "", "Icon name or path")
|
||||||
|
notifyCmd.Flags().StringVar(¬ifyFile, "file", "", "File path (enables Open/Open Folder actions)")
|
||||||
|
notifyCmd.Flags().IntVar(¬ifyTimeout, "timeout", 5000, "Timeout in milliseconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNotify(cmd *cobra.Command, args []string) {
|
||||||
|
summary := args[0]
|
||||||
|
body := ""
|
||||||
|
if len(args) > 1 {
|
||||||
|
body = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
n := notify.Notification{
|
||||||
|
AppName: notifyAppName,
|
||||||
|
Icon: notifyIcon,
|
||||||
|
Summary: summary,
|
||||||
|
Body: body,
|
||||||
|
FilePath: notifyFile,
|
||||||
|
Timeout: int32(notifyTimeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := notify.Send(n); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,11 +18,9 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "dms",
|
Use: "dms",
|
||||||
Short: "dms CLI",
|
Short: "dms CLI",
|
||||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||||
Run: runInteractiveMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add the -c flag
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +34,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
if statErr == nil && !info.IsDir() {
|
if statErr == nil && !info.IsDir() {
|
||||||
configPath = customConfigPath
|
configPath = customConfigPath
|
||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil // <-- Guard statement
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
@@ -76,18 +72,3 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
|
||||||
detector, _ := dms.NewDetector()
|
|
||||||
|
|
||||||
if !detector.IsDMSInstalled() {
|
|
||||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
|
||||||
log.Info("Please install DMS using dankinstall before using this management interface.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := dms.NewModel(Version)
|
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
||||||
if _, err := p.Run(); err != nil {
|
|
||||||
log.Fatalf("Error running program: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -618,9 +618,8 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
log.Error("IPC command requires arguments")
|
printIPCHelp()
|
||||||
log.Info("Usage: dms ipc <command> [args...]")
|
return
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if args[0] != "call" {
|
if args[0] != "call" {
|
||||||
@@ -642,3 +641,45 @@ func runShellIPCCommand(args []string) {
|
|||||||
log.Fatalf("Error running IPC command: %v", err)
|
log.Fatalf("Error running IPC command: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printIPCHelp() {
|
||||||
|
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := parseTargetsFromIPCShowOutput(string(output))
|
||||||
|
if len(targets) == 0 {
|
||||||
|
fmt.Println("No IPC targets available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Targets:")
|
||||||
|
|
||||||
|
targetNames := make([]string, 0, len(targets))
|
||||||
|
for name := range targets {
|
||||||
|
targetNames = append(targetNames, name)
|
||||||
|
}
|
||||||
|
slices.Sort(targetNames)
|
||||||
|
|
||||||
|
for _, targetName := range targetNames {
|
||||||
|
funcs := targets[targetName]
|
||||||
|
funcNames := make([]string, 0, len(funcs))
|
||||||
|
for fn := range funcs {
|
||||||
|
funcNames = append(funcNames, fn)
|
||||||
|
}
|
||||||
|
slices.Sort(funcNames)
|
||||||
|
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
24
core/go.mod
24
core/go.mod
@@ -4,6 +4,7 @@ go 1.24.6
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
|
github.com/alecthomas/chroma/v2 v2.17.2
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
@@ -15,22 +16,25 @@ require (
|
|||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
|
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/yuin/goldmark v1.7.16
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||||
golang.org/x/image v0.34.0
|
golang.org/x/image v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.6.2 // indirect
|
github.com/clipperhouse/displaywidth v0.7.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.2 // indirect
|
github.com/cloudflare/circl v1.6.2 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||||
@@ -38,8 +42,8 @@ require (
|
|||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
golang.org/x/crypto v0.46.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -47,12 +51,12 @@ require (
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19
|
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -66,7 +70,7 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.39.0
|
golang.org/x/sys v0.40.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.33.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
97
core/go.sum
97
core/go.sum
@@ -4,6 +4,14 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
|
|||||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
@@ -16,8 +24,6 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
|
|||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
|
||||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
@@ -26,24 +32,18 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
|||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||||
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
|
||||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
|
||||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -52,8 +52,10 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
@@ -64,22 +66,15 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
|
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
|
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
|
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
|
||||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
@@ -87,6 +82,8 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
|
|||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -127,16 +124,12 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
@@ -146,45 +139,43 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
|
||||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
|
||||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -215,8 +215,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
// Skip if file already exists to preserve user modifications
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
if _, err := os.Stat(path); err == nil {
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -543,7 +543,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cd.deployHyprlandDmsConfigs(dmsDir); err != nil {
|
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -553,20 +553,22 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string) error {
|
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||||
configs := []struct {
|
configs := []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
}{
|
}{
|
||||||
{"colors.conf", HyprColorsConfig},
|
{"colors.conf", HyprColorsConfig},
|
||||||
{"layout.conf", HyprLayoutConfig},
|
{"layout.conf", HyprLayoutConfig},
|
||||||
|
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.conf", ""},
|
{"outputs.conf", ""},
|
||||||
{"cursor.conf", ""},
|
{"cursor.conf", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
if _, err := os.Stat(path); err == nil {
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
content, err := os.ReadFile(result.Path)
|
content, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||||
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "exec-once = ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -444,7 +444,7 @@ general {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -461,9 +461,7 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
|
|||||||
159
core/internal/config/embedded/hypr-binds.conf
Normal file
159
core/internal/config/embedded/hypr-binds.conf
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# === Application Launchers ===
|
||||||
|
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
|
||||||
|
bind = SUPER, space, exec, dms ipc call spotlight toggle
|
||||||
|
bind = SUPER, V, exec, dms ipc call clipboard toggle
|
||||||
|
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
|
||||||
|
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
|
||||||
|
bind = SUPER, N, exec, dms ipc call notifications toggle
|
||||||
|
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
|
||||||
|
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
|
||||||
|
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
|
||||||
|
bind = SUPER, X, exec, dms ipc call powermenu toggle
|
||||||
|
|
||||||
|
# === Cheat sheet
|
||||||
|
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
||||||
|
|
||||||
|
# === Security ===
|
||||||
|
bind = SUPER ALT, L, exec, dms ipc call lock lock
|
||||||
|
bind = SUPER SHIFT, E, exit
|
||||||
|
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
||||||
|
|
||||||
|
# === Audio Controls ===
|
||||||
|
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||||
|
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
||||||
|
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
||||||
|
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
||||||
|
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
||||||
|
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
||||||
|
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
||||||
|
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
||||||
|
|
||||||
|
# === Brightness Controls ===
|
||||||
|
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||||
|
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||||
|
|
||||||
|
# === Window Management ===
|
||||||
|
bind = SUPER, Q, killactive
|
||||||
|
bind = SUPER, F, fullscreen, 1
|
||||||
|
bind = SUPER SHIFT, F, fullscreen, 0
|
||||||
|
bind = SUPER SHIFT, T, togglefloating
|
||||||
|
bind = SUPER, W, togglegroup
|
||||||
|
|
||||||
|
# === Focus Navigation ===
|
||||||
|
bind = SUPER, left, movefocus, l
|
||||||
|
bind = SUPER, down, movefocus, d
|
||||||
|
bind = SUPER, up, movefocus, u
|
||||||
|
bind = SUPER, right, movefocus, r
|
||||||
|
bind = SUPER, H, movefocus, l
|
||||||
|
bind = SUPER, J, movefocus, d
|
||||||
|
bind = SUPER, K, movefocus, u
|
||||||
|
bind = SUPER, L, movefocus, r
|
||||||
|
|
||||||
|
# === Window Movement ===
|
||||||
|
bind = SUPER SHIFT, left, movewindow, l
|
||||||
|
bind = SUPER SHIFT, down, movewindow, d
|
||||||
|
bind = SUPER SHIFT, up, movewindow, u
|
||||||
|
bind = SUPER SHIFT, right, movewindow, r
|
||||||
|
bind = SUPER SHIFT, H, movewindow, l
|
||||||
|
bind = SUPER SHIFT, J, movewindow, d
|
||||||
|
bind = SUPER SHIFT, K, movewindow, u
|
||||||
|
bind = SUPER SHIFT, L, movewindow, r
|
||||||
|
|
||||||
|
# === Column Navigation ===
|
||||||
|
bind = SUPER, Home, focuswindow, first
|
||||||
|
bind = SUPER, End, focuswindow, last
|
||||||
|
|
||||||
|
# === Monitor Navigation ===
|
||||||
|
bind = SUPER CTRL, left, focusmonitor, l
|
||||||
|
bind = SUPER CTRL, right, focusmonitor, r
|
||||||
|
bind = SUPER CTRL, H, focusmonitor, l
|
||||||
|
bind = SUPER CTRL, J, focusmonitor, d
|
||||||
|
bind = SUPER CTRL, K, focusmonitor, u
|
||||||
|
bind = SUPER CTRL, L, focusmonitor, r
|
||||||
|
|
||||||
|
# === Move to Monitor ===
|
||||||
|
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
|
||||||
|
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
|
||||||
|
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
|
||||||
|
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
|
||||||
|
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
|
||||||
|
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
|
||||||
|
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
|
||||||
|
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
|
||||||
|
|
||||||
|
# === Workspace Navigation ===
|
||||||
|
bind = SUPER, Page_Down, workspace, e+1
|
||||||
|
bind = SUPER, Page_Up, workspace, e-1
|
||||||
|
bind = SUPER, U, workspace, e+1
|
||||||
|
bind = SUPER, I, workspace, e-1
|
||||||
|
bind = SUPER CTRL, down, movetoworkspace, e+1
|
||||||
|
bind = SUPER CTRL, up, movetoworkspace, e-1
|
||||||
|
bind = SUPER CTRL, U, movetoworkspace, e+1
|
||||||
|
bind = SUPER CTRL, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Workspace Management ===
|
||||||
|
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
||||||
|
|
||||||
|
# === Move Workspaces ===
|
||||||
|
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
||||||
|
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
||||||
|
bind = SUPER SHIFT, U, movetoworkspace, e+1
|
||||||
|
bind = SUPER SHIFT, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Mouse Wheel Navigation ===
|
||||||
|
bind = SUPER, mouse_down, workspace, e+1
|
||||||
|
bind = SUPER, mouse_up, workspace, e-1
|
||||||
|
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
|
||||||
|
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Numbered Workspaces ===
|
||||||
|
bind = SUPER, 1, workspace, 1
|
||||||
|
bind = SUPER, 2, workspace, 2
|
||||||
|
bind = SUPER, 3, workspace, 3
|
||||||
|
bind = SUPER, 4, workspace, 4
|
||||||
|
bind = SUPER, 5, workspace, 5
|
||||||
|
bind = SUPER, 6, workspace, 6
|
||||||
|
bind = SUPER, 7, workspace, 7
|
||||||
|
bind = SUPER, 8, workspace, 8
|
||||||
|
bind = SUPER, 9, workspace, 9
|
||||||
|
|
||||||
|
# === Move to Numbered Workspaces ===
|
||||||
|
bind = SUPER SHIFT, 1, movetoworkspace, 1
|
||||||
|
bind = SUPER SHIFT, 2, movetoworkspace, 2
|
||||||
|
bind = SUPER SHIFT, 3, movetoworkspace, 3
|
||||||
|
bind = SUPER SHIFT, 4, movetoworkspace, 4
|
||||||
|
bind = SUPER SHIFT, 5, movetoworkspace, 5
|
||||||
|
bind = SUPER SHIFT, 6, movetoworkspace, 6
|
||||||
|
bind = SUPER SHIFT, 7, movetoworkspace, 7
|
||||||
|
bind = SUPER SHIFT, 8, movetoworkspace, 8
|
||||||
|
bind = SUPER SHIFT, 9, movetoworkspace, 9
|
||||||
|
|
||||||
|
# === Column Management ===
|
||||||
|
bind = SUPER, bracketleft, layoutmsg, preselect l
|
||||||
|
bind = SUPER, bracketright, layoutmsg, preselect r
|
||||||
|
|
||||||
|
# === Sizing & Layout ===
|
||||||
|
bind = SUPER, R, layoutmsg, togglesplit
|
||||||
|
bind = SUPER CTRL, F, resizeactive, exact 100%
|
||||||
|
|
||||||
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||||
|
bindmd = SUPER, mouse:273, Resize window, resizewindow
|
||||||
|
|
||||||
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
|
||||||
|
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
|
||||||
|
|
||||||
|
# === Manual Sizing ===
|
||||||
|
binde = SUPER, minus, resizeactive, -10% 0
|
||||||
|
binde = SUPER, equal, resizeactive, 10% 0
|
||||||
|
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
|
||||||
|
binde = SUPER SHIFT, equal, resizeactive, 0 10%
|
||||||
|
|
||||||
|
# === Screenshots ===
|
||||||
|
bind = , Print, exec, dms screenshot
|
||||||
|
bind = CTRL, Print, exec, dms screenshot full
|
||||||
|
bind = ALT, Print, exec, dms screenshot window
|
||||||
|
|
||||||
|
# === System Controls ===
|
||||||
|
bind = SUPER SHIFT, P, dpms, toggle
|
||||||
@@ -106,173 +106,13 @@ windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture
|
|||||||
windowrule = float on, match:class ^(zoom)$
|
windowrule = float on, match:class ^(zoom)$
|
||||||
|
|
||||||
# DMS windows floating by default
|
# DMS windows floating by default
|
||||||
windowrule = float on, match:class ^(org.quickshell)$
|
# ! Hyprland doesn't size these windows correctly so disabling by default here
|
||||||
windowrule = opacity 0.9 0.9, match:float false, match:focus false
|
# windowrule = float on, match:class ^(org.quickshell)$
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
|
|
||||||
# ==================
|
|
||||||
# KEYBINDINGS
|
|
||||||
# ==================
|
|
||||||
$mod = SUPER
|
|
||||||
|
|
||||||
# === Application Launchers ===
|
|
||||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
|
||||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
|
||||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
|
||||||
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
|
||||||
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
|
|
||||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
|
||||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
|
||||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
|
||||||
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
|
|
||||||
|
|
||||||
# === Cheat sheet
|
|
||||||
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|
||||||
|
|
||||||
# === Security ===
|
|
||||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
|
||||||
bind = $mod SHIFT, E, exit
|
|
||||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
|
||||||
|
|
||||||
# === Audio Controls ===
|
|
||||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
|
||||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
|
||||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
|
||||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
|
||||||
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
|
||||||
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
|
||||||
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
|
||||||
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
|
||||||
|
|
||||||
# === Brightness Controls ===
|
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
|
||||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
|
||||||
|
|
||||||
# === Window Management ===
|
|
||||||
bind = $mod, Q, killactive
|
|
||||||
bind = $mod, F, fullscreen, 1
|
|
||||||
bind = $mod SHIFT, F, fullscreen, 0
|
|
||||||
bind = $mod SHIFT, T, togglefloating
|
|
||||||
bind = $mod, W, togglegroup
|
|
||||||
|
|
||||||
# === Focus Navigation ===
|
|
||||||
bind = $mod, left, movefocus, l
|
|
||||||
bind = $mod, down, movefocus, d
|
|
||||||
bind = $mod, up, movefocus, u
|
|
||||||
bind = $mod, right, movefocus, r
|
|
||||||
bind = $mod, H, movefocus, l
|
|
||||||
bind = $mod, J, movefocus, d
|
|
||||||
bind = $mod, K, movefocus, u
|
|
||||||
bind = $mod, L, movefocus, r
|
|
||||||
|
|
||||||
# === Window Movement ===
|
|
||||||
bind = $mod SHIFT, left, movewindow, l
|
|
||||||
bind = $mod SHIFT, down, movewindow, d
|
|
||||||
bind = $mod SHIFT, up, movewindow, u
|
|
||||||
bind = $mod SHIFT, right, movewindow, r
|
|
||||||
bind = $mod SHIFT, H, movewindow, l
|
|
||||||
bind = $mod SHIFT, J, movewindow, d
|
|
||||||
bind = $mod SHIFT, K, movewindow, u
|
|
||||||
bind = $mod SHIFT, L, movewindow, r
|
|
||||||
|
|
||||||
# === Column Navigation ===
|
|
||||||
bind = $mod, Home, focuswindow, first
|
|
||||||
bind = $mod, End, focuswindow, last
|
|
||||||
|
|
||||||
# === Monitor Navigation ===
|
|
||||||
bind = $mod CTRL, left, focusmonitor, l
|
|
||||||
bind = $mod CTRL, right, focusmonitor, r
|
|
||||||
bind = $mod CTRL, H, focusmonitor, l
|
|
||||||
bind = $mod CTRL, J, focusmonitor, d
|
|
||||||
bind = $mod CTRL, K, focusmonitor, u
|
|
||||||
bind = $mod CTRL, L, focusmonitor, r
|
|
||||||
|
|
||||||
# === Move to Monitor ===
|
|
||||||
bind = $mod SHIFT CTRL, left, movewindow, mon:l
|
|
||||||
bind = $mod SHIFT CTRL, down, movewindow, mon:d
|
|
||||||
bind = $mod SHIFT CTRL, up, movewindow, mon:u
|
|
||||||
bind = $mod SHIFT CTRL, right, movewindow, mon:r
|
|
||||||
bind = $mod SHIFT CTRL, H, movewindow, mon:l
|
|
||||||
bind = $mod SHIFT CTRL, J, movewindow, mon:d
|
|
||||||
bind = $mod SHIFT CTRL, K, movewindow, mon:u
|
|
||||||
bind = $mod SHIFT CTRL, L, movewindow, mon:r
|
|
||||||
|
|
||||||
# === Workspace Navigation ===
|
|
||||||
bind = $mod, Page_Down, workspace, e+1
|
|
||||||
bind = $mod, Page_Up, workspace, e-1
|
|
||||||
bind = $mod, U, workspace, e+1
|
|
||||||
bind = $mod, I, workspace, e-1
|
|
||||||
bind = $mod CTRL, down, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, up, movetoworkspace, e-1
|
|
||||||
bind = $mod CTRL, U, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Move Workspaces ===
|
|
||||||
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
|
|
||||||
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
|
|
||||||
bind = $mod SHIFT, U, movetoworkspace, e+1
|
|
||||||
bind = $mod SHIFT, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Mouse Wheel Navigation ===
|
|
||||||
bind = $mod, mouse_down, workspace, e+1
|
|
||||||
bind = $mod, mouse_up, workspace, e-1
|
|
||||||
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Numbered Workspaces ===
|
|
||||||
bind = $mod, 1, workspace, 1
|
|
||||||
bind = $mod, 2, workspace, 2
|
|
||||||
bind = $mod, 3, workspace, 3
|
|
||||||
bind = $mod, 4, workspace, 4
|
|
||||||
bind = $mod, 5, workspace, 5
|
|
||||||
bind = $mod, 6, workspace, 6
|
|
||||||
bind = $mod, 7, workspace, 7
|
|
||||||
bind = $mod, 8, workspace, 8
|
|
||||||
bind = $mod, 9, workspace, 9
|
|
||||||
|
|
||||||
# === Move to Numbered Workspaces ===
|
|
||||||
bind = $mod SHIFT, 1, movetoworkspace, 1
|
|
||||||
bind = $mod SHIFT, 2, movetoworkspace, 2
|
|
||||||
bind = $mod SHIFT, 3, movetoworkspace, 3
|
|
||||||
bind = $mod SHIFT, 4, movetoworkspace, 4
|
|
||||||
bind = $mod SHIFT, 5, movetoworkspace, 5
|
|
||||||
bind = $mod SHIFT, 6, movetoworkspace, 6
|
|
||||||
bind = $mod SHIFT, 7, movetoworkspace, 7
|
|
||||||
bind = $mod SHIFT, 8, movetoworkspace, 8
|
|
||||||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
|
||||||
|
|
||||||
# === Column Management ===
|
|
||||||
bind = $mod, bracketleft, layoutmsg, preselect l
|
|
||||||
bind = $mod, bracketright, layoutmsg, preselect r
|
|
||||||
|
|
||||||
# === Sizing & Layout ===
|
|
||||||
bind = $mod, R, layoutmsg, togglesplit
|
|
||||||
bind = $mod CTRL, F, resizeactive, exact 100%
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindmd = $mod, mouse:272, Move window, movewindow
|
|
||||||
bindmd = $mod, mouse:273, Resize window, resizewindow
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
|
|
||||||
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
|
|
||||||
|
|
||||||
# === Manual Sizing ===
|
|
||||||
binde = $mod, minus, resizeactive, -10% 0
|
|
||||||
binde = $mod, equal, resizeactive, 10% 0
|
|
||||||
binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
|
||||||
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
|
||||||
|
|
||||||
# === Screenshots ===
|
|
||||||
bind = , Print, exec, dms screenshot
|
|
||||||
bind = CTRL, Print, exec, dms screenshot full
|
|
||||||
bind = ALT, Print, exec, dms screenshot window
|
|
||||||
|
|
||||||
# === System Controls ===
|
|
||||||
bind = $mod SHIFT, P, dpms, toggle
|
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
source = ./dms/colors.conf
|
||||||
source = ./dms/outputs.conf
|
source = ./dms/outputs.conf
|
||||||
source = ./dms/layout.conf
|
source = ./dms/layout.conf
|
||||||
source = ./dms/cursor.conf
|
source = ./dms/cursor.conf
|
||||||
|
source = ./dms/binds.conf
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ binds {
|
|||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
Mod+M hotkey-overlay-title="Task Manager" {
|
||||||
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
|
||||||
Mod+Comma hotkey-overlay-title="Settings" {
|
Mod+Comma hotkey-overlay-title="Settings" {
|
||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
@@ -131,6 +133,11 @@ binds {
|
|||||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// === Workspace Management ===
|
||||||
|
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
|
||||||
|
spawn "dms" "ipc" "call" "workspace-rename" "open";
|
||||||
|
}
|
||||||
|
|
||||||
// === Move Workspaces ===
|
// === Move Workspaces ===
|
||||||
Mod+Shift+Page_Down { move-workspace-down; }
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
Mod+Shift+Page_Up { move-workspace-up; }
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ var HyprColorsConfig string
|
|||||||
|
|
||||||
//go:embed embedded/hypr-layout.conf
|
//go:embed embedded/hypr-layout.conf
|
||||||
var HyprLayoutConfig string
|
var HyprLayoutConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-binds.conf
|
||||||
|
var HyprBindsConfig string
|
||||||
|
|||||||
@@ -199,31 +199,6 @@ func labToHex(L, a, b float64) string {
|
|||||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust brightness while keeping the same hue
|
|
||||||
func retoneToL(hex string, Ltarget float64) string {
|
|
||||||
rgb := HexToRGB(hex)
|
|
||||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
|
||||||
L, a, b := col.Lab()
|
|
||||||
L100 := L * 100.0
|
|
||||||
|
|
||||||
scale := 1.0
|
|
||||||
if L100 != 0 {
|
|
||||||
scale = Ltarget / L100
|
|
||||||
}
|
|
||||||
|
|
||||||
a2, b2 := a*scale, b*scale
|
|
||||||
|
|
||||||
// Don't let it get too saturated
|
|
||||||
maxChroma := 0.4
|
|
||||||
if math.Hypot(a2, b2) > maxChroma {
|
|
||||||
k := maxChroma / math.Hypot(a2, b2)
|
|
||||||
a2 *= k
|
|
||||||
b2 *= k
|
|
||||||
}
|
|
||||||
|
|
||||||
return labToHex(Ltarget, a2, b2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||||
Lf := getLstar(hexFg)
|
Lf := getLstar(hexFg)
|
||||||
Lb := getLstar(hexBg)
|
Lb := getLstar(hexBg)
|
||||||
@@ -356,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
return hexColor
|
return hexColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bidirectional contrast - tries both lighter and darker, picks closest to original
|
||||||
|
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||||
|
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||||
|
if current >= minLc {
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fg := HexToRGB(hexColor)
|
||||||
|
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||||
|
origL, af, bf := cf.Lab()
|
||||||
|
|
||||||
|
var darkerResult, lighterResult string
|
||||||
|
darkerL, lighterL := origL, origL
|
||||||
|
darkerFound, lighterFound := false, false
|
||||||
|
|
||||||
|
step := 0.5
|
||||||
|
for i := range 120 {
|
||||||
|
if !darkerFound {
|
||||||
|
darkerL = math.Max(0, origL-float64(i)*step)
|
||||||
|
cand := labToHex(darkerL, af, bf)
|
||||||
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
|
darkerResult = cand
|
||||||
|
darkerFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !lighterFound {
|
||||||
|
lighterL = math.Min(100, origL+float64(i)*step)
|
||||||
|
cand := labToHex(lighterL, af, bf)
|
||||||
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
|
lighterResult = cand
|
||||||
|
lighterFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if darkerFound && lighterFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if darkerFound && lighterFound {
|
||||||
|
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
|
||||||
|
return darkerResult
|
||||||
|
}
|
||||||
|
return lighterResult
|
||||||
|
}
|
||||||
|
if darkerFound {
|
||||||
|
return darkerResult
|
||||||
|
}
|
||||||
|
if lighterFound {
|
||||||
|
return lighterResult
|
||||||
|
}
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
type PaletteOptions struct {
|
type PaletteOptions struct {
|
||||||
IsLight bool
|
IsLight bool
|
||||||
Background string
|
Background string
|
||||||
@@ -369,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
|
|||||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||||
|
if opts.UseDPS {
|
||||||
|
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
|
||||||
|
}
|
||||||
|
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func blendHue(base, target, factor float64) float64 {
|
||||||
|
diff := target - base
|
||||||
|
if diff > 0.5 {
|
||||||
|
diff -= 1.0
|
||||||
|
} else if diff < -0.5 {
|
||||||
|
diff += 1.0
|
||||||
|
}
|
||||||
|
result := base + diff*factor
|
||||||
|
if result < 0 {
|
||||||
|
result += 1.0
|
||||||
|
} else if result >= 1.0 {
|
||||||
|
result -= 1.0
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func DeriveContainer(primary string, isLight bool) string {
|
func DeriveContainer(primary string, isLight bool) string {
|
||||||
rgb := HexToRGB(primary)
|
rgb := HexToRGB(primary)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
@@ -389,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
rgb := HexToRGB(baseColor)
|
rgb := HexToRGB(baseColor)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
pr := HexToRGB(primaryColor)
|
||||||
|
ph := RGBToHSV(pr)
|
||||||
|
|
||||||
var palette Palette
|
var palette Palette
|
||||||
|
|
||||||
var normalTextTarget, secondaryTarget float64
|
var normalTextTarget, secondaryTarget float64
|
||||||
@@ -410,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
}
|
}
|
||||||
palette.Color0 = NewColorInfo(bgColor)
|
palette.Color0 = NewColorInfo(bgColor)
|
||||||
|
|
||||||
hueShift := (hsv.H - 0.6) * 0.12
|
baseSat := math.Max(ph.S, 0.5)
|
||||||
satBoost := 1.15
|
baseVal := math.Max(ph.V, 0.5)
|
||||||
|
|
||||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
redH := blendHue(0.0, ph.H, 0.12)
|
||||||
var redColor string
|
greenH := blendHue(0.33, ph.H, 0.10)
|
||||||
if opts.IsLight {
|
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
|
||||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
|
||||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
accentTarget := secondaryTarget * 0.7
|
||||||
var greenColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
|
||||||
var yellowColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
var blueColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
|
||||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
|
||||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
magH := hsv.H - 0.03
|
|
||||||
if magH < 0 {
|
|
||||||
magH += 1.0
|
|
||||||
}
|
|
||||||
var magColor string
|
|
||||||
hr := HexToRGB(primaryColor)
|
|
||||||
hh := RGBToHSV(hr)
|
|
||||||
if opts.IsLight {
|
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
|
||||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
|
||||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
cyanH := hsv.H + 0.08
|
|
||||||
if cyanH > 1.0 {
|
|
||||||
cyanH -= 1.0
|
|
||||||
}
|
|
||||||
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
|
||||||
|
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
redS := math.Min(baseSat*1.2, 1.0)
|
||||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
redV := baseVal * 0.95
|
||||||
} else {
|
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||||
palette.Color7 = NewColorInfo("#abb2bf")
|
|
||||||
palette.Color8 = NewColorInfo("#5c6370")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.IsLight {
|
greenS := math.Min(baseSat*1.3, 1.0)
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
greenV := baseVal * 0.75
|
||||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
|
||||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
|
||||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
|
||||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
|
||||||
hr := HexToRGB(primaryColor)
|
|
||||||
hh := RGBToHSV(hr)
|
|
||||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
|
||||||
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
|
||||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
|
||||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
|
||||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
|
||||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
|
||||||
} else {
|
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
|
||||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
|
||||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
|
||||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
|
||||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
|
||||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
|
||||||
brightBlue := retoneToL(primaryColor, 85.0)
|
|
||||||
palette.Color12 = NewColorInfo(brightBlue)
|
|
||||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
|
||||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
|
||||||
brightCyanH := hsv.H + 0.02
|
|
||||||
if brightCyanH > 1.0 {
|
|
||||||
brightCyanH -= 1.0
|
|
||||||
}
|
|
||||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
|
||||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.IsLight {
|
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||||
|
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
blueS := math.Min(ph.S*1.05, 1.0)
|
||||||
|
blueV := math.Min(ph.V*1.05, 1.0)
|
||||||
|
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
// Color5 matches primary_container exactly (light container in light mode)
|
||||||
|
container5 := DeriveContainer(primaryColor, true)
|
||||||
|
palette.Color5 = NewColorInfo(container5)
|
||||||
|
|
||||||
|
palette.Color6 = NewColorInfo(primaryColor)
|
||||||
|
|
||||||
|
gray7S := baseSat * 0.08
|
||||||
|
gray7V := baseVal * 0.28
|
||||||
|
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
gray8S := baseSat * 0.05
|
||||||
|
gray8V := baseVal * 0.85
|
||||||
|
dimTarget := secondaryTarget * 0.5
|
||||||
|
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
|
||||||
|
|
||||||
|
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||||
|
brightRedV := math.Min(baseVal*1.2, 1.0)
|
||||||
|
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightGreenS := math.Min(baseSat*1.1, 1.0)
|
||||||
|
brightGreenV := math.Min(baseVal*1.1, 1.0)
|
||||||
|
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightYellowS := math.Min(baseSat*1.4, 1.0)
|
||||||
|
brightYellowV := math.Min(baseVal*1.3, 1.0)
|
||||||
|
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightBlueS := math.Min(ph.S*1.1, 1.0)
|
||||||
|
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||||
|
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
lightContainer := DeriveContainer(primaryColor, true)
|
||||||
|
palette.Color13 = NewColorInfo(lightContainer)
|
||||||
|
|
||||||
|
brightCyanS := ph.S * 0.5
|
||||||
|
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||||
|
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
|
||||||
|
|
||||||
|
white15S := baseSat * 0.04
|
||||||
|
white15V := math.Min(baseVal*1.5, 1.0)
|
||||||
|
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
|
||||||
} else {
|
} else {
|
||||||
palette.Color15 = NewColorInfo("#ffffff")
|
redS := math.Min(baseSat*1.1, 1.0)
|
||||||
|
redV := math.Min(baseVal*1.15, 1.0)
|
||||||
|
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
greenS := math.Min(baseSat*1.0, 1.0)
|
||||||
|
greenV := math.Min(baseVal*1.0, 1.0)
|
||||||
|
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
yellowS := math.Min(baseSat*1.1, 1.0)
|
||||||
|
yellowV := math.Min(baseVal*1.25, 1.0)
|
||||||
|
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
// Slightly more saturated variant of primary
|
||||||
|
blueS := math.Min(ph.S*1.2, 1.0)
|
||||||
|
blueV := ph.V * 0.95
|
||||||
|
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||||
|
darkContainer := DeriveContainer(primaryColor, false)
|
||||||
|
palette.Color5 = NewColorInfo(darkContainer)
|
||||||
|
|
||||||
|
palette.Color6 = NewColorInfo(primaryColor)
|
||||||
|
|
||||||
|
gray7S := baseSat * 0.12
|
||||||
|
gray7V := math.Min(baseVal*1.05, 1.0)
|
||||||
|
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
gray8S := baseSat * 0.15
|
||||||
|
gray8V := baseVal * 0.65
|
||||||
|
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||||
|
|
||||||
|
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||||
|
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||||
|
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightGreenS := math.Min(baseSat*0.7, 1.0)
|
||||||
|
brightGreenV := math.Min(baseVal*1.2, 1.0)
|
||||||
|
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightYellowS := math.Min(baseSat*0.7, 1.0)
|
||||||
|
brightYellowV := math.Min(baseVal*1.5, 1.0)
|
||||||
|
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
|
||||||
|
// Color12: Start of the lighter gradient - slightly desaturated
|
||||||
|
brightBlueS := ph.S * 0.85
|
||||||
|
brightBlueV := math.Min(ph.V*1.1, 1.0)
|
||||||
|
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
// Medium-high saturation pastel primary
|
||||||
|
color13S := ph.S * 0.7
|
||||||
|
color13V := math.Min(ph.V*1.3, 1.0)
|
||||||
|
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
|
||||||
|
|
||||||
|
// Lower saturation, lighter variant
|
||||||
|
color14S := ph.S * 0.45
|
||||||
|
color14V := math.Min(ph.V*1.4, 1.0)
|
||||||
|
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
|
||||||
|
|
||||||
|
white15S := baseSat * 0.05
|
||||||
|
white15V := math.Min(baseVal*1.45, 1.0)
|
||||||
|
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
return palette
|
return palette
|
||||||
|
|||||||
@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
|
|||||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
// Color15 is now derived from primary, so just verify it's a valid color
|
||||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
||||||
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
color15Lum := Luminance(result.Color15.Hex)
|
||||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
if tt.opts.IsLight {
|
||||||
|
// Light mode: Color15 should still be relatively light
|
||||||
|
if color15Lum < 0.5 {
|
||||||
|
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
|
||||||
|
if color15Lum < 0.5 {
|
||||||
|
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -579,6 +588,10 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
|||||||
|
|
||||||
bgColor := result.Color0.Hex
|
bgColor := result.Color0.Hex
|
||||||
for i := 1; i < 8; i++ {
|
for i := 1; i < 8; i++ {
|
||||||
|
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||||
|
if i == 5 || i == 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||||
minLc := 30.0
|
minLc := 30.0
|
||||||
if lc < minLc && lc > 0 {
|
if lc < minLc && lc > 0 {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ func init() {
|
|||||||
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchDistribution struct {
|
type ArchDistribution struct {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
// Standard zypper packages
|
// Standard zypper packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
@@ -117,6 +116,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,450 +0,0 @@
|
|||||||
//go:build !distro_binary
|
|
||||||
|
|
||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StateMainMenu AppState = iota
|
|
||||||
StateUpdate
|
|
||||||
StateUpdatePassword
|
|
||||||
StateUpdateProgress
|
|
||||||
StateShell
|
|
||||||
StatePluginsMenu
|
|
||||||
StatePluginsBrowse
|
|
||||||
StatePluginDetail
|
|
||||||
StatePluginSearch
|
|
||||||
StatePluginsInstalled
|
|
||||||
StatePluginInstalledDetail
|
|
||||||
StateGreeterMenu
|
|
||||||
StateGreeterCompositorSelect
|
|
||||||
StateGreeterPassword
|
|
||||||
StateGreeterInstalling
|
|
||||||
StateAbout
|
|
||||||
)
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
version string
|
|
||||||
detector *Detector
|
|
||||||
dependencies []DependencyInfo
|
|
||||||
state AppState
|
|
||||||
selectedItem int
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
|
|
||||||
// Menu items
|
|
||||||
menuItems []MenuItem
|
|
||||||
|
|
||||||
updateDeps []DependencyInfo
|
|
||||||
selectedUpdateDep int
|
|
||||||
updateToggles map[string]bool
|
|
||||||
|
|
||||||
updateProgressChan chan updateProgressMsg
|
|
||||||
updateProgress updateProgressMsg
|
|
||||||
updateLogs []string
|
|
||||||
sudoPassword string
|
|
||||||
passwordInput string
|
|
||||||
passwordError string
|
|
||||||
|
|
||||||
// Window manager states
|
|
||||||
hyprlandInstalled bool
|
|
||||||
niriInstalled bool
|
|
||||||
|
|
||||||
selectedGreeterItem int
|
|
||||||
greeterInstallChan chan greeterProgressMsg
|
|
||||||
greeterProgress greeterProgressMsg
|
|
||||||
greeterLogs []string
|
|
||||||
greeterPasswordInput string
|
|
||||||
greeterPasswordError string
|
|
||||||
greeterSudoPassword string
|
|
||||||
greeterCompositors []string
|
|
||||||
greeterSelectedComp int
|
|
||||||
greeterChosenCompositor string
|
|
||||||
|
|
||||||
pluginsMenuItems []MenuItem
|
|
||||||
selectedPluginsMenuItem int
|
|
||||||
pluginsList []pluginInfo
|
|
||||||
filteredPluginsList []pluginInfo
|
|
||||||
selectedPluginIndex int
|
|
||||||
pluginsLoading bool
|
|
||||||
pluginsError string
|
|
||||||
pluginSearchQuery string
|
|
||||||
installedPluginsList []pluginInfo
|
|
||||||
selectedInstalledIndex int
|
|
||||||
installedPluginsLoading bool
|
|
||||||
installedPluginsError string
|
|
||||||
pluginInstallStatus map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginInfo struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Category string
|
|
||||||
Author string
|
|
||||||
Description string
|
|
||||||
Repo string
|
|
||||||
Path string
|
|
||||||
Capabilities []string
|
|
||||||
Compositors []string
|
|
||||||
Dependencies []string
|
|
||||||
FirstParty bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type MenuItem struct {
|
|
||||||
Label string
|
|
||||||
Action AppState
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewModel(version string) Model {
|
|
||||||
detector, _ := NewDetector()
|
|
||||||
var dependencies []DependencyInfo
|
|
||||||
var hyprlandInstalled, niriInstalled bool
|
|
||||||
var err error
|
|
||||||
if detector != nil {
|
|
||||||
dependencies = detector.GetInstalledComponents()
|
|
||||||
|
|
||||||
// Use the proper detection method for both window managers
|
|
||||||
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
|
|
||||||
if err != nil {
|
|
||||||
// Fallback to false if detection fails
|
|
||||||
hyprlandInstalled = false
|
|
||||||
niriInstalled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateToggles := make(map[string]bool)
|
|
||||||
for _, dep := range dependencies {
|
|
||||||
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
|
|
||||||
updateToggles[dep.Name] = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m := Model{
|
|
||||||
version: version,
|
|
||||||
detector: detector,
|
|
||||||
dependencies: dependencies,
|
|
||||||
state: StateMainMenu,
|
|
||||||
selectedItem: 0,
|
|
||||||
updateToggles: updateToggles,
|
|
||||||
updateDeps: dependencies,
|
|
||||||
updateProgressChan: make(chan updateProgressMsg, 100),
|
|
||||||
hyprlandInstalled: hyprlandInstalled,
|
|
||||||
niriInstalled: niriInstalled,
|
|
||||||
greeterInstallChan: make(chan greeterProgressMsg, 100),
|
|
||||||
pluginInstallStatus: make(map[string]bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.menuItems = m.buildMenuItems()
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) buildMenuItems() []MenuItem {
|
|
||||||
items := []MenuItem{
|
|
||||||
{Label: "Update", Action: StateUpdate},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shell management
|
|
||||||
if m.isShellRunning() {
|
|
||||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
|
||||||
} else {
|
|
||||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugins management
|
|
||||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
|
||||||
|
|
||||||
// Greeter management
|
|
||||||
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
|
|
||||||
|
|
||||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
|
||||||
return []MenuItem{
|
|
||||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
|
||||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) isShellRunning() bool {
|
|
||||||
// Check for both -c and -p flag patterns since quickshell can be started either way
|
|
||||||
// -c dms: config name mode
|
|
||||||
// -p <path>/dms: path mode (used when installed via system packages)
|
|
||||||
cmd := exec.Command("pgrep", "-f", "qs.*dms")
|
|
||||||
err := cmd.Run()
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.width = msg.Width
|
|
||||||
m.height = msg.Height
|
|
||||||
case shellStartedMsg:
|
|
||||||
m.menuItems = m.buildMenuItems()
|
|
||||||
if m.selectedItem >= len(m.menuItems) {
|
|
||||||
m.selectedItem = len(m.menuItems) - 1
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case updateProgressMsg:
|
|
||||||
m.updateProgress = msg
|
|
||||||
if msg.logOutput != "" {
|
|
||||||
m.updateLogs = append(m.updateLogs, msg.logOutput)
|
|
||||||
}
|
|
||||||
return m, m.waitForProgress()
|
|
||||||
case updateCompleteMsg:
|
|
||||||
m.updateProgress.complete = true
|
|
||||||
m.updateProgress.err = msg.err
|
|
||||||
m.dependencies = m.detector.GetInstalledComponents()
|
|
||||||
m.updateDeps = m.dependencies
|
|
||||||
m.menuItems = m.buildMenuItems()
|
|
||||||
|
|
||||||
// Restart shell if update was successful and shell is running
|
|
||||||
if msg.err == nil && m.isShellRunning() {
|
|
||||||
restartShell()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case greeterProgressMsg:
|
|
||||||
m.greeterProgress = msg
|
|
||||||
if msg.logOutput != "" {
|
|
||||||
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
|
|
||||||
}
|
|
||||||
return m, m.waitForGreeterProgress()
|
|
||||||
case pluginsLoadedMsg:
|
|
||||||
m.pluginsLoading = false
|
|
||||||
if msg.err != nil {
|
|
||||||
m.pluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
|
||||||
for i, p := range msg.plugins {
|
|
||||||
m.pluginsList[i] = pluginInfo{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Category: p.Category,
|
|
||||||
Author: p.Author,
|
|
||||||
Description: p.Description,
|
|
||||||
Repo: p.Repo,
|
|
||||||
Path: p.Path,
|
|
||||||
Capabilities: p.Capabilities,
|
|
||||||
Compositors: p.Compositors,
|
|
||||||
Dependencies: p.Dependencies,
|
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.filteredPluginsList = m.pluginsList
|
|
||||||
m.selectedPluginIndex = 0
|
|
||||||
m.updatePluginInstallStatus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case installedPluginsLoadedMsg:
|
|
||||||
m.installedPluginsLoading = false
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
|
||||||
for i, p := range msg.plugins {
|
|
||||||
m.installedPluginsList[i] = pluginInfo{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Category: p.Category,
|
|
||||||
Author: p.Author,
|
|
||||||
Description: p.Description,
|
|
||||||
Repo: p.Repo,
|
|
||||||
Path: p.Path,
|
|
||||||
Capabilities: p.Capabilities,
|
|
||||||
Compositors: p.Compositors,
|
|
||||||
Dependencies: p.Dependencies,
|
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.selectedInstalledIndex = 0
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case pluginUninstalledMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
m.state = StatePluginInstalledDetail
|
|
||||||
} else {
|
|
||||||
m.state = StatePluginsInstalled
|
|
||||||
m.installedPluginsLoading = true
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
return m, loadInstalledPlugins
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case pluginUpdatedMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case pluginInstalledMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.pluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.pluginInstallStatus[msg.pluginName] = true
|
|
||||||
m.pluginsError = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case greeterPasswordValidMsg:
|
|
||||||
if msg.valid {
|
|
||||||
m.greeterSudoPassword = msg.password
|
|
||||||
m.greeterPasswordInput = ""
|
|
||||||
m.greeterPasswordError = ""
|
|
||||||
m.state = StateGreeterInstalling
|
|
||||||
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
|
|
||||||
m.greeterLogs = []string{}
|
|
||||||
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
|
|
||||||
} else {
|
|
||||||
m.greeterPasswordError = "Incorrect password. Please try again."
|
|
||||||
m.greeterPasswordInput = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case passwordValidMsg:
|
|
||||||
if msg.valid {
|
|
||||||
m.sudoPassword = msg.password
|
|
||||||
m.passwordInput = ""
|
|
||||||
m.passwordError = ""
|
|
||||||
m.state = StateUpdateProgress
|
|
||||||
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
|
|
||||||
m.updateLogs = []string{}
|
|
||||||
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
|
|
||||||
} else {
|
|
||||||
m.passwordError = "Incorrect password. Please try again."
|
|
||||||
m.passwordInput = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch m.state {
|
|
||||||
case StateMainMenu:
|
|
||||||
return m.updateMainMenu(msg)
|
|
||||||
case StateUpdate:
|
|
||||||
return m.updateUpdateView(msg)
|
|
||||||
case StateUpdatePassword:
|
|
||||||
return m.updatePasswordView(msg)
|
|
||||||
case StateUpdateProgress:
|
|
||||||
return m.updateProgressView(msg)
|
|
||||||
case StateShell:
|
|
||||||
return m.updateShellView(msg)
|
|
||||||
case StatePluginsMenu:
|
|
||||||
return m.updatePluginsMenu(msg)
|
|
||||||
case StatePluginsBrowse:
|
|
||||||
return m.updatePluginsBrowse(msg)
|
|
||||||
case StatePluginDetail:
|
|
||||||
return m.updatePluginDetail(msg)
|
|
||||||
case StatePluginSearch:
|
|
||||||
return m.updatePluginSearch(msg)
|
|
||||||
case StatePluginsInstalled:
|
|
||||||
return m.updatePluginsInstalled(msg)
|
|
||||||
case StatePluginInstalledDetail:
|
|
||||||
return m.updatePluginInstalledDetail(msg)
|
|
||||||
case StateGreeterMenu:
|
|
||||||
return m.updateGreeterMenu(msg)
|
|
||||||
case StateGreeterCompositorSelect:
|
|
||||||
return m.updateGreeterCompositorSelect(msg)
|
|
||||||
case StateGreeterPassword:
|
|
||||||
return m.updateGreeterPasswordView(msg)
|
|
||||||
case StateGreeterInstalling:
|
|
||||||
return m.updateGreeterInstalling(msg)
|
|
||||||
case StateAbout:
|
|
||||||
return m.updateAboutView(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateProgressMsg struct {
|
|
||||||
progress float64
|
|
||||||
step string
|
|
||||||
complete bool
|
|
||||||
err error
|
|
||||||
logOutput string
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateCompleteMsg struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type passwordValidMsg struct {
|
|
||||||
password string
|
|
||||||
valid bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type greeterProgressMsg struct {
|
|
||||||
step string
|
|
||||||
complete bool
|
|
||||||
err error
|
|
||||||
logOutput string
|
|
||||||
}
|
|
||||||
|
|
||||||
type greeterPasswordValidMsg struct {
|
|
||||||
password string
|
|
||||||
valid bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) waitForProgress() tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
return <-m.updateProgressChan
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) waitForGreeterProgress() tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
return <-m.greeterInstallChan
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) View() string {
|
|
||||||
switch m.state {
|
|
||||||
case StateMainMenu:
|
|
||||||
return m.renderMainMenu()
|
|
||||||
case StateUpdate:
|
|
||||||
return m.renderUpdateView()
|
|
||||||
case StateUpdatePassword:
|
|
||||||
return m.renderPasswordView()
|
|
||||||
case StateUpdateProgress:
|
|
||||||
return m.renderProgressView()
|
|
||||||
case StateShell:
|
|
||||||
return m.renderShellView()
|
|
||||||
case StatePluginsMenu:
|
|
||||||
return m.renderPluginsMenu()
|
|
||||||
case StatePluginsBrowse:
|
|
||||||
return m.renderPluginsBrowse()
|
|
||||||
case StatePluginDetail:
|
|
||||||
return m.renderPluginDetail()
|
|
||||||
case StatePluginSearch:
|
|
||||||
return m.renderPluginSearch()
|
|
||||||
case StatePluginsInstalled:
|
|
||||||
return m.renderPluginsInstalled()
|
|
||||||
case StatePluginInstalledDetail:
|
|
||||||
return m.renderPluginInstalledDetail()
|
|
||||||
case StateGreeterMenu:
|
|
||||||
return m.renderGreeterMenu()
|
|
||||||
case StateGreeterCompositorSelect:
|
|
||||||
return m.renderGreeterCompositorSelect()
|
|
||||||
case StateGreeterPassword:
|
|
||||||
return m.renderGreeterPasswordView()
|
|
||||||
case StateGreeterInstalling:
|
|
||||||
return m.renderGreeterInstalling()
|
|
||||||
case StateAbout:
|
|
||||||
return m.renderAboutView()
|
|
||||||
default:
|
|
||||||
return m.renderMainMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
//go:build distro_binary
|
|
||||||
|
|
||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StateMainMenu AppState = iota
|
|
||||||
StateShell
|
|
||||||
StatePluginsMenu
|
|
||||||
StatePluginsBrowse
|
|
||||||
StatePluginDetail
|
|
||||||
StatePluginSearch
|
|
||||||
StatePluginsInstalled
|
|
||||||
StatePluginInstalledDetail
|
|
||||||
StateAbout
|
|
||||||
)
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
version string
|
|
||||||
detector *Detector
|
|
||||||
dependencies []DependencyInfo
|
|
||||||
state AppState
|
|
||||||
selectedItem int
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
|
|
||||||
// Menu items
|
|
||||||
menuItems []MenuItem
|
|
||||||
|
|
||||||
// Window manager states
|
|
||||||
hyprlandInstalled bool
|
|
||||||
niriInstalled bool
|
|
||||||
|
|
||||||
pluginsMenuItems []MenuItem
|
|
||||||
selectedPluginsMenuItem int
|
|
||||||
pluginsList []pluginInfo
|
|
||||||
filteredPluginsList []pluginInfo
|
|
||||||
selectedPluginIndex int
|
|
||||||
pluginsLoading bool
|
|
||||||
pluginsError string
|
|
||||||
pluginSearchQuery string
|
|
||||||
installedPluginsList []pluginInfo
|
|
||||||
selectedInstalledIndex int
|
|
||||||
installedPluginsLoading bool
|
|
||||||
installedPluginsError string
|
|
||||||
pluginInstallStatus map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginInfo struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Category string
|
|
||||||
Author string
|
|
||||||
Description string
|
|
||||||
Repo string
|
|
||||||
Path string
|
|
||||||
Capabilities []string
|
|
||||||
Compositors []string
|
|
||||||
Dependencies []string
|
|
||||||
FirstParty bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type MenuItem struct {
|
|
||||||
Label string
|
|
||||||
Action AppState
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewModel(version string) Model {
|
|
||||||
detector, _ := NewDetector()
|
|
||||||
|
|
||||||
var dependencies []DependencyInfo
|
|
||||||
var hyprlandInstalled, niriInstalled bool
|
|
||||||
|
|
||||||
if detector != nil {
|
|
||||||
dependencies = detector.GetInstalledComponents()
|
|
||||||
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
m := Model{
|
|
||||||
version: version,
|
|
||||||
detector: detector,
|
|
||||||
dependencies: dependencies,
|
|
||||||
state: StateMainMenu,
|
|
||||||
selectedItem: 0,
|
|
||||||
hyprlandInstalled: hyprlandInstalled,
|
|
||||||
niriInstalled: niriInstalled,
|
|
||||||
pluginInstallStatus: make(map[string]bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.menuItems = m.buildMenuItems()
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) buildMenuItems() []MenuItem {
|
|
||||||
items := []MenuItem{}
|
|
||||||
|
|
||||||
// Shell management
|
|
||||||
if m.isShellRunning() {
|
|
||||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
|
||||||
} else {
|
|
||||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugins management
|
|
||||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
|
||||||
|
|
||||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
|
||||||
return []MenuItem{
|
|
||||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
|
||||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) isShellRunning() bool {
|
|
||||||
cmd := exec.Command("pgrep", "-f", "qs -c dms")
|
|
||||||
err := cmd.Run()
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.width = msg.Width
|
|
||||||
m.height = msg.Height
|
|
||||||
case pluginsLoadedMsg:
|
|
||||||
m.pluginsLoading = false
|
|
||||||
if msg.err != nil {
|
|
||||||
m.pluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
|
||||||
for i, p := range msg.plugins {
|
|
||||||
m.pluginsList[i] = pluginInfo{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Category: p.Category,
|
|
||||||
Author: p.Author,
|
|
||||||
Description: p.Description,
|
|
||||||
Repo: p.Repo,
|
|
||||||
Path: p.Path,
|
|
||||||
Capabilities: p.Capabilities,
|
|
||||||
Compositors: p.Compositors,
|
|
||||||
Dependencies: p.Dependencies,
|
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.filteredPluginsList = m.pluginsList
|
|
||||||
m.selectedPluginIndex = 0
|
|
||||||
m.updatePluginInstallStatus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case installedPluginsLoadedMsg:
|
|
||||||
m.installedPluginsLoading = false
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
|
||||||
for i, p := range msg.plugins {
|
|
||||||
m.installedPluginsList[i] = pluginInfo{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Category: p.Category,
|
|
||||||
Author: p.Author,
|
|
||||||
Description: p.Description,
|
|
||||||
Repo: p.Repo,
|
|
||||||
Path: p.Path,
|
|
||||||
Capabilities: p.Capabilities,
|
|
||||||
Compositors: p.Compositors,
|
|
||||||
Dependencies: p.Dependencies,
|
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.selectedInstalledIndex = 0
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case pluginUninstalledMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
m.state = StatePluginInstalledDetail
|
|
||||||
} else {
|
|
||||||
m.state = StatePluginsInstalled
|
|
||||||
m.installedPluginsLoading = true
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
return m, loadInstalledPlugins
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case pluginUpdatedMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case pluginInstalledMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.pluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.pluginInstallStatus[msg.pluginName] = true
|
|
||||||
m.pluginsError = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch m.state {
|
|
||||||
case StateMainMenu:
|
|
||||||
return m.updateMainMenu(msg)
|
|
||||||
case StateShell:
|
|
||||||
return m.updateShellView(msg)
|
|
||||||
case StatePluginsMenu:
|
|
||||||
return m.updatePluginsMenu(msg)
|
|
||||||
case StatePluginsBrowse:
|
|
||||||
return m.updatePluginsBrowse(msg)
|
|
||||||
case StatePluginDetail:
|
|
||||||
return m.updatePluginDetail(msg)
|
|
||||||
case StatePluginSearch:
|
|
||||||
return m.updatePluginSearch(msg)
|
|
||||||
case StatePluginsInstalled:
|
|
||||||
return m.updatePluginsInstalled(msg)
|
|
||||||
case StatePluginInstalledDetail:
|
|
||||||
return m.updatePluginInstalledDetail(msg)
|
|
||||||
case StateAbout:
|
|
||||||
return m.updateAboutView(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) View() string {
|
|
||||||
switch m.state {
|
|
||||||
case StateMainMenu:
|
|
||||||
return m.renderMainMenu()
|
|
||||||
case StateShell:
|
|
||||||
return m.renderShellView()
|
|
||||||
case StatePluginsMenu:
|
|
||||||
return m.renderPluginsMenu()
|
|
||||||
case StatePluginsBrowse:
|
|
||||||
return m.renderPluginsBrowse()
|
|
||||||
case StatePluginDetail:
|
|
||||||
return m.renderPluginDetail()
|
|
||||||
case StatePluginSearch:
|
|
||||||
return m.renderPluginSearch()
|
|
||||||
case StatePluginsInstalled:
|
|
||||||
return m.renderPluginsInstalled()
|
|
||||||
case StatePluginInstalledDetail:
|
|
||||||
return m.renderPluginInstalledDetail()
|
|
||||||
case StateAbout:
|
|
||||||
return m.renderAboutView()
|
|
||||||
default:
|
|
||||||
return m.renderMainMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Detector struct {
|
|
||||||
homeDir string
|
|
||||||
distribution distros.Distribution
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Detector) GetDistribution() distros.Distribution {
|
|
||||||
return d.distribution
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDetector() (*Detector, error) {
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logChan := make(chan string, 100)
|
|
||||||
go func() {
|
|
||||||
for range logChan {
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dist, err := distros.NewDistribution(osInfo.Distribution.ID, logChan)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Detector{
|
|
||||||
homeDir: homeDir,
|
|
||||||
distribution: dist,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Detector) IsDMSInstalled() bool {
|
|
||||||
_, err := config.LocateDMSConfig()
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Detector) GetDependencyStatus() ([]deps.Dependency, error) {
|
|
||||||
hyprlandDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerHyprland)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
niriDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerNiri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine dependencies and deduplicate
|
|
||||||
depMap := make(map[string]deps.Dependency)
|
|
||||||
|
|
||||||
for _, dep := range hyprlandDeps {
|
|
||||||
depMap[dep.Name] = dep
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dep := range niriDeps {
|
|
||||||
// If dependency already exists, keep the one that's installed or needs update
|
|
||||||
if existing, exists := depMap[dep.Name]; exists {
|
|
||||||
if dep.Status > existing.Status {
|
|
||||||
depMap[dep.Name] = dep
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
depMap[dep.Name] = dep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert map back to slice
|
|
||||||
var allDeps []deps.Dependency
|
|
||||||
for _, dep := range depMap {
|
|
||||||
allDeps = append(allDeps, dep)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allDeps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Detector) GetWindowManagerStatus() (bool, bool, error) {
|
|
||||||
// Reuse the existing command detection logic from BaseDistribution
|
|
||||||
// Since all distros embed BaseDistribution, we can access it via interface
|
|
||||||
type CommandChecker interface {
|
|
||||||
CommandExists(string) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
checker, ok := d.distribution.(CommandChecker)
|
|
||||||
if !ok {
|
|
||||||
// Fallback to direct command check if interface not available
|
|
||||||
hyprlandInstalled := d.commandExists("hyprland") || d.commandExists("Hyprland")
|
|
||||||
niriInstalled := d.commandExists("niri")
|
|
||||||
return hyprlandInstalled, niriInstalled, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hyprlandInstalled := checker.CommandExists("hyprland") || checker.CommandExists("Hyprland")
|
|
||||||
niriInstalled := checker.CommandExists("niri")
|
|
||||||
|
|
||||||
return hyprlandInstalled, niriInstalled, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Detector) commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
|
||||||
dependencies, err := d.GetDependencyStatus()
|
|
||||||
if err != nil {
|
|
||||||
return []DependencyInfo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var components []DependencyInfo
|
|
||||||
for _, dep := range dependencies {
|
|
||||||
components = append(components, DependencyInfo{
|
|
||||||
Name: dep.Name,
|
|
||||||
Status: dep.Status,
|
|
||||||
Description: dep.Description,
|
|
||||||
Required: dep.Required,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return components
|
|
||||||
}
|
|
||||||
|
|
||||||
type DependencyInfo struct {
|
|
||||||
Name string
|
|
||||||
Status deps.DependencyStatus
|
|
||||||
Description string
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) updateShellView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateMainMenu
|
|
||||||
default:
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateAboutView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q", "esc":
|
|
||||||
if msg.String() == "esc" {
|
|
||||||
m.state = StateMainMenu
|
|
||||||
} else {
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func terminateShell() {
|
|
||||||
patterns := []string{"dms run", "qs -c dms"}
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
cmd := exec.Command("pkill", "-f", pattern)
|
|
||||||
cmd.Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startShellDaemon() {
|
|
||||||
cmd := exec.Command("dms", "run", "-d")
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Errorf("Error starting daemon: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartShell() {
|
|
||||||
terminateShell()
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
startShellDaemon()
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
//go:build !distro_binary
|
|
||||||
|
|
||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) updateUpdateView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
filteredDeps := m.getFilteredDeps()
|
|
||||||
maxIndex := len(filteredDeps) - 1
|
|
||||||
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateMainMenu
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedUpdateDep > 0 {
|
|
||||||
m.selectedUpdateDep--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedUpdateDep < maxIndex {
|
|
||||||
m.selectedUpdateDep++
|
|
||||||
}
|
|
||||||
case " ":
|
|
||||||
if dep := m.getDepAtVisualIndex(m.selectedUpdateDep); dep != nil {
|
|
||||||
m.updateToggles[dep.Name] = !m.updateToggles[dep.Name]
|
|
||||||
}
|
|
||||||
case "enter":
|
|
||||||
hasSelected := false
|
|
||||||
for _, toggle := range m.updateToggles {
|
|
||||||
if toggle {
|
|
||||||
hasSelected = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasSelected {
|
|
||||||
m.state = StateMainMenu
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.state = StateUpdatePassword
|
|
||||||
m.passwordInput = ""
|
|
||||||
m.passwordError = ""
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updatePasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateUpdate
|
|
||||||
m.passwordInput = ""
|
|
||||||
m.passwordError = ""
|
|
||||||
return m, nil
|
|
||||||
case "enter":
|
|
||||||
if m.passwordInput == "" {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, m.validatePassword(m.passwordInput)
|
|
||||||
case "backspace":
|
|
||||||
if len(m.passwordInput) > 0 {
|
|
||||||
m.passwordInput = m.passwordInput[:len(m.passwordInput)-1]
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
|
||||||
m.passwordInput += msg.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateProgressView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
if m.updateProgress.complete {
|
|
||||||
m.state = StateMainMenu
|
|
||||||
m.updateProgress = updateProgressMsg{}
|
|
||||||
m.updateLogs = []string{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) validatePassword(password string) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer stdin.Close()
|
|
||||||
fmt.Fprintf(stdin, "%s\n", password)
|
|
||||||
}()
|
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
outputStr := string(output)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
|
||||||
strings.Contains(outputStr, "incorrect password") ||
|
|
||||||
strings.Contains(outputStr, "authentication failure") {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return passwordValidMsg{password: password, valid: true}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) performUpdate() tea.Cmd {
|
|
||||||
var depsToUpdate []deps.Dependency
|
|
||||||
|
|
||||||
for _, depInfo := range m.updateDeps {
|
|
||||||
if m.updateToggles[depInfo.Name] {
|
|
||||||
depsToUpdate = append(depsToUpdate, deps.Dependency{
|
|
||||||
Name: depInfo.Name,
|
|
||||||
Status: depInfo.Status,
|
|
||||||
Description: depInfo.Description,
|
|
||||||
Required: depInfo.Required,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(depsToUpdate) == 0 {
|
|
||||||
return func() tea.Msg {
|
|
||||||
return updateCompleteMsg{err: nil}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wm := deps.WindowManagerHyprland
|
|
||||||
if m.niriInstalled {
|
|
||||||
wm = deps.WindowManagerNiri
|
|
||||||
}
|
|
||||||
|
|
||||||
sudoPassword := m.sudoPassword
|
|
||||||
reinstallFlags := make(map[string]bool)
|
|
||||||
for name, toggled := range m.updateToggles {
|
|
||||||
if toggled {
|
|
||||||
reinstallFlags[name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
distribution := m.detector.GetDistribution()
|
|
||||||
progressChan := m.updateProgressChan
|
|
||||||
|
|
||||||
return func() tea.Msg {
|
|
||||||
installerChan := make(chan distros.InstallProgressMsg, 100)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ctx := context.Background()
|
|
||||||
disabledFlags := make(map[string]bool)
|
|
||||||
err := distribution.InstallPackages(ctx, depsToUpdate, wm, sudoPassword, reinstallFlags, disabledFlags, false, installerChan)
|
|
||||||
close(installerChan)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
progressChan <- updateProgressMsg{complete: true, err: err}
|
|
||||||
} else {
|
|
||||||
progressChan <- updateProgressMsg{complete: true}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for msg := range installerChan {
|
|
||||||
progressChan <- updateProgressMsg{
|
|
||||||
progress: msg.Progress,
|
|
||||||
step: msg.Step,
|
|
||||||
complete: msg.IsComplete,
|
|
||||||
err: msg.Error,
|
|
||||||
logOutput: msg.LogOutput,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateGreeterMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
greeterMenuItems := []string{"Install Greeter"}
|
|
||||||
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateMainMenu
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedGreeterItem > 0 {
|
|
||||||
m.selectedGreeterItem--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedGreeterItem < len(greeterMenuItems)-1 {
|
|
||||||
m.selectedGreeterItem++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
if m.selectedGreeterItem == 0 {
|
|
||||||
compositors := greeter.DetectCompositors()
|
|
||||||
if len(compositors) == 0 {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.greeterCompositors = compositors
|
|
||||||
|
|
||||||
if len(compositors) > 1 {
|
|
||||||
m.state = StateGreeterCompositorSelect
|
|
||||||
m.greeterSelectedComp = 0
|
|
||||||
return m, nil
|
|
||||||
} else {
|
|
||||||
m.greeterChosenCompositor = compositors[0]
|
|
||||||
m.state = StateGreeterPassword
|
|
||||||
m.greeterPasswordInput = ""
|
|
||||||
m.greeterPasswordError = ""
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateGreeterCompositorSelect(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateGreeterMenu
|
|
||||||
return m, nil
|
|
||||||
case "up", "k":
|
|
||||||
if m.greeterSelectedComp > 0 {
|
|
||||||
m.greeterSelectedComp--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.greeterSelectedComp < len(m.greeterCompositors)-1 {
|
|
||||||
m.greeterSelectedComp++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
m.greeterChosenCompositor = m.greeterCompositors[m.greeterSelectedComp]
|
|
||||||
m.state = StateGreeterPassword
|
|
||||||
m.greeterPasswordInput = ""
|
|
||||||
m.greeterPasswordError = ""
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateGreeterPasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateGreeterMenu
|
|
||||||
m.greeterPasswordInput = ""
|
|
||||||
m.greeterPasswordError = ""
|
|
||||||
return m, nil
|
|
||||||
case "enter":
|
|
||||||
if m.greeterPasswordInput == "" {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, m.validateGreeterPassword(m.greeterPasswordInput)
|
|
||||||
case "backspace":
|
|
||||||
if len(m.greeterPasswordInput) > 0 {
|
|
||||||
m.greeterPasswordInput = m.greeterPasswordInput[:len(m.greeterPasswordInput)-1]
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
|
||||||
m.greeterPasswordInput += msg.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateGreeterInstalling(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
if m.greeterProgress.complete {
|
|
||||||
m.state = StateMainMenu
|
|
||||||
m.greeterProgress = greeterProgressMsg{}
|
|
||||||
m.greeterLogs = []string{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) performGreeterInstall() tea.Cmd {
|
|
||||||
progressChan := m.greeterInstallChan
|
|
||||||
sudoPassword := m.greeterSudoPassword
|
|
||||||
compositor := m.greeterChosenCompositor
|
|
||||||
|
|
||||||
return func() tea.Msg {
|
|
||||||
go func() {
|
|
||||||
logFunc := func(msg string) {
|
|
||||||
progressChan <- greeterProgressMsg{step: msg, logOutput: msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- greeterProgressMsg{step: "Checking greetd installation..."}
|
|
||||||
if err := performGreeterInstallSteps(progressChan, logFunc, sudoPassword, compositor); err != nil {
|
|
||||||
progressChan <- greeterProgressMsg{step: "Installation failed", complete: true, err: err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- greeterProgressMsg{step: "Installation complete", complete: true}
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) validateGreeterPassword(password string) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return greeterPasswordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer stdin.Close()
|
|
||||||
fmt.Fprintf(stdin, "%s\n", password)
|
|
||||||
}()
|
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
outputStr := string(output)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
|
||||||
strings.Contains(outputStr, "incorrect password") ||
|
|
||||||
strings.Contains(outputStr, "authentication failure") {
|
|
||||||
return greeterPasswordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
return greeterPasswordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return greeterPasswordValidMsg{password: password, valid: true}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func performGreeterInstallSteps(progressChan chan greeterProgressMsg, logFunc func(string), sudoPassword string, compositor string) error {
|
|
||||||
if err := greeter.EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- greeterProgressMsg{step: "Detecting DMS installation..."}
|
|
||||||
dmsPath, err := greeter.DetectDMSPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
|
|
||||||
|
|
||||||
logFunc(fmt.Sprintf("✓ Selected compositor: %s", compositor))
|
|
||||||
|
|
||||||
progressChan <- greeterProgressMsg{step: "Copying greeter files..."}
|
|
||||||
if err := greeter.CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- greeterProgressMsg{step: "Configuring greetd..."}
|
|
||||||
if err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- greeterProgressMsg{step: "Synchronizing DMS configurations..."}
|
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, sudoPassword); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
//go:build !distro_binary
|
|
||||||
|
|
||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
type shellStartedMsg struct{}
|
|
||||||
|
|
||||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q", "esc":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedItem > 0 {
|
|
||||||
m.selectedItem--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedItem < len(m.menuItems)-1 {
|
|
||||||
m.selectedItem++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
if m.selectedItem < len(m.menuItems) {
|
|
||||||
selectedAction := m.menuItems[m.selectedItem].Action
|
|
||||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
|
||||||
|
|
||||||
switch selectedAction {
|
|
||||||
case StateUpdate:
|
|
||||||
m.state = StateUpdate
|
|
||||||
m.selectedUpdateDep = 0
|
|
||||||
case StateShell:
|
|
||||||
if selectedLabel == "Terminate Shell" {
|
|
||||||
terminateShell()
|
|
||||||
m.menuItems = m.buildMenuItems()
|
|
||||||
if m.selectedItem >= len(m.menuItems) {
|
|
||||||
m.selectedItem = len(m.menuItems) - 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startShellDaemon()
|
|
||||||
// Wait a moment for the daemon to actually start before checking status
|
|
||||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
|
||||||
return shellStartedMsg{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case StatePluginsMenu:
|
|
||||||
m.state = StatePluginsMenu
|
|
||||||
m.selectedPluginsMenuItem = 0
|
|
||||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
|
||||||
case StateGreeterMenu:
|
|
||||||
m.state = StateGreeterMenu
|
|
||||||
m.selectedGreeterItem = 0
|
|
||||||
case StateAbout:
|
|
||||||
m.state = StateAbout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//go:build distro_binary
|
|
||||||
|
|
||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
type shellStartedMsg struct{}
|
|
||||||
|
|
||||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q", "esc":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedItem > 0 {
|
|
||||||
m.selectedItem--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedItem < len(m.menuItems)-1 {
|
|
||||||
m.selectedItem++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
if m.selectedItem < len(m.menuItems) {
|
|
||||||
selectedAction := m.menuItems[m.selectedItem].Action
|
|
||||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
|
||||||
|
|
||||||
switch selectedAction {
|
|
||||||
case StateShell:
|
|
||||||
if selectedLabel == "Terminate Shell" {
|
|
||||||
terminateShell()
|
|
||||||
m.menuItems = m.buildMenuItems()
|
|
||||||
if m.selectedItem >= len(m.menuItems) {
|
|
||||||
m.selectedItem = len(m.menuItems) - 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startShellDaemon()
|
|
||||||
// Wait a moment for the daemon to actually start before checking status
|
|
||||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
|
||||||
return shellStartedMsg{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case StatePluginsMenu:
|
|
||||||
m.state = StatePluginsMenu
|
|
||||||
m.selectedPluginsMenuItem = 0
|
|
||||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
|
||||||
case StateAbout:
|
|
||||||
m.state = StateAbout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) updatePluginsMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StateMainMenu
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedPluginsMenuItem > 0 {
|
|
||||||
m.selectedPluginsMenuItem--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems)-1 {
|
|
||||||
m.selectedPluginsMenuItem++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems) {
|
|
||||||
selectedAction := m.pluginsMenuItems[m.selectedPluginsMenuItem].Action
|
|
||||||
switch selectedAction {
|
|
||||||
case StatePluginsBrowse:
|
|
||||||
m.state = StatePluginsBrowse
|
|
||||||
m.pluginsLoading = true
|
|
||||||
m.pluginsError = ""
|
|
||||||
m.pluginsList = nil
|
|
||||||
return m, loadPlugins
|
|
||||||
case StatePluginsInstalled:
|
|
||||||
m.state = StatePluginsInstalled
|
|
||||||
m.installedPluginsLoading = true
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
m.installedPluginsList = nil
|
|
||||||
return m, loadInstalledPlugins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updatePluginsBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StatePluginsMenu
|
|
||||||
m.pluginSearchQuery = ""
|
|
||||||
m.filteredPluginsList = m.pluginsList
|
|
||||||
m.selectedPluginIndex = 0
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedPluginIndex > 0 {
|
|
||||||
m.selectedPluginIndex--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedPluginIndex < len(m.filteredPluginsList)-1 {
|
|
||||||
m.selectedPluginIndex++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
|
||||||
m.state = StatePluginDetail
|
|
||||||
}
|
|
||||||
case "/":
|
|
||||||
m.state = StatePluginSearch
|
|
||||||
m.pluginSearchQuery = ""
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updatePluginDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StatePluginsBrowse
|
|
||||||
case "i":
|
|
||||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
|
||||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
|
||||||
installed := m.pluginInstallStatus[plugin.Name]
|
|
||||||
if !installed {
|
|
||||||
return m, installPlugin(plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updatePluginSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StatePluginsBrowse
|
|
||||||
m.pluginSearchQuery = ""
|
|
||||||
m.filteredPluginsList = m.pluginsList
|
|
||||||
m.selectedPluginIndex = 0
|
|
||||||
case "enter":
|
|
||||||
m.state = StatePluginsBrowse
|
|
||||||
m.filterPlugins()
|
|
||||||
case "backspace":
|
|
||||||
if len(m.pluginSearchQuery) > 0 {
|
|
||||||
m.pluginSearchQuery = m.pluginSearchQuery[:len(m.pluginSearchQuery)-1]
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if len(msg.String()) == 1 {
|
|
||||||
m.pluginSearchQuery += msg.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) filterPlugins() {
|
|
||||||
if m.pluginSearchQuery == "" {
|
|
||||||
m.filteredPluginsList = m.pluginsList
|
|
||||||
m.selectedPluginIndex = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPlugins := make([]plugins.Plugin, len(m.pluginsList))
|
|
||||||
for i, p := range m.pluginsList {
|
|
||||||
rawPlugins[i] = plugins.Plugin{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Category: p.Category,
|
|
||||||
Author: p.Author,
|
|
||||||
Description: p.Description,
|
|
||||||
Repo: p.Repo,
|
|
||||||
Path: p.Path,
|
|
||||||
Capabilities: p.Capabilities,
|
|
||||||
Compositors: p.Compositors,
|
|
||||||
Dependencies: p.Dependencies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResults := plugins.FuzzySearch(m.pluginSearchQuery, rawPlugins)
|
|
||||||
searchResults = plugins.SortByFirstParty(searchResults)
|
|
||||||
|
|
||||||
filtered := make([]pluginInfo, len(searchResults))
|
|
||||||
for i, p := range searchResults {
|
|
||||||
filtered[i] = pluginInfo{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Category: p.Category,
|
|
||||||
Author: p.Author,
|
|
||||||
Description: p.Description,
|
|
||||||
Repo: p.Repo,
|
|
||||||
Path: p.Path,
|
|
||||||
Capabilities: p.Capabilities,
|
|
||||||
Compositors: p.Compositors,
|
|
||||||
Dependencies: p.Dependencies,
|
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.filteredPluginsList = filtered
|
|
||||||
m.selectedPluginIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginsLoadedMsg struct {
|
|
||||||
plugins []plugins.Plugin
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPlugins() tea.Msg {
|
|
||||||
registry, err := plugins.NewRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return pluginsLoadedMsg{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginList, err := registry.List()
|
|
||||||
if err != nil {
|
|
||||||
return pluginsLoadedMsg{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginsLoadedMsg{plugins: pluginList}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) updatePluginInstallStatus() {
|
|
||||||
manager, err := plugins.NewManager()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, plugin := range m.pluginsList {
|
|
||||||
p := plugins.Plugin{ID: plugin.ID}
|
|
||||||
installed, err := manager.IsInstalled(p)
|
|
||||||
if err == nil {
|
|
||||||
m.pluginInstallStatus[plugin.Name] = installed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updatePluginsInstalled(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StatePluginsMenu
|
|
||||||
case "up", "k":
|
|
||||||
if m.selectedInstalledIndex > 0 {
|
|
||||||
m.selectedInstalledIndex--
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
if m.selectedInstalledIndex < len(m.installedPluginsList)-1 {
|
|
||||||
m.selectedInstalledIndex++
|
|
||||||
}
|
|
||||||
case "enter", " ":
|
|
||||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
|
||||||
m.state = StatePluginInstalledDetail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc":
|
|
||||||
m.state = StatePluginsInstalled
|
|
||||||
case "u":
|
|
||||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
|
||||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
|
||||||
return m, uninstallPlugin(plugin)
|
|
||||||
}
|
|
||||||
case "p":
|
|
||||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
|
||||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
|
||||||
return m, updatePlugin(plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type installedPluginsLoadedMsg struct {
|
|
||||||
plugins []plugins.Plugin
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginUninstalledMsg struct {
|
|
||||||
pluginName string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginInstalledMsg struct {
|
|
||||||
pluginName string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginUpdatedMsg struct {
|
|
||||||
pluginName string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadInstalledPlugins() tea.Msg {
|
|
||||||
manager, err := plugins.NewManager()
|
|
||||||
if err != nil {
|
|
||||||
return installedPluginsLoadedMsg{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry, err := plugins.NewRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return installedPluginsLoadedMsg{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
installedNames, err := manager.ListInstalled()
|
|
||||||
if err != nil {
|
|
||||||
return installedPluginsLoadedMsg{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
allPlugins, err := registry.List()
|
|
||||||
if err != nil {
|
|
||||||
return installedPluginsLoadedMsg{err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
var installed []plugins.Plugin
|
|
||||||
for _, id := range installedNames {
|
|
||||||
for _, p := range allPlugins {
|
|
||||||
if p.ID == id {
|
|
||||||
installed = append(installed, p)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
installed = plugins.SortByFirstParty(installed)
|
|
||||||
|
|
||||||
return installedPluginsLoadedMsg{plugins: installed}
|
|
||||||
}
|
|
||||||
|
|
||||||
func installPlugin(plugin pluginInfo) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
manager, err := plugins.NewManager()
|
|
||||||
if err != nil {
|
|
||||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := plugins.Plugin{
|
|
||||||
ID: plugin.ID,
|
|
||||||
Name: plugin.Name,
|
|
||||||
Category: plugin.Category,
|
|
||||||
Author: plugin.Author,
|
|
||||||
Description: plugin.Description,
|
|
||||||
Repo: plugin.Repo,
|
|
||||||
Path: plugin.Path,
|
|
||||||
Capabilities: plugin.Capabilities,
|
|
||||||
Compositors: plugin.Compositors,
|
|
||||||
Dependencies: plugin.Dependencies,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.Install(p); err != nil {
|
|
||||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginInstalledMsg{pluginName: plugin.Name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func uninstallPlugin(plugin pluginInfo) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
manager, err := plugins.NewManager()
|
|
||||||
if err != nil {
|
|
||||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := plugins.Plugin{
|
|
||||||
ID: plugin.ID,
|
|
||||||
Name: plugin.Name,
|
|
||||||
Category: plugin.Category,
|
|
||||||
Author: plugin.Author,
|
|
||||||
Description: plugin.Description,
|
|
||||||
Repo: plugin.Repo,
|
|
||||||
Path: plugin.Path,
|
|
||||||
Capabilities: plugin.Capabilities,
|
|
||||||
Compositors: plugin.Compositors,
|
|
||||||
Dependencies: plugin.Dependencies,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.Uninstall(p); err != nil {
|
|
||||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginUninstalledMsg{pluginName: plugin.Name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePlugin(plugin pluginInfo) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
manager, err := plugins.NewManager()
|
|
||||||
if err != nil {
|
|
||||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := plugins.Plugin{
|
|
||||||
ID: plugin.ID,
|
|
||||||
Name: plugin.Name,
|
|
||||||
Category: plugin.Category,
|
|
||||||
Author: plugin.Author,
|
|
||||||
Description: plugin.Description,
|
|
||||||
Repo: plugin.Repo,
|
|
||||||
Path: plugin.Path,
|
|
||||||
Capabilities: plugin.Capabilities,
|
|
||||||
Compositors: plugin.Compositors,
|
|
||||||
Dependencies: plugin.Dependencies,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.Update(p); err != nil {
|
|
||||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginUpdatedMsg{pluginName: plugin.Name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) renderPluginsMenu() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
selectedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA")).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("Plugins"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
for i, item := range m.pluginsMenuItems {
|
|
||||||
if i == m.selectedPluginsMenuItem {
|
|
||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", item.Label)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Select | Esc: Back | q: Quit"))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderPluginsBrowse() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
|
|
||||||
selectedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA")).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("Browse Plugins"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if m.pluginsLoading {
|
|
||||||
b.WriteString(normalStyle.Render("Fetching plugins from registry..."))
|
|
||||||
} else if m.pluginsError != "" {
|
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.pluginsError)))
|
|
||||||
} else if len(m.filteredPluginsList) == 0 {
|
|
||||||
if m.pluginSearchQuery != "" {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf("No plugins match '%s'", m.pluginSearchQuery)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render("No plugins found in registry."))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
installedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
|
|
||||||
for i, plugin := range m.filteredPluginsList {
|
|
||||||
installed := m.pluginInstallStatus[plugin.Name]
|
|
||||||
installMarker := ""
|
|
||||||
if installed {
|
|
||||||
installMarker = " [Installed]"
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == m.selectedPluginIndex {
|
|
||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
|
||||||
if installed {
|
|
||||||
b.WriteString(installedStyle.Render(installMarker))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
|
||||||
if installed {
|
|
||||||
b.WriteString(installedStyle.Render(installMarker))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
|
|
||||||
if m.pluginsLoading || m.pluginsError != "" {
|
|
||||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
|
||||||
} else {
|
|
||||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: View/Install | /: Search | Esc: Back | q: Quit"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderPluginDetail() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
labelStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
|
|
||||||
if m.selectedPluginIndex >= len(m.filteredPluginsList) {
|
|
||||||
return "No plugin selected"
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render(plugin.Name))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("ID: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.ID))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Category: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.Category))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Author: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.Author))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Description:"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
wrapped := wrapText(plugin.Description, 60)
|
|
||||||
b.WriteString(normalStyle.Render(wrapped))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Repository: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if len(plugin.Capabilities) > 0 {
|
|
||||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
|
||||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plugin.Compositors) > 0 {
|
|
||||||
b.WriteString(labelStyle.Render("Compositors: "))
|
|
||||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plugin.Dependencies) > 0 {
|
|
||||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
|
||||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
installed := m.pluginInstallStatus[plugin.Name]
|
|
||||||
if installed {
|
|
||||||
b.WriteString(labelStyle.Render("Status: "))
|
|
||||||
installedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
b.WriteString(installedStyle.Render("Installed"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
|
|
||||||
if installed {
|
|
||||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
|
||||||
} else {
|
|
||||||
b.WriteString(instructionStyle.Render("i: Install | Esc: Back | q: Quit"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderPluginSearch() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("Search Plugins"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("Query: "))
|
|
||||||
b.WriteString(titleStyle.Render(m.pluginSearchQuery + "▌"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("Enter: Search | Esc: Cancel"))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderPluginsInstalled() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
|
|
||||||
selectedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA")).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("Installed Plugins"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if m.installedPluginsLoading {
|
|
||||||
b.WriteString(normalStyle.Render("Loading installed plugins..."))
|
|
||||||
} else if m.installedPluginsError != "" {
|
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
|
||||||
} else if len(m.installedPluginsList) == 0 {
|
|
||||||
b.WriteString(normalStyle.Render("No plugins installed."))
|
|
||||||
} else {
|
|
||||||
for i, plugin := range m.installedPluginsList {
|
|
||||||
if i == m.selectedInstalledIndex {
|
|
||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
|
|
||||||
if m.installedPluginsLoading || m.installedPluginsError != "" {
|
|
||||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
|
||||||
} else {
|
|
||||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Details | Esc: Back | q: Quit"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderPluginInstalledDetail() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
labelStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
|
|
||||||
if m.selectedInstalledIndex >= len(m.installedPluginsList) {
|
|
||||||
return "No plugin selected"
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render(plugin.Name))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("ID: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.ID))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Category: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.Category))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Author: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.Author))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Description:"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
wrapped := wrapText(plugin.Description, 60)
|
|
||||||
b.WriteString(normalStyle.Render(wrapped))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Repository: "))
|
|
||||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if len(plugin.Capabilities) > 0 {
|
|
||||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
|
||||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plugin.Compositors) > 0 {
|
|
||||||
b.WriteString(labelStyle.Render("Compositors: "))
|
|
||||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plugin.Dependencies) > 0 {
|
|
||||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
|
||||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.installedPluginsError != "" {
|
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("u: Uninstall | Esc: Back | q: Quit"))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapText(text string, width int) string {
|
|
||||||
words := strings.Fields(text)
|
|
||||||
if len(words) == 0 {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines []string
|
|
||||||
currentLine := words[0]
|
|
||||||
|
|
||||||
for _, word := range words[1:] {
|
|
||||||
if len(currentLine)+1+len(word) <= width {
|
|
||||||
currentLine += " " + word
|
|
||||||
} else {
|
|
||||||
lines = append(lines, currentLine)
|
|
||||||
currentLine = word
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines = append(lines, currentLine)
|
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) renderMainMenu() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("dms"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
selectedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA")).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
for i, item := range m.menuItems {
|
|
||||||
if i == m.selectedItem {
|
|
||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item.Label)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "↑/↓: Navigate, Enter: Select, q/Esc: Exit"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderShellView() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Shell"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("Opening interactive shell..."))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render("This will launch a shell with DMS environment loaded."))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "Press any key to launch shell, Esc: Back"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderAboutView() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("About DankMaterialShell"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf("DMS Management Interface %s", m.version)))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString(normalStyle.Render("DankMaterialShell is a comprehensive desktop environment"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render("built around Quickshell, providing a modern Material Design"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render("experience for Wayland compositors."))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("Components:"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
if len(m.dependencies) == 0 {
|
|
||||||
b.WriteString(normalStyle.Render("\n Component detection not supported on this platform."))
|
|
||||||
}
|
|
||||||
for _, dep := range m.dependencies {
|
|
||||||
status := "✗"
|
|
||||||
if dep.Status == 1 {
|
|
||||||
status = "✓"
|
|
||||||
}
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s %s", status, dep.Name)))
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "Esc: Back to main menu"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderBanner() string {
|
|
||||||
theme := tui.TerminalTheme()
|
|
||||||
|
|
||||||
logo := `
|
|
||||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
|
||||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
|
||||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
|
||||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
|
||||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
|
||||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color(theme.Primary)).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
return titleStyle.Render(logo)
|
|
||||||
}
|
|
||||||
@@ -1,529 +0,0 @@
|
|||||||
//go:build !distro_binary
|
|
||||||
|
|
||||||
package dms
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) renderUpdateView() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Update Dependencies"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
if len(m.updateDeps) == 0 {
|
|
||||||
b.WriteString("Loading dependencies...\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
categories := m.categorizeDependencies()
|
|
||||||
currentIndex := 0
|
|
||||||
|
|
||||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
|
||||||
deps, exists := categories[category]
|
|
||||||
if !exists || len(deps) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
categoryStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#7060ac")).
|
|
||||||
Bold(true).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
b.WriteString(categoryStyle.Render(category + ":"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
for _, dep := range deps {
|
|
||||||
var statusText, icon, reinstallMarker string
|
|
||||||
var style lipgloss.Style
|
|
||||||
|
|
||||||
if m.updateToggles[dep.Name] {
|
|
||||||
reinstallMarker = "🔄 "
|
|
||||||
if dep.Status == 0 {
|
|
||||||
statusText = "Will be installed"
|
|
||||||
} else {
|
|
||||||
statusText = "Will be upgraded"
|
|
||||||
}
|
|
||||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
|
||||||
} else {
|
|
||||||
switch dep.Status {
|
|
||||||
case 1:
|
|
||||||
icon = "✓"
|
|
||||||
statusText = "Installed"
|
|
||||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
case 0:
|
|
||||||
icon = "○"
|
|
||||||
statusText = "Not installed"
|
|
||||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
|
||||||
case 2:
|
|
||||||
icon = "△"
|
|
||||||
statusText = "Needs update"
|
|
||||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
|
||||||
case 3:
|
|
||||||
icon = "!"
|
|
||||||
statusText = "Needs reinstall"
|
|
||||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line := fmt.Sprintf("%s%s%-25s %s", reinstallMarker, icon, dep.Name, statusText)
|
|
||||||
|
|
||||||
if currentIndex == m.selectedUpdateDep {
|
|
||||||
line = "▶ " + line
|
|
||||||
selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7060ac")).Bold(true)
|
|
||||||
b.WriteString(selectedStyle.Render(line))
|
|
||||||
} else {
|
|
||||||
line = " " + line
|
|
||||||
b.WriteString(style.Render(line))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
currentIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "↑/↓: Navigate, Space: Toggle, Enter: Update Selected, Esc: Back"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderPasswordView() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("Package installation requires sudo privileges."))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
inputStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
maskedPassword := strings.Repeat("*", len(m.passwordInput))
|
|
||||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
if m.passwordError != "" {
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
b.WriteString(errorStyle.Render("✗ " + m.passwordError))
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderProgressView() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Updating Packages"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if !m.updateProgress.complete {
|
|
||||||
progressStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
b.WriteString(progressStyle.Render(m.updateProgress.step))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
progressBar := fmt.Sprintf("[%s%s] %.0f%%",
|
|
||||||
strings.Repeat("█", int(m.updateProgress.progress*30)),
|
|
||||||
strings.Repeat("░", 30-int(m.updateProgress.progress*30)),
|
|
||||||
m.updateProgress.progress*100)
|
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render(progressBar))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
if len(m.updateLogs) > 0 {
|
|
||||||
b.WriteString("\n")
|
|
||||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Live Output:")
|
|
||||||
b.WriteString(logHeader)
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
maxLines := 8
|
|
||||||
startIdx := 0
|
|
||||||
if len(m.updateLogs) > maxLines {
|
|
||||||
startIdx = len(m.updateLogs) - maxLines
|
|
||||||
}
|
|
||||||
|
|
||||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
|
||||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
|
||||||
if m.updateLogs[i] != "" {
|
|
||||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.updateProgress.err != nil {
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Update failed: %v", m.updateProgress.err)))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
if len(m.updateLogs) > 0 {
|
|
||||||
b.WriteString("\n")
|
|
||||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Error Logs:")
|
|
||||||
b.WriteString(logHeader)
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
maxLines := 15
|
|
||||||
startIdx := 0
|
|
||||||
if len(m.updateLogs) > maxLines {
|
|
||||||
startIdx = len(m.updateLogs) - maxLines
|
|
||||||
}
|
|
||||||
|
|
||||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
|
||||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
|
||||||
if m.updateLogs[i] != "" {
|
|
||||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
|
||||||
} else if m.updateProgress.complete {
|
|
||||||
successStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(successStyle.Render("✓ Update complete!"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) getFilteredDeps() []DependencyInfo {
|
|
||||||
categories := m.categorizeDependencies()
|
|
||||||
var filtered []DependencyInfo
|
|
||||||
|
|
||||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
|
||||||
deps, exists := categories[category]
|
|
||||||
if exists {
|
|
||||||
filtered = append(filtered, deps...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) getDepAtVisualIndex(index int) *DependencyInfo {
|
|
||||||
filtered := m.getFilteredDeps()
|
|
||||||
if index >= 0 && index < len(filtered) {
|
|
||||||
return &filtered[index]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderGreeterPasswordView() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("Greeter installation requires sudo privileges."))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
inputStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
maskedPassword := strings.Repeat("*", len(m.greeterPasswordInput))
|
|
||||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
if m.greeterPasswordError != "" {
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
b.WriteString(errorStyle.Render("✗ " + m.greeterPasswordError))
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderGreeterCompositorSelect() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Select Compositor"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("Multiple compositors detected. Choose which one to use for the greeter:"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
selectedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA")).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
for i, comp := range m.greeterCompositors {
|
|
||||||
if i == m.greeterSelectedComp {
|
|
||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", comp)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", comp)))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderGreeterMenu() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Greeter Management"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
greeterMenuItems := []string{"Install Greeter"}
|
|
||||||
|
|
||||||
selectedStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA")).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
for i, item := range greeterMenuItems {
|
|
||||||
if i == m.selectedGreeterItem {
|
|
||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item)))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888")).
|
|
||||||
MarginTop(1)
|
|
||||||
|
|
||||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
|
||||||
b.WriteString(instructionStyle.Render(instructions))
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) renderGreeterInstalling() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF")).
|
|
||||||
Bold(true).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
b.WriteString(headerStyle.Render("Installing Greeter"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if !m.greeterProgress.complete {
|
|
||||||
progressStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
b.WriteString(progressStyle.Render(m.greeterProgress.step))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
if len(m.greeterLogs) > 0 {
|
|
||||||
b.WriteString("\n")
|
|
||||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Output:")
|
|
||||||
b.WriteString(logHeader)
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
maxLines := 10
|
|
||||||
startIdx := 0
|
|
||||||
if len(m.greeterLogs) > maxLines {
|
|
||||||
startIdx = len(m.greeterLogs) - maxLines
|
|
||||||
}
|
|
||||||
|
|
||||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
|
||||||
for i := startIdx; i < len(m.greeterLogs); i++ {
|
|
||||||
if m.greeterLogs[i] != "" {
|
|
||||||
b.WriteString(logStyle.Render(" " + m.greeterLogs[i]))
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.greeterProgress.err != nil {
|
|
||||||
errorStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FF0000"))
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Installation failed: %v", m.greeterProgress.err)))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
|
||||||
} else if m.greeterProgress.complete {
|
|
||||||
successStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#00D4AA"))
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(successStyle.Render("✓ Greeter installation complete!"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
normalStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#FFFFFF"))
|
|
||||||
|
|
||||||
b.WriteString(normalStyle.Render("To test the greeter, run:"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render(" sudo systemctl start greetd"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString(normalStyle.Render("To enable on boot, run:"))
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(normalStyle.Render(" sudo systemctl enable --now greetd"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
instructionStyle := lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#888888"))
|
|
||||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
|
||||||
categories := map[string][]DependencyInfo{
|
|
||||||
"Shell": {},
|
|
||||||
"Shared Components": {},
|
|
||||||
"Hyprland Components": {},
|
|
||||||
"Niri Components": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
excludeList := map[string]bool{
|
|
||||||
"git": true,
|
|
||||||
"polkit-agent": true,
|
|
||||||
"jq": true,
|
|
||||||
"xdg-desktop-portal": true,
|
|
||||||
"xdg-desktop-portal-wlr": true,
|
|
||||||
"xdg-desktop-portal-hyprland": true,
|
|
||||||
"xdg-desktop-portal-gtk": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dep := range m.updateDeps {
|
|
||||||
if excludeList[dep.Name] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dep.Name {
|
|
||||||
case "dms (DankMaterialShell)", "quickshell":
|
|
||||||
categories["Shell"] = append(categories["Shell"], dep)
|
|
||||||
case "hyprland", "hyprctl":
|
|
||||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
|
||||||
case "niri":
|
|
||||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
|
||||||
case "kitty", "alacritty", "ghostty":
|
|
||||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
|
||||||
default:
|
|
||||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
@@ -2,45 +2,93 @@ package providers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HyprlandProvider struct {
|
type HyprlandProvider struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
parsed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHyprlandProvider(configPath string) *HyprlandProvider {
|
func NewHyprlandProvider(configPath string) *HyprlandProvider {
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
configPath = "$HOME/.config/hypr"
|
configPath = defaultHyprlandConfigDir()
|
||||||
}
|
}
|
||||||
return &HyprlandProvider{
|
return &HyprlandProvider{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultHyprlandConfigDir() string {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(configDir, "hypr")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) Name() string {
|
func (h *HyprlandProvider) Name() string {
|
||||||
return "hyprland"
|
return "hyprland"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
section, err := ParseHyprlandKeys(h.configPath)
|
result, err := ParseHyprlandKeysWithDMS(h.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
|
return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
h.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
h.convertSection(section, "", categorizedBinds)
|
h.parsed = true
|
||||||
|
|
||||||
return &keybinds.CheatSheet{
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
Title: "Hyprland Keybinds",
|
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
||||||
Provider: h.Name(),
|
|
||||||
Binds: categorizedBinds,
|
sheet := &keybinds.CheatSheet{
|
||||||
}, nil
|
Title: "Hyprland Keybinds",
|
||||||
|
Provider: h.Name(),
|
||||||
|
Binds: categorizedBinds,
|
||||||
|
DMSBindsIncluded: result.DMSBindsIncluded,
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DMSStatus != nil {
|
||||||
|
sheet.DMSStatus = &keybinds.DMSBindsStatus{
|
||||||
|
Exists: result.DMSStatus.Exists,
|
||||||
|
Included: result.DMSStatus.Included,
|
||||||
|
IncludePosition: result.DMSStatus.IncludePosition,
|
||||||
|
TotalIncludes: result.DMSStatus.TotalIncludes,
|
||||||
|
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
|
||||||
|
Effective: result.DMSStatus.Effective,
|
||||||
|
OverriddenBy: result.DMSStatus.OverriddenBy,
|
||||||
|
StatusMessage: result.DMSStatus.StatusMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sheet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
|
||||||
|
if h.parsed {
|
||||||
|
return h.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseHyprlandKeysWithDMS(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
h.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
h.parsed = true
|
||||||
|
return h.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) {
|
||||||
currentSubcat := subcategory
|
currentSubcat := subcategory
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
currentSubcat = section.Name
|
currentSubcat = section.Name
|
||||||
@@ -48,12 +96,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
|
|||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range section.Keybinds {
|
||||||
category := h.categorizeByDispatcher(kb.Dispatcher)
|
category := h.categorizeByDispatcher(kb.Dispatcher)
|
||||||
bind := h.convertKeybind(&kb, currentSubcat)
|
bind := h.convertKeybind(&kb, currentSubcat, conflicts)
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, child := range section.Children {
|
for _, child := range section.Children {
|
||||||
h.convertSection(&child, currentSubcat, categorizedBinds)
|
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +133,8 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
|
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
|
||||||
key := h.formatKey(kb)
|
keyStr := h.formatKey(kb)
|
||||||
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
|
|
||||||
@@ -94,12 +142,33 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
desc = rawAction
|
desc = rawAction
|
||||||
}
|
}
|
||||||
|
|
||||||
return keybinds.Keybind{
|
source := "config"
|
||||||
Key: key,
|
if strings.Contains(kb.Source, "dms/binds.conf") {
|
||||||
|
source = "dms"
|
||||||
|
}
|
||||||
|
|
||||||
|
bind := keybinds.Keybind{
|
||||||
|
Key: keyStr,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Action: rawAction,
|
Action: rawAction,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
|
Source: source,
|
||||||
|
Flags: kb.Flags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if source == "dms" && conflicts != nil {
|
||||||
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
|
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||||
|
bind.Conflict = &keybinds.Keybind{
|
||||||
|
Key: keyStr,
|
||||||
|
Description: conflictKb.Comment,
|
||||||
|
Action: h.formatRawAction(conflictKb.Dispatcher, conflictKb.Params),
|
||||||
|
Source: "config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
|
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
|
||||||
@@ -115,3 +184,314 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
|||||||
parts = append(parts, kb.Key)
|
parts = append(parts, kb.Key)
|
||||||
return strings.Join(parts, "+")
|
return strings.Join(parts, "+")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) GetOverridePath() string {
|
||||||
|
expanded, err := utils.ExpandPath(h.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return filepath.Join(h.configPath, "dms", "binds.conf")
|
||||||
|
}
|
||||||
|
return filepath.Join(expanded, "dms", "binds.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) validateAction(action string) error {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
switch {
|
||||||
|
case action == "":
|
||||||
|
return fmt.Errorf("action cannot be empty")
|
||||||
|
case action == "exec" || action == "exec ":
|
||||||
|
return fmt.Errorf("exec dispatcher requires arguments")
|
||||||
|
case strings.HasPrefix(action, "exec "):
|
||||||
|
rest := strings.TrimSpace(strings.TrimPrefix(action, "exec "))
|
||||||
|
if rest == "" {
|
||||||
|
return fmt.Errorf("exec dispatcher requires arguments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
|
||||||
|
if err := h.validateAction(action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
overridePath := h.GetOverridePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBinds, err := h.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
existingBinds = make(map[string]*hyprlandOverrideBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract flags from options
|
||||||
|
var flags string
|
||||||
|
if options != nil {
|
||||||
|
if f, ok := options["flags"].(string); ok {
|
||||||
|
flags = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedKey := strings.ToLower(key)
|
||||||
|
existingBinds[normalizedKey] = &hyprlandOverrideBind{
|
||||||
|
Key: key,
|
||||||
|
Action: action,
|
||||||
|
Description: description,
|
||||||
|
Flags: flags,
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) RemoveBind(key string) error {
|
||||||
|
existingBinds, err := h.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedKey := strings.ToLower(key)
|
||||||
|
delete(existingBinds, normalizedKey)
|
||||||
|
return h.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandOverrideBind struct {
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Description string
|
||||||
|
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
||||||
|
Options map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
||||||
|
overridePath := h.GetOverridePath()
|
||||||
|
binds := make(map[string]*hyprlandOverrideBind)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(overridePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(line, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract flags from bind type
|
||||||
|
bindType := strings.TrimSpace(parts[0])
|
||||||
|
flags := extractBindFlags(bindType)
|
||||||
|
hasDescFlag := strings.Contains(flags, "d")
|
||||||
|
|
||||||
|
content := strings.TrimSpace(parts[1])
|
||||||
|
commentParts := strings.SplitN(content, "#", 2)
|
||||||
|
bindContent := strings.TrimSpace(commentParts[0])
|
||||||
|
|
||||||
|
var comment string
|
||||||
|
if len(commentParts) > 1 {
|
||||||
|
comment = strings.TrimSpace(commentParts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// For bindd, format is: mods, key, description, dispatcher, params
|
||||||
|
var minFields, descIndex, dispatcherIndex int
|
||||||
|
if hasDescFlag {
|
||||||
|
minFields = 4
|
||||||
|
descIndex = 2
|
||||||
|
dispatcherIndex = 3
|
||||||
|
} else {
|
||||||
|
minFields = 3
|
||||||
|
dispatcherIndex = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.SplitN(bindContent, ",", minFields+2)
|
||||||
|
if len(fields) < minFields {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mods := strings.TrimSpace(fields[0])
|
||||||
|
keyName := strings.TrimSpace(fields[1])
|
||||||
|
|
||||||
|
var dispatcher, params string
|
||||||
|
if hasDescFlag {
|
||||||
|
if comment == "" {
|
||||||
|
comment = strings.TrimSpace(fields[descIndex])
|
||||||
|
}
|
||||||
|
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
||||||
|
if len(fields) > dispatcherIndex+1 {
|
||||||
|
paramParts := fields[dispatcherIndex+1:]
|
||||||
|
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
||||||
|
if len(fields) > dispatcherIndex+1 {
|
||||||
|
paramParts := fields[dispatcherIndex+1:]
|
||||||
|
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyStr := h.buildKeyString(mods, keyName)
|
||||||
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
|
action := dispatcher
|
||||||
|
if params != "" {
|
||||||
|
action = dispatcher + " " + params
|
||||||
|
}
|
||||||
|
|
||||||
|
binds[normalizedKey] = &hyprlandOverrideBind{
|
||||||
|
Key: keyStr,
|
||||||
|
Action: action,
|
||||||
|
Description: comment,
|
||||||
|
Flags: flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
|
||||||
|
if mods == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
||||||
|
return r == '+' || r == ' '
|
||||||
|
})
|
||||||
|
|
||||||
|
parts := append(modList, key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"):
|
||||||
|
return 0
|
||||||
|
case strings.Contains(action, "workspace"):
|
||||||
|
return 1
|
||||||
|
case strings.Contains(action, "window") || strings.Contains(action, "focus") ||
|
||||||
|
strings.Contains(action, "move") || strings.Contains(action, "swap") ||
|
||||||
|
strings.Contains(action, "resize"):
|
||||||
|
return 2
|
||||||
|
case strings.Contains(action, "monitor"):
|
||||||
|
return 3
|
||||||
|
case strings.HasPrefix(action, "exec"):
|
||||||
|
return 4
|
||||||
|
case action == "exit" || strings.Contains(action, "dpms"):
|
||||||
|
return 5
|
||||||
|
default:
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
||||||
|
overridePath := h.GetOverridePath()
|
||||||
|
content := h.generateBindsContent(binds)
|
||||||
|
return os.WriteFile(overridePath, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
|
||||||
|
if len(binds) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
bindList := make([]*hyprlandOverrideBind, 0, len(binds))
|
||||||
|
for _, bind := range binds {
|
||||||
|
bindList = append(bindList, bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(bindList, func(i, j int) bool {
|
||||||
|
pi, pj := h.getBindSortPriority(bindList[i].Action), h.getBindSortPriority(bindList[j].Action)
|
||||||
|
if pi != pj {
|
||||||
|
return pi < pj
|
||||||
|
}
|
||||||
|
return bindList[i].Key < bindList[j].Key
|
||||||
|
})
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, bind := range bindList {
|
||||||
|
h.writeBindLine(&sb, bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
||||||
|
mods, key := h.parseKeyString(bind.Key)
|
||||||
|
dispatcher, params := h.parseAction(bind.Action)
|
||||||
|
|
||||||
|
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
||||||
|
sb.WriteString("bind")
|
||||||
|
if bind.Flags != "" {
|
||||||
|
sb.WriteString(bind.Flags)
|
||||||
|
}
|
||||||
|
sb.WriteString(" = ")
|
||||||
|
sb.WriteString(mods)
|
||||||
|
sb.WriteString(", ")
|
||||||
|
sb.WriteString(key)
|
||||||
|
sb.WriteString(", ")
|
||||||
|
|
||||||
|
// For bindd (description flag), include description before dispatcher
|
||||||
|
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
|
||||||
|
sb.WriteString(bind.Description)
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(dispatcher)
|
||||||
|
|
||||||
|
if params != "" {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
sb.WriteString(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add comment if not using bindd (which has inline description)
|
||||||
|
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
|
||||||
|
sb.WriteString(" # ")
|
||||||
|
sb.WriteString(bind.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
|
||||||
|
parts := strings.Split(keyStr, "+")
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return "", keyStr
|
||||||
|
case 1:
|
||||||
|
return "", parts[0]
|
||||||
|
default:
|
||||||
|
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
|
||||||
|
parts := strings.SplitN(action, " ", 2)
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return action, ""
|
||||||
|
case 1:
|
||||||
|
dispatcher = parts[0]
|
||||||
|
default:
|
||||||
|
dispatcher = parts[0]
|
||||||
|
params = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal spawn format to Hyprland's exec
|
||||||
|
if dispatcher == "spawn" {
|
||||||
|
dispatcher = "exec"
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatcher, params
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type HyprlandKeyBinding struct {
|
|||||||
Dispatcher string `json:"dispatcher"`
|
Dispatcher string `json:"dispatcher"`
|
||||||
Params string `json:"params"`
|
Params string `json:"params"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Flags string `json:"flags"` // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
||||||
}
|
}
|
||||||
|
|
||||||
type HyprlandSection struct {
|
type HyprlandSection struct {
|
||||||
@@ -32,14 +34,36 @@ type HyprlandSection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HyprlandParser struct {
|
type HyprlandParser struct {
|
||||||
contentLines []string
|
contentLines []string
|
||||||
readingLine int
|
readingLine int
|
||||||
|
configDir string
|
||||||
|
currentSource string
|
||||||
|
dmsBindsExists bool
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
includeCount int
|
||||||
|
dmsIncludePos int
|
||||||
|
bindsAfterDMS int
|
||||||
|
dmsBindKeys map[string]bool
|
||||||
|
configBindKeys map[string]bool
|
||||||
|
conflictingConfigs map[string]*HyprlandKeyBinding
|
||||||
|
bindMap map[string]*HyprlandKeyBinding
|
||||||
|
bindOrder []string
|
||||||
|
processedFiles map[string]bool
|
||||||
|
dmsProcessed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHyprlandParser() *HyprlandParser {
|
func NewHyprlandParser(configDir string) *HyprlandParser {
|
||||||
return &HyprlandParser{
|
return &HyprlandParser{
|
||||||
contentLines: []string{},
|
contentLines: []string{},
|
||||||
readingLine: 0,
|
readingLine: 0,
|
||||||
|
configDir: configDir,
|
||||||
|
dmsIncludePos: -1,
|
||||||
|
dmsBindKeys: make(map[string]bool),
|
||||||
|
configBindKeys: make(map[string]bool),
|
||||||
|
conflictingConfigs: make(map[string]*HyprlandKeyBinding),
|
||||||
|
bindMap: make(map[string]*HyprlandKeyBinding),
|
||||||
|
bindOrder: []string{},
|
||||||
|
processedFiles: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,71 +219,7 @@ func hyprlandAutogenerateComment(dispatcher, params string) string {
|
|||||||
|
|
||||||
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
|
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
|
||||||
line := p.contentLines[lineNumber]
|
line := p.contentLines[lineNumber]
|
||||||
parts := strings.SplitN(line, "=", 2)
|
return p.parseBindLine(line)
|
||||||
if len(parts) < 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := parts[1]
|
|
||||||
keyParts := strings.SplitN(keys, "#", 2)
|
|
||||||
keys = keyParts[0]
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(keyParts) > 1 {
|
|
||||||
comment = strings.TrimSpace(keyParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
keyFields := strings.SplitN(keys, ",", 5)
|
|
||||||
if len(keyFields) < 3 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(keyFields[0])
|
|
||||||
key := strings.TrimSpace(keyFields[1])
|
|
||||||
dispatcher := strings.TrimSpace(keyFields[2])
|
|
||||||
|
|
||||||
var params string
|
|
||||||
if len(keyFields) > 3 {
|
|
||||||
paramParts := keyFields[3:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment != "" {
|
|
||||||
if strings.HasPrefix(comment, HideComment) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
comment = hyprlandAutogenerateComment(dispatcher, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
var modList []string
|
|
||||||
if mods != "" {
|
|
||||||
modstring := mods + string(ModSeparators[0])
|
|
||||||
p := 0
|
|
||||||
for index, char := range modstring {
|
|
||||||
isModSep := false
|
|
||||||
for _, sep := range ModSeparators {
|
|
||||||
if char == sep {
|
|
||||||
isModSep = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isModSep {
|
|
||||||
if index-p > 1 {
|
|
||||||
modList = append(modList, modstring[p:index])
|
|
||||||
}
|
|
||||||
p = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &HyprlandKeyBinding{
|
|
||||||
Mods: modList,
|
|
||||||
Key: key,
|
|
||||||
Dispatcher: dispatcher,
|
|
||||||
Params: params,
|
|
||||||
Comment: comment,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
|
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
|
||||||
@@ -320,9 +280,348 @@ func (p *HyprlandParser) ParseKeys() *HyprlandSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
|
func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
|
||||||
parser := NewHyprlandParser()
|
parser := NewHyprlandParser(path)
|
||||||
if err := parser.ReadContent(path); err != nil {
|
if err := parser.ReadContent(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return parser.ParseKeys(), nil
|
return parser.ParseKeys(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HyprlandParseResult struct {
|
||||||
|
Section *HyprlandSection
|
||||||
|
DMSBindsIncluded bool
|
||||||
|
DMSStatus *HyprlandDMSStatus
|
||||||
|
ConflictingConfigs map[string]*HyprlandKeyBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
type HyprlandDMSStatus struct {
|
||||||
|
Exists bool
|
||||||
|
Included bool
|
||||||
|
IncludePosition int
|
||||||
|
TotalIncludes int
|
||||||
|
BindsAfterDMS int
|
||||||
|
Effective bool
|
||||||
|
OverriddenBy int
|
||||||
|
StatusMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
|
||||||
|
status := &HyprlandDMSStatus{
|
||||||
|
Exists: p.dmsBindsExists,
|
||||||
|
Included: p.dmsBindsIncluded,
|
||||||
|
IncludePosition: p.dmsIncludePos,
|
||||||
|
TotalIncludes: p.includeCount,
|
||||||
|
BindsAfterDMS: p.bindsAfterDMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !p.dmsBindsExists:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/binds.conf does not exist"
|
||||||
|
case !p.dmsBindsIncluded:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/binds.conf is not sourced in config"
|
||||||
|
case p.bindsAfterDMS > 0:
|
||||||
|
status.Effective = true
|
||||||
|
status.OverriddenBy = p.bindsAfterDMS
|
||||||
|
status.StatusMessage = "Some DMS binds may be overridden by config binds"
|
||||||
|
default:
|
||||||
|
status.Effective = true
|
||||||
|
status.StatusMessage = "DMS binds are active"
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) formatBindKey(kb *HyprlandKeyBinding) string {
|
||||||
|
parts := make([]string, 0, len(kb.Mods)+1)
|
||||||
|
parts = append(parts, kb.Mods...)
|
||||||
|
parts = append(parts, kb.Key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) normalizeKey(key string) string {
|
||||||
|
return strings.ToLower(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
|
||||||
|
key := p.formatBindKey(kb)
|
||||||
|
normalizedKey := p.normalizeKey(key)
|
||||||
|
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf")
|
||||||
|
|
||||||
|
if isDMSBind {
|
||||||
|
p.dmsBindKeys[normalizedKey] = true
|
||||||
|
} else if p.dmsBindKeys[normalizedKey] {
|
||||||
|
p.bindsAfterDMS++
|
||||||
|
p.conflictingConfigs[normalizedKey] = kb
|
||||||
|
p.configBindKeys[normalizedKey] = true
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
p.configBindKeys[normalizedKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := p.bindMap[normalizedKey]; !exists {
|
||||||
|
p.bindOrder = append(p.bindOrder, key)
|
||||||
|
}
|
||||||
|
p.bindMap[normalizedKey] = kb
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
||||||
|
expandedDir, err := utils.ExpandPath(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
|
||||||
|
if _, err := os.Stat(dmsBindsPath); err == nil {
|
||||||
|
p.dmsBindsExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(expandedDir, "hyprland.conf")
|
||||||
|
section, err := p.parseFileWithSource(mainConfig, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.dmsBindsExists && !p.dmsProcessed {
|
||||||
|
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.processedFiles[absPath] {
|
||||||
|
return &HyprlandSection{Name: sectionName}, nil
|
||||||
|
}
|
||||||
|
p.processedFiles[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
|
||||||
|
section := &HyprlandSection{Name: sectionName}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "source") {
|
||||||
|
p.handleSource(trimmed, section, filepath.Dir(absPath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := p.parseBindLine(line)
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kb.Source = p.currentSource
|
||||||
|
if p.addBind(kb) {
|
||||||
|
section.Keybinds = append(section.Keybinds, *kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, baseDir string) {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
|
||||||
|
|
||||||
|
p.includeCount++
|
||||||
|
if isDMSSource {
|
||||||
|
p.dmsBindsIncluded = true
|
||||||
|
p.dmsIncludePos = p.includeCount
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
includedSection, err := p.parseFileWithSource(expanded, "")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
section.Children = append(section.Children, *includedSection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
|
||||||
|
data, err := os.ReadFile(dmsBindsPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = dmsBindsPath
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(trimmed, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := p.parseBindLine(line)
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kb.Source = dmsBindsPath
|
||||||
|
if p.addBind(kb) {
|
||||||
|
section.Keybinds = append(section.Keybinds, *kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract bind type and flags from the left side of "="
|
||||||
|
bindType := strings.TrimSpace(parts[0])
|
||||||
|
flags := extractBindFlags(bindType)
|
||||||
|
hasDescFlag := strings.Contains(flags, "d")
|
||||||
|
|
||||||
|
keys := parts[1]
|
||||||
|
keyParts := strings.SplitN(keys, "#", 2)
|
||||||
|
keys = keyParts[0]
|
||||||
|
|
||||||
|
var comment string
|
||||||
|
if len(keyParts) > 1 {
|
||||||
|
comment = strings.TrimSpace(keyParts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// For bindd, the format is: bindd = MODS, key, description, dispatcher, params
|
||||||
|
// For regular binds: bind = MODS, key, dispatcher, params
|
||||||
|
var minFields, descIndex, dispatcherIndex int
|
||||||
|
if hasDescFlag {
|
||||||
|
minFields = 4 // mods, key, description, dispatcher
|
||||||
|
descIndex = 2
|
||||||
|
dispatcherIndex = 3
|
||||||
|
} else {
|
||||||
|
minFields = 3 // mods, key, dispatcher
|
||||||
|
dispatcherIndex = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
keyFields := strings.SplitN(keys, ",", minFields+2) // Allow for params
|
||||||
|
if len(keyFields) < minFields {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mods := strings.TrimSpace(keyFields[0])
|
||||||
|
key := strings.TrimSpace(keyFields[1])
|
||||||
|
|
||||||
|
var dispatcher, params string
|
||||||
|
if hasDescFlag {
|
||||||
|
// bindd format: description is in the bind itself
|
||||||
|
if comment == "" {
|
||||||
|
comment = strings.TrimSpace(keyFields[descIndex])
|
||||||
|
}
|
||||||
|
dispatcher = strings.TrimSpace(keyFields[dispatcherIndex])
|
||||||
|
if len(keyFields) > dispatcherIndex+1 {
|
||||||
|
paramParts := keyFields[dispatcherIndex+1:]
|
||||||
|
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatcher = strings.TrimSpace(keyFields[dispatcherIndex])
|
||||||
|
if len(keyFields) > dispatcherIndex+1 {
|
||||||
|
paramParts := keyFields[dispatcherIndex+1:]
|
||||||
|
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment != "" && strings.HasPrefix(comment, HideComment) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment == "" {
|
||||||
|
comment = hyprlandAutogenerateComment(dispatcher, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
var modList []string
|
||||||
|
if mods != "" {
|
||||||
|
modstring := mods + string(ModSeparators[0])
|
||||||
|
idx := 0
|
||||||
|
for index, char := range modstring {
|
||||||
|
isModSep := false
|
||||||
|
for _, sep := range ModSeparators {
|
||||||
|
if char == sep {
|
||||||
|
isModSep = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isModSep {
|
||||||
|
if index-idx > 1 {
|
||||||
|
modList = append(modList, modstring[idx:index])
|
||||||
|
}
|
||||||
|
idx = index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HyprlandKeyBinding{
|
||||||
|
Mods: modList,
|
||||||
|
Key: key,
|
||||||
|
Dispatcher: dispatcher,
|
||||||
|
Params: params,
|
||||||
|
Comment: comment,
|
||||||
|
Flags: flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractBindFlags extracts the flags from a bind type string
|
||||||
|
// e.g., "binde" -> "e", "bindel" -> "el", "bindd" -> "d"
|
||||||
|
func extractBindFlags(bindType string) string {
|
||||||
|
bindType = strings.TrimSpace(bindType)
|
||||||
|
if !strings.HasPrefix(bindType, "bind") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return bindType[4:] // Everything after "bind"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
|
||||||
|
parser := NewHyprlandParser(path)
|
||||||
|
section, err := parser.ParseWithDMS()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HyprlandParseResult{
|
||||||
|
Section: section,
|
||||||
|
DMSBindsIncluded: parser.dmsBindsIncluded,
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func TestHyprlandGetKeybindAtLine(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parser := NewHyprlandParser()
|
parser := NewHyprlandParser("")
|
||||||
parser.contentLines = []string{tt.line}
|
parser.contentLines = []string{tt.line}
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewHyprlandParser()
|
parser := NewHyprlandParser("")
|
||||||
if err := parser.ReadContent(tmpDir); err != nil {
|
if err := parser.ReadContent(tmpDir); err != nil {
|
||||||
t.Fatalf("ReadContent failed: %v", err)
|
t.Fatalf("ReadContent failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -343,7 +343,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
t.Skip("Cannot create relative path")
|
t.Skip("Cannot create relative path")
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewHyprlandParser()
|
parser := NewHyprlandParser("")
|
||||||
tildePathMatch := "~/" + relPath
|
tildePathMatch := "~/" + relPath
|
||||||
err = parser.ReadContent(tildePathMatch)
|
err = parser.ReadContent(tildePathMatch)
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
|
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
|
||||||
parser := NewHyprlandParser()
|
parser := NewHyprlandParser("")
|
||||||
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
|
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
|
||||||
|
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
@@ -394,3 +394,126 @@ bind = SUPER, T, exec, kitty
|
|||||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractBindFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
bindType string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"bind", ""},
|
||||||
|
{"binde", "e"},
|
||||||
|
{"bindl", "l"},
|
||||||
|
{"bindr", "r"},
|
||||||
|
{"bindd", "d"},
|
||||||
|
{"bindo", "o"},
|
||||||
|
{"bindel", "el"},
|
||||||
|
{"bindler", "ler"},
|
||||||
|
{"bindem", "em"},
|
||||||
|
{" bind ", ""},
|
||||||
|
{" binde ", "e"},
|
||||||
|
{"notbind", ""},
|
||||||
|
{"", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.bindType, func(t *testing.T) {
|
||||||
|
result := extractBindFlags(tt.bindType)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("extractBindFlags(%q) = %q, want %q", tt.bindType, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandBindFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
expectedFlags string
|
||||||
|
expectedKey string
|
||||||
|
expectedDisp string
|
||||||
|
expectedDesc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "regular bind",
|
||||||
|
line: "bind = SUPER, Q, killactive",
|
||||||
|
expectedFlags: "",
|
||||||
|
expectedKey: "Q",
|
||||||
|
expectedDisp: "killactive",
|
||||||
|
expectedDesc: "Close window",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binde (repeat on hold)",
|
||||||
|
line: "binde = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
expectedFlags: "e",
|
||||||
|
expectedKey: "XF86AudioRaiseVolume",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindl (locked/inhibitor bypass)",
|
||||||
|
line: "bindl = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
|
||||||
|
expectedFlags: "l",
|
||||||
|
expectedKey: "XF86AudioLowerVolume",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindr (release trigger)",
|
||||||
|
line: "bindr = SUPER, SUPER_L, exec, pkill wofi || wofi",
|
||||||
|
expectedFlags: "r",
|
||||||
|
expectedKey: "SUPER_L",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "pkill wofi || wofi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindd (description)",
|
||||||
|
line: "bindd = SUPER, Q, Open my favourite terminal, exec, kitty",
|
||||||
|
expectedFlags: "d",
|
||||||
|
expectedKey: "Q",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "Open my favourite terminal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindo (long press)",
|
||||||
|
line: "bindo = SUPER, XF86AudioNext, exec, playerctl next",
|
||||||
|
expectedFlags: "o",
|
||||||
|
expectedKey: "XF86AudioNext",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "playerctl next",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindel (combined flags)",
|
||||||
|
line: "bindel = , XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
expectedFlags: "el",
|
||||||
|
expectedKey: "XF86AudioRaiseVolume",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
parser := NewHyprlandParser("")
|
||||||
|
parser.contentLines = []string{tt.line}
|
||||||
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("Expected keybind, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Flags != tt.expectedFlags {
|
||||||
|
t.Errorf("Flags = %q, want %q", result.Flags, tt.expectedFlags)
|
||||||
|
}
|
||||||
|
if result.Key != tt.expectedKey {
|
||||||
|
t.Errorf("Key = %q, want %q", result.Key, tt.expectedKey)
|
||||||
|
}
|
||||||
|
if result.Dispatcher != tt.expectedDisp {
|
||||||
|
t.Errorf("Dispatcher = %q, want %q", result.Dispatcher, tt.expectedDisp)
|
||||||
|
}
|
||||||
|
if result.Comment != tt.expectedDesc {
|
||||||
|
t.Errorf("Comment = %q, want %q", result.Comment, tt.expectedDesc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,35 +7,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewHyprlandProvider(t *testing.T) {
|
func TestNewHyprlandProvider(t *testing.T) {
|
||||||
tests := []struct {
|
t.Run("custom path", func(t *testing.T) {
|
||||||
name string
|
p := NewHyprlandProvider("/custom/path")
|
||||||
configPath string
|
if p == nil {
|
||||||
wantPath string
|
t.Fatal("NewHyprlandProvider returned nil")
|
||||||
}{
|
}
|
||||||
{
|
if p.configPath != "/custom/path" {
|
||||||
name: "custom path",
|
t.Errorf("configPath = %q, want %q", p.configPath, "/custom/path")
|
||||||
configPath: "/custom/path",
|
}
|
||||||
wantPath: "/custom/path",
|
})
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty path defaults",
|
|
||||||
configPath: "",
|
|
||||||
wantPath: "$HOME/.config/hypr",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
t.Run("empty path defaults", func(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
p := NewHyprlandProvider("")
|
||||||
p := NewHyprlandProvider(tt.configPath)
|
if p == nil {
|
||||||
if p == nil {
|
t.Fatal("NewHyprlandProvider returned nil")
|
||||||
t.Fatal("NewHyprlandProvider returned nil")
|
}
|
||||||
}
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
if p.configPath != tt.wantPath {
|
t.Fatalf("UserConfigDir failed: %v", err)
|
||||||
t.Errorf("configPath = %q, want %q", p.configPath, tt.wantPath)
|
}
|
||||||
}
|
expected := filepath.Join(configDir, "hypr")
|
||||||
})
|
if p.configPath != expected {
|
||||||
}
|
t.Errorf("configPath = %q, want %q", p.configPath, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandProviderName(t *testing.T) {
|
func TestHyprlandProviderName(t *testing.T) {
|
||||||
@@ -109,7 +104,7 @@ func TestHyprlandProviderGetCheatSheetError(t *testing.T) {
|
|||||||
|
|
||||||
func TestFormatKey(t *testing.T) {
|
func TestFormatKey(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "test.conf")
|
configFile := filepath.Join(tmpDir, "hyprland.conf")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -163,7 +158,7 @@ func TestFormatKey(t *testing.T) {
|
|||||||
|
|
||||||
func TestDescriptionFallback(t *testing.T) {
|
func TestDescriptionFallback(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "test.conf")
|
configFile := filepath.Join(tmpDir, "hyprland.conf")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -2,46 +2,94 @@ package providers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MangoWCProvider struct {
|
type MangoWCProvider struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
parsed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMangoWCProvider(configPath string) *MangoWCProvider {
|
func NewMangoWCProvider(configPath string) *MangoWCProvider {
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
configPath = "$HOME/.config/mango"
|
configPath = defaultMangoWCConfigDir()
|
||||||
}
|
}
|
||||||
return &MangoWCProvider{
|
return &MangoWCProvider{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultMangoWCConfigDir() string {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(configDir, "mango")
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) Name() string {
|
func (m *MangoWCProvider) Name() string {
|
||||||
return "mangowc"
|
return "mangowc"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
keybinds_list, err := ParseMangoWCKeys(m.configPath)
|
result, err := ParseMangoWCKeysWithDMS(m.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
|
return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
m.parsed = true
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
for _, kb := range keybinds_list {
|
for _, kb := range result.Keybinds {
|
||||||
category := m.categorizeByCommand(kb.Command)
|
category := m.categorizeByCommand(kb.Command)
|
||||||
bind := m.convertKeybind(&kb)
|
bind := m.convertKeybind(&kb, result.ConflictingConfigs)
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &keybinds.CheatSheet{
|
sheet := &keybinds.CheatSheet{
|
||||||
Title: "MangoWC Keybinds",
|
Title: "MangoWC Keybinds",
|
||||||
Provider: m.Name(),
|
Provider: m.Name(),
|
||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
}, nil
|
DMSBindsIncluded: result.DMSBindsIncluded,
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DMSStatus != nil {
|
||||||
|
sheet.DMSStatus = &keybinds.DMSBindsStatus{
|
||||||
|
Exists: result.DMSStatus.Exists,
|
||||||
|
Included: result.DMSStatus.Included,
|
||||||
|
IncludePosition: result.DMSStatus.IncludePosition,
|
||||||
|
TotalIncludes: result.DMSStatus.TotalIncludes,
|
||||||
|
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
|
||||||
|
Effective: result.DMSStatus.Effective,
|
||||||
|
OverriddenBy: result.DMSStatus.OverriddenBy,
|
||||||
|
StatusMessage: result.DMSStatus.StatusMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sheet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) HasDMSBindsIncluded() bool {
|
||||||
|
if m.parsed {
|
||||||
|
return m.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseMangoWCKeysWithDMS(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
m.parsed = true
|
||||||
|
return m.dmsBindsIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
||||||
@@ -82,8 +130,8 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
|
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[string]*MangoWCKeyBinding) keybinds.Keybind {
|
||||||
key := m.formatKey(kb)
|
keyStr := m.formatKey(kb)
|
||||||
rawAction := m.formatRawAction(kb.Command, kb.Params)
|
rawAction := m.formatRawAction(kb.Command, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
|
|
||||||
@@ -91,11 +139,31 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind
|
|||||||
desc = rawAction
|
desc = rawAction
|
||||||
}
|
}
|
||||||
|
|
||||||
return keybinds.Keybind{
|
source := "config"
|
||||||
Key: key,
|
if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
|
||||||
|
source = "dms"
|
||||||
|
}
|
||||||
|
|
||||||
|
bind := keybinds.Keybind{
|
||||||
|
Key: keyStr,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Action: rawAction,
|
Action: rawAction,
|
||||||
|
Source: source,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if source == "dms" && conflicts != nil {
|
||||||
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
|
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||||
|
bind.Conflict = &keybinds.Keybind{
|
||||||
|
Key: keyStr,
|
||||||
|
Description: conflictKb.Comment,
|
||||||
|
Action: m.formatRawAction(conflictKb.Command, conflictKb.Params),
|
||||||
|
Source: "config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) formatRawAction(command, params string) string {
|
func (m *MangoWCProvider) formatRawAction(command, params string) string {
|
||||||
@@ -111,3 +179,264 @@ func (m *MangoWCProvider) formatKey(kb *MangoWCKeyBinding) string {
|
|||||||
parts = append(parts, kb.Key)
|
parts = append(parts, kb.Key)
|
||||||
return strings.Join(parts, "+")
|
return strings.Join(parts, "+")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) GetOverridePath() string {
|
||||||
|
expanded, err := utils.ExpandPath(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return filepath.Join(m.configPath, "dms", "binds.conf")
|
||||||
|
}
|
||||||
|
return filepath.Join(expanded, "dms", "binds.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) validateAction(action string) error {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
switch {
|
||||||
|
case action == "":
|
||||||
|
return fmt.Errorf("action cannot be empty")
|
||||||
|
case action == "spawn" || action == "spawn ":
|
||||||
|
return fmt.Errorf("spawn command requires arguments")
|
||||||
|
case action == "spawn_shell" || action == "spawn_shell ":
|
||||||
|
return fmt.Errorf("spawn_shell command requires arguments")
|
||||||
|
case strings.HasPrefix(action, "spawn "):
|
||||||
|
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
|
||||||
|
if rest == "" {
|
||||||
|
return fmt.Errorf("spawn command requires arguments")
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(action, "spawn_shell "):
|
||||||
|
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn_shell "))
|
||||||
|
if rest == "" {
|
||||||
|
return fmt.Errorf("spawn_shell command requires arguments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) SetBind(key, action, description string, options map[string]any) error {
|
||||||
|
if err := m.validateAction(action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
overridePath := m.GetOverridePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBinds, err := m.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
existingBinds = make(map[string]*mangowcOverrideBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedKey := strings.ToLower(key)
|
||||||
|
existingBinds[normalizedKey] = &mangowcOverrideBind{
|
||||||
|
Key: key,
|
||||||
|
Action: action,
|
||||||
|
Description: description,
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) RemoveBind(key string) error {
|
||||||
|
existingBinds, err := m.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedKey := strings.ToLower(key)
|
||||||
|
delete(existingBinds, normalizedKey)
|
||||||
|
return m.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mangowcOverrideBind struct {
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Description string
|
||||||
|
Options map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
|
||||||
|
overridePath := m.GetOverridePath()
|
||||||
|
binds := make(map[string]*mangowcOverrideBind)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(overridePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(line, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content := strings.TrimSpace(parts[1])
|
||||||
|
commentParts := strings.SplitN(content, "#", 2)
|
||||||
|
bindContent := strings.TrimSpace(commentParts[0])
|
||||||
|
|
||||||
|
var comment string
|
||||||
|
if len(commentParts) > 1 {
|
||||||
|
comment = strings.TrimSpace(commentParts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.SplitN(bindContent, ",", 4)
|
||||||
|
if len(fields) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mods := strings.TrimSpace(fields[0])
|
||||||
|
keyName := strings.TrimSpace(fields[1])
|
||||||
|
command := strings.TrimSpace(fields[2])
|
||||||
|
|
||||||
|
var params string
|
||||||
|
if len(fields) > 3 {
|
||||||
|
params = strings.TrimSpace(fields[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
keyStr := m.buildKeyString(mods, keyName)
|
||||||
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
|
action := command
|
||||||
|
if params != "" {
|
||||||
|
action = command + " " + params
|
||||||
|
}
|
||||||
|
|
||||||
|
binds[normalizedKey] = &mangowcOverrideBind{
|
||||||
|
Key: keyStr,
|
||||||
|
Action: action,
|
||||||
|
Description: comment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
|
||||||
|
if mods == "" || strings.EqualFold(mods, "none") {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
||||||
|
return r == '+' || r == ' '
|
||||||
|
})
|
||||||
|
|
||||||
|
parts := append(modList, key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
|
||||||
|
return 0
|
||||||
|
case strings.Contains(action, "view") || strings.Contains(action, "tag"):
|
||||||
|
return 1
|
||||||
|
case strings.Contains(action, "focus") || strings.Contains(action, "exchange") ||
|
||||||
|
strings.Contains(action, "resize") || strings.Contains(action, "move"):
|
||||||
|
return 2
|
||||||
|
case strings.Contains(action, "mon"):
|
||||||
|
return 3
|
||||||
|
case strings.HasPrefix(action, "spawn"):
|
||||||
|
return 4
|
||||||
|
case action == "quit" || action == "reload_config":
|
||||||
|
return 5
|
||||||
|
default:
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
||||||
|
overridePath := m.GetOverridePath()
|
||||||
|
content := m.generateBindsContent(binds)
|
||||||
|
return os.WriteFile(overridePath, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
||||||
|
if len(binds) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
bindList := make([]*mangowcOverrideBind, 0, len(binds))
|
||||||
|
for _, bind := range binds {
|
||||||
|
bindList = append(bindList, bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(bindList, func(i, j int) bool {
|
||||||
|
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
|
||||||
|
if pi != pj {
|
||||||
|
return pi < pj
|
||||||
|
}
|
||||||
|
return bindList[i].Key < bindList[j].Key
|
||||||
|
})
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, bind := range bindList {
|
||||||
|
m.writeBindLine(&sb, bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
|
||||||
|
mods, key := m.parseKeyString(bind.Key)
|
||||||
|
command, params := m.parseAction(bind.Action)
|
||||||
|
|
||||||
|
sb.WriteString("bind=")
|
||||||
|
if mods == "" {
|
||||||
|
sb.WriteString("none")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(mods)
|
||||||
|
}
|
||||||
|
sb.WriteString(",")
|
||||||
|
sb.WriteString(key)
|
||||||
|
sb.WriteString(",")
|
||||||
|
sb.WriteString(command)
|
||||||
|
|
||||||
|
if params != "" {
|
||||||
|
sb.WriteString(",")
|
||||||
|
sb.WriteString(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bind.Description != "" {
|
||||||
|
sb.WriteString(" # ")
|
||||||
|
sb.WriteString(bind.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
|
||||||
|
parts := strings.Split(keyStr, "+")
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return "", keyStr
|
||||||
|
case 1:
|
||||||
|
return "", parts[0]
|
||||||
|
default:
|
||||||
|
return strings.Join(parts[:len(parts)-1], "+"), parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) parseAction(action string) (command, params string) {
|
||||||
|
parts := strings.SplitN(action, " ", 2)
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return action, ""
|
||||||
|
case 1:
|
||||||
|
return parts[0], ""
|
||||||
|
default:
|
||||||
|
return parts[0], parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,17 +21,40 @@ type MangoWCKeyBinding struct {
|
|||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Params string `json:"params"`
|
Params string `json:"params"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
|
Source string `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MangoWCParser struct {
|
type MangoWCParser struct {
|
||||||
contentLines []string
|
contentLines []string
|
||||||
readingLine int
|
readingLine int
|
||||||
|
configDir string
|
||||||
|
currentSource string
|
||||||
|
dmsBindsExists bool
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
includeCount int
|
||||||
|
dmsIncludePos int
|
||||||
|
bindsAfterDMS int
|
||||||
|
dmsBindKeys map[string]bool
|
||||||
|
configBindKeys map[string]bool
|
||||||
|
conflictingConfigs map[string]*MangoWCKeyBinding
|
||||||
|
bindMap map[string]*MangoWCKeyBinding
|
||||||
|
bindOrder []string
|
||||||
|
processedFiles map[string]bool
|
||||||
|
dmsProcessed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMangoWCParser() *MangoWCParser {
|
func NewMangoWCParser(configDir string) *MangoWCParser {
|
||||||
return &MangoWCParser{
|
return &MangoWCParser{
|
||||||
contentLines: []string{},
|
contentLines: []string{},
|
||||||
readingLine: 0,
|
readingLine: 0,
|
||||||
|
configDir: configDir,
|
||||||
|
dmsIncludePos: -1,
|
||||||
|
dmsBindKeys: make(map[string]bool),
|
||||||
|
configBindKeys: make(map[string]bool),
|
||||||
|
conflictingConfigs: make(map[string]*MangoWCKeyBinding),
|
||||||
|
bindMap: make(map[string]*MangoWCKeyBinding),
|
||||||
|
bindOrder: []string{},
|
||||||
|
processedFiles: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,9 +317,297 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
|
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
|
||||||
parser := NewMangoWCParser()
|
parser := NewMangoWCParser(path)
|
||||||
if err := parser.ReadContent(path); err != nil {
|
if err := parser.ReadContent(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return parser.ParseKeys(), nil
|
return parser.ParseKeys(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MangoWCParseResult struct {
|
||||||
|
Keybinds []MangoWCKeyBinding
|
||||||
|
DMSBindsIncluded bool
|
||||||
|
DMSStatus *MangoWCDMSStatus
|
||||||
|
ConflictingConfigs map[string]*MangoWCKeyBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
type MangoWCDMSStatus struct {
|
||||||
|
Exists bool
|
||||||
|
Included bool
|
||||||
|
IncludePosition int
|
||||||
|
TotalIncludes int
|
||||||
|
BindsAfterDMS int
|
||||||
|
Effective bool
|
||||||
|
OverriddenBy int
|
||||||
|
StatusMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) buildDMSStatus() *MangoWCDMSStatus {
|
||||||
|
status := &MangoWCDMSStatus{
|
||||||
|
Exists: p.dmsBindsExists,
|
||||||
|
Included: p.dmsBindsIncluded,
|
||||||
|
IncludePosition: p.dmsIncludePos,
|
||||||
|
TotalIncludes: p.includeCount,
|
||||||
|
BindsAfterDMS: p.bindsAfterDMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !p.dmsBindsExists:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/binds.conf does not exist"
|
||||||
|
case !p.dmsBindsIncluded:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/binds.conf is not sourced in config"
|
||||||
|
case p.bindsAfterDMS > 0:
|
||||||
|
status.Effective = true
|
||||||
|
status.OverriddenBy = p.bindsAfterDMS
|
||||||
|
status.StatusMessage = "Some DMS binds may be overridden by config binds"
|
||||||
|
default:
|
||||||
|
status.Effective = true
|
||||||
|
status.StatusMessage = "DMS binds are active"
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) formatBindKey(kb *MangoWCKeyBinding) string {
|
||||||
|
parts := make([]string, 0, len(kb.Mods)+1)
|
||||||
|
parts = append(parts, kb.Mods...)
|
||||||
|
parts = append(parts, kb.Key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) normalizeKey(key string) string {
|
||||||
|
return strings.ToLower(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) addBind(kb *MangoWCKeyBinding) {
|
||||||
|
key := p.formatBindKey(kb)
|
||||||
|
normalizedKey := p.normalizeKey(key)
|
||||||
|
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(os.PathSeparator)+"binds.conf")
|
||||||
|
|
||||||
|
if isDMSBind {
|
||||||
|
p.dmsBindKeys[normalizedKey] = true
|
||||||
|
} else if p.dmsBindKeys[normalizedKey] {
|
||||||
|
p.bindsAfterDMS++
|
||||||
|
p.conflictingConfigs[normalizedKey] = kb
|
||||||
|
p.configBindKeys[normalizedKey] = true
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
p.configBindKeys[normalizedKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := p.bindMap[normalizedKey]; !exists {
|
||||||
|
p.bindOrder = append(p.bindOrder, key)
|
||||||
|
}
|
||||||
|
p.bindMap[normalizedKey] = kb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) ParseWithDMS() ([]MangoWCKeyBinding, error) {
|
||||||
|
expandedDir, err := utils.ExpandPath(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
|
||||||
|
if _, err := os.Stat(dmsBindsPath); err == nil {
|
||||||
|
p.dmsBindsExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(expandedDir, "config.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
mainConfig = filepath.Join(expandedDir, "mango.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.parseFileWithSource(mainConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.dmsBindsExists && !p.dmsProcessed {
|
||||||
|
p.parseDMSBindsDirectly(dmsBindsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybinds []MangoWCKeyBinding
|
||||||
|
for _, key := range p.bindOrder {
|
||||||
|
normalizedKey := p.normalizeKey(key)
|
||||||
|
if kb, exists := p.bindMap[normalizedKey]; exists {
|
||||||
|
keybinds = append(keybinds, *kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keybinds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBinding, error) {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.processedFiles[absPath] {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
p.processedFiles[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
|
||||||
|
var keybinds []MangoWCKeyBinding
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for lineNum, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "source") {
|
||||||
|
p.handleSource(trimmed, filepath.Dir(absPath), &keybinds)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := p.getKeybindAtLineContent(line, lineNum)
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kb.Source = p.currentSource
|
||||||
|
p.addBind(kb)
|
||||||
|
keybinds = append(keybinds, *kb)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
return keybinds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKeyBinding) {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
isDMSSource := sourcePath == "dms/binds.conf" || sourcePath == "./dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
|
||||||
|
|
||||||
|
p.includeCount++
|
||||||
|
if isDMSSource {
|
||||||
|
p.dmsBindsIncluded = true
|
||||||
|
p.dmsIncludePos = p.includeCount
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := expanded
|
||||||
|
if !filepath.IsAbs(expanded) {
|
||||||
|
fullPath = filepath.Join(baseDir, expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
includedBinds, err := p.parseFileWithSource(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*keybinds = append(*keybinds, includedBinds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
||||||
|
keybinds, err := p.parseFileWithSource(dmsBindsPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.dmsProcessed = true
|
||||||
|
return keybinds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyBinding {
|
||||||
|
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
||||||
|
matches := bindMatch.FindStringSubmatch(line)
|
||||||
|
if len(matches) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content := matches[2]
|
||||||
|
parts := strings.SplitN(content, "#", 2)
|
||||||
|
keys := parts[0]
|
||||||
|
|
||||||
|
var comment string
|
||||||
|
if len(parts) > 1 {
|
||||||
|
comment = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(comment, MangoWCHideComment) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keyFields := strings.SplitN(keys, ",", 4)
|
||||||
|
if len(keyFields) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mods := strings.TrimSpace(keyFields[0])
|
||||||
|
key := strings.TrimSpace(keyFields[1])
|
||||||
|
command := strings.TrimSpace(keyFields[2])
|
||||||
|
|
||||||
|
var params string
|
||||||
|
if len(keyFields) > 3 {
|
||||||
|
params = strings.TrimSpace(keyFields[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment == "" {
|
||||||
|
comment = mangowcAutogenerateComment(command, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
var modList []string
|
||||||
|
if mods != "" && !strings.EqualFold(mods, "none") {
|
||||||
|
modstring := mods + string(MangoWCModSeparators[0])
|
||||||
|
idx := 0
|
||||||
|
for index, char := range modstring {
|
||||||
|
isModSep := false
|
||||||
|
for _, sep := range MangoWCModSeparators {
|
||||||
|
if char == sep {
|
||||||
|
isModSep = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isModSep {
|
||||||
|
if index-idx > 1 {
|
||||||
|
modList = append(modList, modstring[idx:index])
|
||||||
|
}
|
||||||
|
idx = index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MangoWCKeyBinding{
|
||||||
|
Mods: modList,
|
||||||
|
Key: key,
|
||||||
|
Command: command,
|
||||||
|
Params: params,
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMangoWCKeysWithDMS(path string) (*MangoWCParseResult, error) {
|
||||||
|
parser := NewMangoWCParser(path)
|
||||||
|
keybinds, err := parser.ParseWithDMS()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MangoWCParseResult{
|
||||||
|
Keybinds: keybinds,
|
||||||
|
DMSBindsIncluded: parser.dmsBindsIncluded,
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parser := NewMangoWCParser()
|
parser := NewMangoWCParser("")
|
||||||
parser.contentLines = []string{tt.line}
|
parser.contentLines = []string{tt.line}
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewMangoWCParser()
|
parser := NewMangoWCParser("")
|
||||||
if err := parser.ReadContent(tmpDir); err != nil {
|
if err := parser.ReadContent(tmpDir); err != nil {
|
||||||
t.Fatalf("ReadContent failed: %v", err)
|
t.Fatalf("ReadContent failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -304,7 +304,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write config: %v", err)
|
t.Fatalf("Failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewMangoWCParser()
|
parser := NewMangoWCParser("")
|
||||||
if err := parser.ReadContent(configFile); err != nil {
|
if err := parser.ReadContent(configFile); err != nil {
|
||||||
t.Fatalf("ReadContent failed: %v", err)
|
t.Fatalf("ReadContent failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
t.Skip("Cannot create relative path")
|
t.Skip("Cannot create relative path")
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewMangoWCParser()
|
parser := NewMangoWCParser("")
|
||||||
tildePathMatch := "~/" + relPath
|
tildePathMatch := "~/" + relPath
|
||||||
err = parser.ReadContent(tildePathMatch)
|
err = parser.ReadContent(tildePathMatch)
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ func TestMangoWCInvalidBindLines(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parser := NewMangoWCParser()
|
parser := NewMangoWCParser("")
|
||||||
parser.contentLines = []string{tt.line}
|
parser.contentLines = []string{tt.line}
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,17 @@ func TestMangoWCProviderName(t *testing.T) {
|
|||||||
|
|
||||||
func TestMangoWCProviderDefaultPath(t *testing.T) {
|
func TestMangoWCProviderDefaultPath(t *testing.T) {
|
||||||
provider := NewMangoWCProvider("")
|
provider := NewMangoWCProvider("")
|
||||||
if provider.configPath != "$HOME/.config/mango" {
|
configDir, err := os.UserConfigDir()
|
||||||
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/mango")
|
if err != nil {
|
||||||
|
// Fall back to testing for non-empty path
|
||||||
|
if provider.configPath == "" {
|
||||||
|
t.Error("configPath should not be empty")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expected := filepath.Join(configDir, "mango")
|
||||||
|
if provider.configPath != expected {
|
||||||
|
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +183,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
|
|||||||
provider := NewMangoWCProvider("")
|
provider := NewMangoWCProvider("")
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := provider.convertKeybind(tt.keybind)
|
result := provider.convertKeybind(tt.keybind, nil)
|
||||||
if result.Key != tt.wantKey {
|
if result.Key != tt.wantKey {
|
||||||
t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey)
|
t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,15 @@ func (n *NiriProvider) formatRawAction(action string, args []string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return action + " " + strings.Join(args, " ")
|
quotedArgs := make([]string, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
if arg == "" {
|
||||||
|
quotedArgs[i] = `""`
|
||||||
|
} else {
|
||||||
|
quotedArgs[i] = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action + " " + strings.Join(quotedArgs, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
||||||
@@ -293,9 +301,15 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keyStr := parser.formatBindKey(kb)
|
keyStr := parser.formatBindKey(kb)
|
||||||
|
|
||||||
|
action := n.buildActionFromNode(child)
|
||||||
|
if action == "" {
|
||||||
|
action = n.formatRawAction(kb.Action, kb.Args)
|
||||||
|
}
|
||||||
|
|
||||||
binds[keyStr] = &overrideBind{
|
binds[keyStr] = &overrideBind{
|
||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
Action: n.formatRawAction(kb.Action, kb.Args),
|
Action: action,
|
||||||
Description: kb.Description,
|
Description: kb.Description,
|
||||||
Options: n.extractOptions(child),
|
Options: n.extractOptions(child),
|
||||||
}
|
}
|
||||||
@@ -305,6 +319,42 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
return binds, nil
|
return binds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
||||||
|
if len(bindNode.Children) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
actionNode := bindNode.Children[0]
|
||||||
|
actionName := actionNode.Name.String()
|
||||||
|
if actionName == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := []string{actionName}
|
||||||
|
for _, arg := range actionNode.Arguments {
|
||||||
|
val := arg.ValueString()
|
||||||
|
if val == "" {
|
||||||
|
parts = append(parts, `""`)
|
||||||
|
} else {
|
||||||
|
parts = append(parts, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionNode.Properties != nil {
|
||||||
|
if val, ok := actionNode.Properties.Get("focus"); ok {
|
||||||
|
parts = append(parts, "focus="+val.String())
|
||||||
|
}
|
||||||
|
if val, ok := actionNode.Properties.Get("show-pointer"); ok {
|
||||||
|
parts = append(parts, "show-pointer="+val.String())
|
||||||
|
}
|
||||||
|
if val, ok := actionNode.Properties.Get("write-to-disk"); ok {
|
||||||
|
parts = append(parts, "write-to-disk="+val.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
||||||
if node.Properties == nil {
|
if node.Properties == nil {
|
||||||
return make(map[string]any)
|
return make(map[string]any)
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ func TestNiriFormatRawAction(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"spawn", []string{"kitty"}, "spawn kitty"},
|
{"spawn", []string{"kitty"}, "spawn kitty"},
|
||||||
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
|
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
|
||||||
|
{"spawn", []string{"dms", "ipc", "call", "brightness", "increment", "5", ""}, `spawn dms ipc call brightness increment 5 ""`},
|
||||||
|
{"spawn", []string{"dms", "ipc", "call", "dash", "toggle", ""}, `spawn dms ipc call dash toggle ""`},
|
||||||
{"close-window", nil, "close-window"},
|
{"close-window", nil, "close-window"},
|
||||||
{"fullscreen-window", nil, "fullscreen-window"},
|
{"fullscreen-window", nil, "fullscreen-window"},
|
||||||
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
|
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
|
||||||
@@ -324,6 +326,58 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNiriEmptyArgsPreservation(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"XF86MonBrightnessUp": {
|
||||||
|
Key: "XF86MonBrightnessUp",
|
||||||
|
Action: `spawn dms ipc call brightness increment 5 ""`,
|
||||||
|
Description: "Brightness Up",
|
||||||
|
},
|
||||||
|
"XF86MonBrightnessDown": {
|
||||||
|
Key: "XF86MonBrightnessDown",
|
||||||
|
Action: `spawn dms ipc call brightness decrement 5 ""`,
|
||||||
|
Description: "Brightness Down",
|
||||||
|
},
|
||||||
|
"Super+Alt+Page_Up": {
|
||||||
|
Key: "Super+Alt+Page_Up",
|
||||||
|
Action: `spawn dms ipc call dash toggle ""`,
|
||||||
|
Description: "Dashboard Toggle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create dms directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
||||||
|
if err := os.WriteFile(bindsFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write binds file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testProvider := NewNiriProvider(tmpDir)
|
||||||
|
loadedBinds, err := testProvider.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load binds: %v\nContent was:\n%s", err, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expected := range binds {
|
||||||
|
loaded, ok := loadedBinds[key]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Missing bind for key %s", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if loaded.Action != expected.Action {
|
||||||
|
t.Errorf("Action mismatch for %s:\n got: %q\n want: %q", key, loaded.Action, expected.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Keybind struct {
|
|||||||
Source string `json:"source,omitempty"`
|
Source string `json:"source,omitempty"`
|
||||||
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
||||||
CooldownMs int `json:"cooldownMs,omitempty"`
|
CooldownMs int `json:"cooldownMs,omitempty"`
|
||||||
|
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
|
||||||
Conflict *Keybind `json:"conflict,omitempty"`
|
Conflict *Keybind `json:"conflict,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
@@ -314,6 +315,7 @@ output_path = '%s'
|
|||||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
||||||
default:
|
default:
|
||||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||||
}
|
}
|
||||||
|
|||||||
170
core/internal/notify/notify.go
Normal file
170
core/internal/notify/notify.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
notifyDest = "org.freedesktop.Notifications"
|
||||||
|
notifyPath = "/org/freedesktop/Notifications"
|
||||||
|
notifyInterface = "org.freedesktop.Notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
AppName string
|
||||||
|
Icon string
|
||||||
|
Summary string
|
||||||
|
Body string
|
||||||
|
FilePath string
|
||||||
|
Timeout int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func Send(n Notification) error {
|
||||||
|
conn, err := dbus.SessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dbus session failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.AppName == "" {
|
||||||
|
n.AppName = "DMS"
|
||||||
|
}
|
||||||
|
if n.Timeout == 0 {
|
||||||
|
n.Timeout = 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions []string
|
||||||
|
if n.FilePath != "" {
|
||||||
|
actions = []string{
|
||||||
|
"open", "Open",
|
||||||
|
"folder", "Open Folder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := map[string]dbus.Variant{}
|
||||||
|
if n.FilePath != "" {
|
||||||
|
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(notifyDest, notifyPath)
|
||||||
|
call := obj.Call(
|
||||||
|
notifyInterface+".Notify",
|
||||||
|
0,
|
||||||
|
n.AppName,
|
||||||
|
uint32(0),
|
||||||
|
n.Icon,
|
||||||
|
n.Summary,
|
||||||
|
n.Body,
|
||||||
|
actions,
|
||||||
|
hints,
|
||||||
|
n.Timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if call.Err != nil {
|
||||||
|
return fmt.Errorf("notify call failed: %w", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationID uint32
|
||||||
|
if err := call.Store(¬ificationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to get notification id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actions) > 0 && n.FilePath != "" {
|
||||||
|
spawnActionListener(notificationID, n.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func spawnActionListener(notificationID uint32, filePath string) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exe, "notify-action-generic", fmt.Sprintf("%d", notificationID), filePath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunActionListener(args []string) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationID, err := strconv.ParseUint(args[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := args[1]
|
||||||
|
|
||||||
|
conn, err := dbus.SessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.AddMatchSignal(
|
||||||
|
dbus.WithMatchObjectPath(notifyPath),
|
||||||
|
dbus.WithMatchInterface(notifyInterface),
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signals := make(chan *dbus.Signal, 10)
|
||||||
|
conn.Signal(signals)
|
||||||
|
|
||||||
|
for sig := range signals {
|
||||||
|
switch sig.Name {
|
||||||
|
case notifyInterface + ".ActionInvoked":
|
||||||
|
if len(sig.Body) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, ok := sig.Body[0].(uint32)
|
||||||
|
if !ok || id != uint32(notificationID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
action, ok := sig.Body[1].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
handleAction(action, filePath)
|
||||||
|
return
|
||||||
|
|
||||||
|
case notifyInterface + ".NotificationClosed":
|
||||||
|
if len(sig.Body) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, ok := sig.Body[0].(uint32)
|
||||||
|
if !ok || id != uint32(notificationID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(action, filePath string) {
|
||||||
|
switch action {
|
||||||
|
case "open", "default":
|
||||||
|
openPath(filePath)
|
||||||
|
case "folder":
|
||||||
|
openPath(filepath.Dir(filePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openPath(path string) {
|
||||||
|
cmd := exec.Command("xdg-open", path)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
cmd.Start()
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ type Plugin struct {
|
|||||||
Compositors []string `json:"compositors"`
|
Compositors []string `json:"compositors"`
|
||||||
Distro []string `json:"distro"`
|
Distro []string `json:"distro"`
|
||||||
Screenshot string `json:"screenshot,omitempty"`
|
Screenshot string `json:"screenshot,omitempty"`
|
||||||
|
RequiresDMS string `json:"requires_dms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitClient interface {
|
type GitClient interface {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,17 +111,15 @@ func (m *Manager) updateAdapterState() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
powered, _ := poweredVar.Value().(bool)
|
|
||||||
|
|
||||||
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
discovering, _ := discoveringVar.Value().(bool)
|
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Powered = powered
|
m.state.Powered = dbusutil.AsOr(poweredVar, false)
|
||||||
m.state.Discovering = discovering
|
m.state.Discovering = dbusutil.AsOr(discoveringVar, false)
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -169,65 +168,20 @@ func (m *Manager) updateDevices() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
||||||
dev := Device{Path: path}
|
return Device{
|
||||||
|
Path: path,
|
||||||
if v, ok := props["Address"]; ok {
|
Address: dbusutil.GetOr(props, "Address", ""),
|
||||||
if addr, ok := v.Value().(string); ok {
|
Name: dbusutil.GetOr(props, "Name", ""),
|
||||||
dev.Address = addr
|
Alias: dbusutil.GetOr(props, "Alias", ""),
|
||||||
}
|
Paired: dbusutil.GetOr(props, "Paired", false),
|
||||||
|
Trusted: dbusutil.GetOr(props, "Trusted", false),
|
||||||
|
Blocked: dbusutil.GetOr(props, "Blocked", false),
|
||||||
|
Connected: dbusutil.GetOr(props, "Connected", false),
|
||||||
|
Class: dbusutil.GetOr(props, "Class", uint32(0)),
|
||||||
|
Icon: dbusutil.GetOr(props, "Icon", ""),
|
||||||
|
RSSI: dbusutil.GetOr(props, "RSSI", int16(0)),
|
||||||
|
LegacyPairing: dbusutil.GetOr(props, "LegacyPairing", false),
|
||||||
}
|
}
|
||||||
if v, ok := props["Name"]; ok {
|
|
||||||
if name, ok := v.Value().(string); ok {
|
|
||||||
dev.Name = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Alias"]; ok {
|
|
||||||
if alias, ok := v.Value().(string); ok {
|
|
||||||
dev.Alias = alias
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Paired"]; ok {
|
|
||||||
if paired, ok := v.Value().(bool); ok {
|
|
||||||
dev.Paired = paired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Trusted"]; ok {
|
|
||||||
if trusted, ok := v.Value().(bool); ok {
|
|
||||||
dev.Trusted = trusted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Blocked"]; ok {
|
|
||||||
if blocked, ok := v.Value().(bool); ok {
|
|
||||||
dev.Blocked = blocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Connected"]; ok {
|
|
||||||
if connected, ok := v.Value().(bool); ok {
|
|
||||||
dev.Connected = connected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Class"]; ok {
|
|
||||||
if class, ok := v.Value().(uint32); ok {
|
|
||||||
dev.Class = class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Icon"]; ok {
|
|
||||||
if icon, ok := v.Value().(string); ok {
|
|
||||||
dev.Icon = icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["RSSI"]; ok {
|
|
||||||
if rssi, ok := v.Value().(int16); ok {
|
|
||||||
dev.RSSI = rssi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["LegacyPairing"]; ok {
|
|
||||||
if legacy, ok := v.Value().(bool); ok {
|
|
||||||
dev.LegacyPairing = legacy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dev
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) startAgent() error {
|
func (m *Manager) startAgent() error {
|
||||||
@@ -328,17 +282,13 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
dirty := false
|
dirty := false
|
||||||
|
|
||||||
if v, ok := changed["Powered"]; ok {
|
if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok {
|
||||||
if powered, ok := v.Value().(bool); ok {
|
m.state.Powered = powered
|
||||||
m.state.Powered = powered
|
dirty = true
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if v, ok := changed["Discovering"]; ok {
|
if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok {
|
||||||
if discovering, ok := v.Value().(bool); ok {
|
m.state.Discovering = discovering
|
||||||
m.state.Discovering = discovering
|
dirty = true
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
@@ -349,31 +299,28 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) {
|
func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) {
|
||||||
pairedVar, hasPaired := changed["Paired"]
|
paired, hasPaired := dbusutil.Get[bool](changed, "Paired")
|
||||||
_, hasConnected := changed["Connected"]
|
_, hasConnected := changed["Connected"]
|
||||||
_, hasTrusted := changed["Trusted"]
|
_, hasTrusted := changed["Trusted"]
|
||||||
|
|
||||||
if hasPaired {
|
if hasPaired {
|
||||||
devicePath := string(path)
|
devicePath := string(path)
|
||||||
if paired, ok := pairedVar.Value().(bool); ok {
|
if paired {
|
||||||
if paired {
|
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
if wasPending {
|
||||||
|
select {
|
||||||
if wasPending {
|
case m.eventQueue <- func() {
|
||||||
select {
|
time.Sleep(300 * time.Millisecond)
|
||||||
case m.eventQueue <- func() {
|
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||||
time.Sleep(300 * time.Millisecond)
|
if err := m.ConnectDevice(devicePath); err != nil {
|
||||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||||
if err := m.ConnectDevice(devicePath); err != nil {
|
|
||||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
|
||||||
}
|
|
||||||
}:
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
}:
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
m.pendingPairings.Delete(devicePath)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
m.pendingPairings.Delete(devicePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
handleSetConfig(conn, req, m)
|
handleSetConfig(conn, req, m)
|
||||||
case "clipboard.store":
|
case "clipboard.store":
|
||||||
handleStore(conn, req, m)
|
handleStore(conn, req, m)
|
||||||
|
case "clipboard.pinEntry":
|
||||||
|
handlePinEntry(conn, req, m)
|
||||||
|
case "clipboard.unpinEntry":
|
||||||
|
handleUnpinEntry(conn, req, m)
|
||||||
|
case "clipboard.getPinnedEntries":
|
||||||
|
handleGetPinnedEntries(conn, req, m)
|
||||||
|
case "clipboard.getPinnedCount":
|
||||||
|
handleGetPinnedCount(conn, req, m)
|
||||||
default:
|
default:
|
||||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||||
}
|
}
|
||||||
@@ -205,6 +213,9 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
if v, ok := models.Get[bool](req, "disabled"); ok {
|
if v, ok := models.Get[bool](req, "disabled"); ok {
|
||||||
cfg.Disabled = v
|
cfg.Disabled = v
|
||||||
}
|
}
|
||||||
|
if v, ok := models.Get[float64](req, "maxPinned"); ok {
|
||||||
|
cfg.MaxPinned = int(v)
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.SetConfig(cfg); err != nil {
|
if err := m.SetConfig(cfg); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
@@ -230,3 +241,43 @@ func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
id, err := params.Int(req.Params, "id")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.PinEntry(uint64(id)); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
id, err := params.Int(req.Params, "id")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.UnpinEntry(uint64(id)); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
pinned := m.GetPinnedEntries()
|
||||||
|
models.Respond(conn, req.ID, pinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
count := m.GetPinnedCount()
|
||||||
|
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These mime types wont be stored in history
|
// These mime types won't be stored in history
|
||||||
var sensitiveMimeTypes = []string{
|
var sensitiveMimeTypes = []string{
|
||||||
"x-kde-passwordManagerHint",
|
"x-kde-passwordManagerHint",
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,11 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
var count int
|
var count int
|
||||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err == nil && entry.Pinned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if count < m.config.MaxHistory {
|
if count < m.config.MaxHistory {
|
||||||
count++
|
count++
|
||||||
continue
|
continue
|
||||||
@@ -419,6 +423,11 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
buf.WriteByte(0)
|
buf.WriteByte(0)
|
||||||
}
|
}
|
||||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||||
|
if e.Pinned {
|
||||||
|
buf.WriteByte(1)
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(0)
|
||||||
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
@@ -462,6 +471,12 @@ func decodeEntry(data []byte) (Entry, error) {
|
|||||||
binary.Read(buf, binary.BigEndian, &e.Hash)
|
binary.Read(buf, binary.BigEndian, &e.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if buf.Len() >= 1 {
|
||||||
|
var pinnedByte byte
|
||||||
|
binary.Read(buf, binary.BigEndian, &pinnedByte)
|
||||||
|
e.Pinned = pinnedByte == 1
|
||||||
|
}
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,19 +750,54 @@ func (m *Manager) ClearHistory() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete only non-pinned entries
|
||||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
return err
|
if b == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
_, err := tx.CreateBucket([]byte("clipboard"))
|
|
||||||
return err
|
var toDelete [][]byte
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err != nil || !entry.Pinned {
|
||||||
|
toDelete = append(toDelete, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range toDelete {
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Errorf("Failed to clear clipboard history: %v", err)
|
log.Errorf("Failed to clear clipboard history: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.compactDB(); err != nil {
|
pinnedCount := 0
|
||||||
log.Errorf("Failed to compact database: %v", err)
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
if b != nil {
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
entry, _ := decodeEntry(v)
|
||||||
|
if entry.Pinned {
|
||||||
|
pinnedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("Failed to count pinned entries: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pinnedCount == 0 {
|
||||||
|
if err := m.compactDB(); err != nil {
|
||||||
|
log.Errorf("Failed to compact database: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
@@ -960,6 +1010,10 @@ func (m *Manager) clearOldEntries(days int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Skip pinned entries
|
||||||
|
if entry.Pinned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if entry.Timestamp.Before(cutoff) {
|
if entry.Timestamp.Before(cutoff) {
|
||||||
toDelete = append(toDelete, k)
|
toDelete = append(toDelete, k)
|
||||||
}
|
}
|
||||||
@@ -1250,3 +1304,153 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) PinEntry(id uint64) error {
|
||||||
|
if m.db == nil {
|
||||||
|
return fmt.Errorf("database not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pinned count
|
||||||
|
cfg := m.getConfig()
|
||||||
|
pinnedCount := 0
|
||||||
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err == nil && entry.Pinned {
|
||||||
|
pinnedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("Failed to count pinned entries: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pinnedCount >= cfg.MaxPinned {
|
||||||
|
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
v := b.Get(itob(id))
|
||||||
|
if v == nil {
|
||||||
|
return fmt.Errorf("entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Pinned = true
|
||||||
|
encoded, err := encodeEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Put(itob(id), encoded)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
m.updateState()
|
||||||
|
m.notifySubscribers()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UnpinEntry(id uint64) error {
|
||||||
|
if m.db == nil {
|
||||||
|
return fmt.Errorf("database not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
v := b.Get(itob(id))
|
||||||
|
if v == nil {
|
||||||
|
return fmt.Errorf("entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Pinned = false
|
||||||
|
encoded, err := encodeEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Put(itob(id), encoded)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
m.updateState()
|
||||||
|
m.notifySubscribers()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetPinnedEntries() []Entry {
|
||||||
|
if m.db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pinned []Entry
|
||||||
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.Pinned {
|
||||||
|
entry.Data = nil
|
||||||
|
pinned = append(pinned, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("Failed to get pinned entries: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetPinnedCount() int {
|
||||||
|
if m.db == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("clipboard"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
entry, err := decodeEntry(v)
|
||||||
|
if err == nil && entry.Pinned {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("Failed to count pinned entries: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Config struct {
|
|||||||
AutoClearDays int `json:"autoClearDays"`
|
AutoClearDays int `json:"autoClearDays"`
|
||||||
ClearAtStartup bool `json:"clearAtStartup"`
|
ClearAtStartup bool `json:"clearAtStartup"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
MaxPinned int `json:"maxPinned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
@@ -27,6 +28,7 @@ func DefaultConfig() Config {
|
|||||||
MaxEntrySize: 5 * 1024 * 1024,
|
MaxEntrySize: 5 * 1024 * 1024,
|
||||||
AutoClearDays: 0,
|
AutoClearDays: 0,
|
||||||
ClearAtStartup: false,
|
ClearAtStartup: false,
|
||||||
|
MaxPinned: 25,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ type Entry struct {
|
|||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
IsImage bool `json:"isImage"`
|
IsImage bool `json:"isImage"`
|
||||||
Hash uint64 `json:"hash,omitempty"`
|
Hash uint64 `json:"hash,omitempty"`
|
||||||
|
Pinned bool `json:"pinned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
|
|||||||
237
core/internal/server/dbus/handlers.go
Normal file
237
core/internal/server/dbus/handlers.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package dbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
type objectParams struct {
|
||||||
|
bus string
|
||||||
|
dest string
|
||||||
|
path string
|
||||||
|
iface string
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractObjectParams(p map[string]any, requirePath bool) (objectParams, error) {
|
||||||
|
bus, err := params.String(p, "bus")
|
||||||
|
if err != nil {
|
||||||
|
return objectParams{}, err
|
||||||
|
}
|
||||||
|
dest, err := params.String(p, "dest")
|
||||||
|
if err != nil {
|
||||||
|
return objectParams{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var path string
|
||||||
|
if requirePath {
|
||||||
|
path, err = params.String(p, "path")
|
||||||
|
if err != nil {
|
||||||
|
return objectParams{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = params.StringOpt(p, "path", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
iface, err := params.String(p, "interface")
|
||||||
|
if err != nil {
|
||||||
|
return objectParams{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectParams{bus: bus, dest: dest, path: path, iface: iface}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||||
|
switch req.Method {
|
||||||
|
case "dbus.call":
|
||||||
|
handleCall(conn, req, m)
|
||||||
|
case "dbus.getProperty":
|
||||||
|
handleGetProperty(conn, req, m)
|
||||||
|
case "dbus.setProperty":
|
||||||
|
handleSetProperty(conn, req, m)
|
||||||
|
case "dbus.getAllProperties":
|
||||||
|
handleGetAllProperties(conn, req, m)
|
||||||
|
case "dbus.introspect":
|
||||||
|
handleIntrospect(conn, req, m)
|
||||||
|
case "dbus.listNames":
|
||||||
|
handleListNames(conn, req, m)
|
||||||
|
case "dbus.subscribe":
|
||||||
|
handleSubscribe(conn, req, m, clientID)
|
||||||
|
case "dbus.unsubscribe":
|
||||||
|
handleUnsubscribe(conn, req, m)
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCall(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
op, err := extractObjectParams(req.Params, true)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
method, err := params.String(req.Params, "method")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []any
|
||||||
|
if argsRaw, ok := params.Any(req.Params, "args"); ok {
|
||||||
|
if argsSlice, ok := argsRaw.([]any); ok {
|
||||||
|
args = argsSlice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := m.Call(op.bus, op.dest, op.path, op.iface, method, args)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
op, err := extractObjectParams(req.Params, true)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
property, err := params.String(req.Params, "property")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := m.GetProperty(op.bus, op.dest, op.path, op.iface, property)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
op, err := extractObjectParams(req.Params, true)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
property, err := params.String(req.Params, "property")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := params.Any(req.Params, "value")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing 'value' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.SetProperty(op.bus, op.dest, op.path, op.iface, property, value); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
op, err := extractObjectParams(req.Params, true)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := m.GetAllProperties(op.bus, op.dest, op.path, op.iface)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
bus, err := params.String(req.Params, "bus")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dest, err := params.String(req.Params, "dest")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := params.StringOpt(req.Params, "path", "/")
|
||||||
|
|
||||||
|
result, err := m.Introspect(bus, dest, path)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleListNames(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
bus, err := params.String(req.Params, "bus")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := m.ListNames(bus)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||||
|
bus, err := params.String(req.Params, "bus")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := params.StringOpt(req.Params, "sender", "")
|
||||||
|
path := params.StringOpt(req.Params, "path", "")
|
||||||
|
iface := params.StringOpt(req.Params, "interface", "")
|
||||||
|
member := params.StringOpt(req.Params, "member", "")
|
||||||
|
|
||||||
|
result, err := m.Subscribe(clientID, bus, sender, path, iface, member)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
subID, err := params.String(req.Params, "subscriptionId")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Unsubscribe(subID); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||||
|
}
|
||||||
362
core/internal/server/dbus/manager.go
Normal file
362
core/internal/server/dbus/manager.go
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package dbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewManager() (*Manager, error) {
|
||||||
|
systemConn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionConn, err := dbus.ConnectSessionBus()
|
||||||
|
if err != nil {
|
||||||
|
systemConn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &Manager{
|
||||||
|
systemConn: systemConn,
|
||||||
|
sessionConn: sessionConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.processSystemSignals()
|
||||||
|
go m.processSessionSignals()
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getConn(bus string) (*dbus.Conn, error) {
|
||||||
|
switch bus {
|
||||||
|
case "system":
|
||||||
|
if m.systemConn == nil {
|
||||||
|
return nil, fmt.Errorf("system bus not connected")
|
||||||
|
}
|
||||||
|
return m.systemConn, nil
|
||||||
|
case "session":
|
||||||
|
if m.sessionConn == nil {
|
||||||
|
return nil, fmt.Errorf("session bus not connected")
|
||||||
|
}
|
||||||
|
return m.sessionConn, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid bus: %s (must be 'system' or 'session')", bus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Call(bus, dest, path, iface, method string, args []any) (*CallResult, error) {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||||
|
fullMethod := iface + "." + method
|
||||||
|
|
||||||
|
call := obj.Call(fullMethod, 0, args...)
|
||||||
|
if call.Err != nil {
|
||||||
|
return nil, fmt.Errorf("dbus call failed: %w", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CallResult{Values: call.Body}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||||
|
|
||||||
|
var variant dbus.Variant
|
||||||
|
err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, iface, property).Store(&variant)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get property: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PropertyResult{Value: dbusutil.Normalize(variant.Value())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetProperty(bus, dest, path, iface, property string, value any) error {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||||
|
|
||||||
|
call := obj.Call("org.freedesktop.DBus.Properties.Set", 0, iface, property, dbus.MakeVariant(value))
|
||||||
|
if call.Err != nil {
|
||||||
|
return fmt.Errorf("failed to set property: %w", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetAllProperties(bus, dest, path, iface string) (map[string]any, error) {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||||
|
|
||||||
|
var props map[string]dbus.Variant
|
||||||
|
err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface).Store(&props)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get properties: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]any)
|
||||||
|
for k, v := range props {
|
||||||
|
result[k] = dbusutil.Normalize(v.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Introspect(bus, dest, path string) (*IntrospectResult, error) {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||||
|
|
||||||
|
var xml string
|
||||||
|
err = obj.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&xml)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to introspect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &IntrospectResult{XML: xml}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ListNames(bus string) (*ListNamesResult, error) {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list names: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ListNamesResult{Names: names}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscribe(clientID, bus, sender, path, iface, member string) (*SubscribeResult, error) {
|
||||||
|
conn, err := m.getConn(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
subID := generateSubscriptionID()
|
||||||
|
|
||||||
|
parts := []string{"type='signal'"}
|
||||||
|
if sender != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sender='%s'", sender))
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("path='%s'", path))
|
||||||
|
}
|
||||||
|
if iface != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("interface='%s'", iface))
|
||||||
|
}
|
||||||
|
if member != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("member='%s'", member))
|
||||||
|
}
|
||||||
|
matchRule := strings.Join(parts, ",")
|
||||||
|
|
||||||
|
call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule)
|
||||||
|
if call.Err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add match rule: %w", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := &signalSubscription{
|
||||||
|
Bus: bus,
|
||||||
|
Sender: sender,
|
||||||
|
Path: path,
|
||||||
|
Interface: iface,
|
||||||
|
Member: member,
|
||||||
|
ClientID: clientID,
|
||||||
|
}
|
||||||
|
m.subscriptions.Store(subID, sub)
|
||||||
|
|
||||||
|
log.Debugf("dbus: subscribed %s to %s", subID, matchRule)
|
||||||
|
|
||||||
|
return &SubscribeResult{SubscriptionID: subID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Unsubscribe(subID string) error {
|
||||||
|
sub, ok := m.subscriptions.LoadAndDelete(subID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("subscription not found: %s", subID)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := m.getConn(sub.Bus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := []string{"type='signal'"}
|
||||||
|
if sub.Sender != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sender='%s'", sub.Sender))
|
||||||
|
}
|
||||||
|
if sub.Path != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("path='%s'", sub.Path))
|
||||||
|
}
|
||||||
|
if sub.Interface != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("interface='%s'", sub.Interface))
|
||||||
|
}
|
||||||
|
if sub.Member != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("member='%s'", sub.Member))
|
||||||
|
}
|
||||||
|
matchRule := strings.Join(parts, ",")
|
||||||
|
|
||||||
|
call := conn.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule)
|
||||||
|
if call.Err != nil {
|
||||||
|
log.Warnf("dbus: failed to remove match rule: %v", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("dbus: unsubscribed %s", subID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UnsubscribeClient(clientID string) {
|
||||||
|
var toDelete []string
|
||||||
|
m.subscriptions.Range(func(subID string, sub *signalSubscription) bool {
|
||||||
|
if sub.ClientID == clientID {
|
||||||
|
toDelete = append(toDelete, subID)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, subID := range toDelete {
|
||||||
|
if err := m.Unsubscribe(subID); err != nil {
|
||||||
|
log.Warnf("dbus: failed to unsubscribe %s: %v", subID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SubscribeSignals(clientID string) chan SignalEvent {
|
||||||
|
ch := make(chan SignalEvent, 64)
|
||||||
|
existing, loaded := m.signalSubscribers.LoadOrStore(clientID, ch)
|
||||||
|
if loaded {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UnsubscribeSignals(clientID string) {
|
||||||
|
if ch, ok := m.signalSubscribers.LoadAndDelete(clientID); ok {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
m.UnsubscribeClient(clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) processSystemSignals() {
|
||||||
|
if m.systemConn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch := make(chan *dbus.Signal, 256)
|
||||||
|
m.systemConn.Signal(ch)
|
||||||
|
|
||||||
|
for sig := range ch {
|
||||||
|
m.dispatchSignal("system", sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) processSessionSignals() {
|
||||||
|
if m.sessionConn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch := make(chan *dbus.Signal, 256)
|
||||||
|
m.sessionConn.Signal(ch)
|
||||||
|
|
||||||
|
for sig := range ch {
|
||||||
|
m.dispatchSignal("session", sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) dispatchSignal(bus string, sig *dbus.Signal) {
|
||||||
|
path := string(sig.Path)
|
||||||
|
iface := ""
|
||||||
|
member := sig.Name
|
||||||
|
|
||||||
|
if idx := strings.LastIndex(sig.Name, "."); idx != -1 {
|
||||||
|
iface = sig.Name[:idx]
|
||||||
|
member = sig.Name[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
m.subscriptions.Range(func(subID string, sub *signalSubscription) bool {
|
||||||
|
if sub.Bus != bus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if sub.Path != "" && sub.Path != path && !strings.HasPrefix(path, sub.Path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if sub.Interface != "" && sub.Interface != iface {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if sub.Member != "" && sub.Member != member {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
event := SignalEvent{
|
||||||
|
SubscriptionID: subID,
|
||||||
|
Sender: sig.Sender,
|
||||||
|
Path: path,
|
||||||
|
Interface: iface,
|
||||||
|
Member: member,
|
||||||
|
Body: dbusutil.NormalizeSlice(sig.Body),
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, ok := m.signalSubscribers.Load(sub.ClientID)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ch <- event:
|
||||||
|
default:
|
||||||
|
log.Warnf("dbus: channel full for %s, dropping signal", subID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
m.signalSubscribers.Range(func(clientID string, ch chan SignalEvent) bool {
|
||||||
|
close(ch)
|
||||||
|
m.signalSubscribers.Delete(clientID)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if m.systemConn != nil {
|
||||||
|
m.systemConn.Close()
|
||||||
|
}
|
||||||
|
if m.sessionConn != nil {
|
||||||
|
m.sessionConn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSubscriptionID() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
log.Warnf("dbus: failed to generate random subscription ID: %v", err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
52
core/internal/server/dbus/types.go
Normal file
52
core/internal/server/dbus/types.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package dbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
systemConn *dbus.Conn
|
||||||
|
sessionConn *dbus.Conn
|
||||||
|
|
||||||
|
subscriptions syncmap.Map[string, *signalSubscription]
|
||||||
|
signalSubscribers syncmap.Map[string, chan SignalEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
type signalSubscription struct {
|
||||||
|
Bus string
|
||||||
|
Sender string
|
||||||
|
Path string
|
||||||
|
Interface string
|
||||||
|
Member string
|
||||||
|
ClientID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignalEvent struct {
|
||||||
|
SubscriptionID string `json:"subscriptionId"`
|
||||||
|
Sender string `json:"sender"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
Member string `json:"member"`
|
||||||
|
Body []any `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallResult struct {
|
||||||
|
Values []any `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropertyResult struct {
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntrospectResult struct {
|
||||||
|
XML string `json:"xml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNamesResult struct {
|
||||||
|
Names []string `json:"names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeResult struct {
|
||||||
|
SubscriptionID string `json:"subscriptionId"`
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,61 +111,17 @@ func (m *Manager) updateAccountsState() error {
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
defer m.stateMutex.Unlock()
|
defer m.stateMutex.Unlock()
|
||||||
|
|
||||||
if v, ok := props["IconFile"]; ok {
|
m.state.Accounts.IconFile = dbusutil.GetOr(props, "IconFile", "")
|
||||||
if val, ok := v.Value().(string); ok {
|
m.state.Accounts.RealName = dbusutil.GetOr(props, "RealName", "")
|
||||||
m.state.Accounts.IconFile = val
|
m.state.Accounts.UserName = dbusutil.GetOr(props, "UserName", "")
|
||||||
}
|
m.state.Accounts.AccountType = dbusutil.GetOr(props, "AccountType", int32(0))
|
||||||
}
|
m.state.Accounts.HomeDirectory = dbusutil.GetOr(props, "HomeDirectory", "")
|
||||||
if v, ok := props["RealName"]; ok {
|
m.state.Accounts.Shell = dbusutil.GetOr(props, "Shell", "")
|
||||||
if val, ok := v.Value().(string); ok {
|
m.state.Accounts.Email = dbusutil.GetOr(props, "Email", "")
|
||||||
m.state.Accounts.RealName = val
|
m.state.Accounts.Language = dbusutil.GetOr(props, "Language", "")
|
||||||
}
|
m.state.Accounts.Location = dbusutil.GetOr(props, "Location", "")
|
||||||
}
|
m.state.Accounts.Locked = dbusutil.GetOr(props, "Locked", false)
|
||||||
if v, ok := props["UserName"]; ok {
|
m.state.Accounts.PasswordMode = dbusutil.GetOr(props, "PasswordMode", int32(0))
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.UserName = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["AccountType"]; ok {
|
|
||||||
if val, ok := v.Value().(int32); ok {
|
|
||||||
m.state.Accounts.AccountType = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["HomeDirectory"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.HomeDirectory = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Shell"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Shell = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Email"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Email = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Language"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Language = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Location"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Accounts.Location = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Locked"]; ok {
|
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.Accounts.Locked = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["PasswordMode"]; ok {
|
|
||||||
if val, ok := v.Value().(int32); ok {
|
|
||||||
m.state.Accounts.PasswordMode = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -180,7 +137,7 @@ func (m *Manager) updateSettingsState() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if colorScheme, ok := variant.Value().(uint32); ok {
|
if colorScheme, ok := dbusutil.As[uint32](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Settings.ColorScheme = colorScheme
|
m.state.Settings.ColorScheme = colorScheme
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,37 +133,15 @@ func (m *Manager) updateSessionState() error {
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
defer m.stateMutex.Unlock()
|
defer m.stateMutex.Unlock()
|
||||||
|
|
||||||
if v, ok := props["Active"]; ok {
|
m.state.Active = dbusutil.GetOr(props, "Active", m.state.Active)
|
||||||
if val, ok := v.Value().(bool); ok {
|
m.state.IdleHint = dbusutil.GetOr(props, "IdleHint", m.state.IdleHint)
|
||||||
m.state.Active = val
|
m.state.IdleSinceHint = dbusutil.GetOr(props, "IdleSinceHint", m.state.IdleSinceHint)
|
||||||
}
|
if lockedHint, ok := dbusutil.Get[bool](props, "LockedHint"); ok {
|
||||||
}
|
m.state.LockedHint = lockedHint
|
||||||
if v, ok := props["IdleHint"]; ok {
|
m.state.Locked = lockedHint
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.IdleHint = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["IdleSinceHint"]; ok {
|
|
||||||
if val, ok := v.Value().(uint64); ok {
|
|
||||||
m.state.IdleSinceHint = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["LockedHint"]; ok {
|
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.LockedHint = val
|
|
||||||
m.state.Locked = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Type"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.SessionType = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Class"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.SessionClass = val
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
m.state.SessionType = dbusutil.GetOr(props, "Type", m.state.SessionType)
|
||||||
|
m.state.SessionClass = dbusutil.GetOr(props, "Class", m.state.SessionClass)
|
||||||
if v, ok := props["User"]; ok {
|
if v, ok := props["User"]; ok {
|
||||||
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
||||||
if uid, ok := userArr[0].(uint32); ok {
|
if uid, ok := userArr[0].(uint32); ok {
|
||||||
@@ -170,36 +149,12 @@ func (m *Manager) updateSessionState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, ok := props["Name"]; ok {
|
m.state.UserName = dbusutil.GetOr(props, "Name", m.state.UserName)
|
||||||
if val, ok := v.Value().(string); ok {
|
m.state.RemoteHost = dbusutil.GetOr(props, "RemoteHost", m.state.RemoteHost)
|
||||||
m.state.UserName = val
|
m.state.Service = dbusutil.GetOr(props, "Service", m.state.Service)
|
||||||
}
|
m.state.TTY = dbusutil.GetOr(props, "TTY", m.state.TTY)
|
||||||
}
|
m.state.Display = dbusutil.GetOr(props, "Display", m.state.Display)
|
||||||
if v, ok := props["RemoteHost"]; ok {
|
m.state.Remote = dbusutil.GetOr(props, "Remote", m.state.Remote)
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.RemoteHost = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Service"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Service = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["TTY"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.TTY = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Display"]; ok {
|
|
||||||
if val, ok := v.Value().(string); ok {
|
|
||||||
m.state.Display = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Remote"]; ok {
|
|
||||||
if val, ok := v.Value().(bool); ok {
|
|
||||||
m.state.Remote = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := props["Seat"]; ok {
|
if v, ok := props["Seat"]; ok {
|
||||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||||
if seatID, ok := seatArr[0].(string); ok {
|
if seatID, ok := seatArr[0].(string); ok {
|
||||||
@@ -207,11 +162,7 @@ func (m *Manager) updateSessionState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, ok := props["VTNr"]; ok {
|
m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr)
|
||||||
if val, ok := v.Value().(uint32); ok {
|
|
||||||
m.state.VTNr = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package loginctl
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,31 +118,28 @@ func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) {
|
|||||||
for key, variant := range changes {
|
for key, variant := range changes {
|
||||||
switch key {
|
switch key {
|
||||||
case "Active":
|
case "Active":
|
||||||
if val, ok := variant.Value().(bool); ok {
|
if val, ok := dbusutil.As[bool](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Active = val
|
m.state.Active = val
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "IdleHint":
|
case "IdleHint":
|
||||||
if val, ok := variant.Value().(bool); ok {
|
if val, ok := dbusutil.As[bool](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.IdleHint = val
|
m.state.IdleHint = val
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "IdleSinceHint":
|
case "IdleSinceHint":
|
||||||
if val, ok := variant.Value().(uint64); ok {
|
if val, ok := dbusutil.As[uint64](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.IdleSinceHint = val
|
m.state.IdleSinceHint = val
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
needsUpdate = true
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "LockedHint":
|
case "LockedHint":
|
||||||
if val, ok := variant.Value().(bool); ok {
|
if val, ok := dbusutil.As[bool](variant); ok {
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.LockedHint = val
|
m.state.LockedHint = val
|
||||||
m.state.Locked = val
|
m.state.Locked = val
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package network
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -925,25 +926,24 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
|
|||||||
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
||||||
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
||||||
|
|
||||||
var output []byte
|
var allErrors []error
|
||||||
var err error
|
var outputStr string
|
||||||
for _, vpnType := range vpnTypes {
|
for _, vpnType := range vpnTypes {
|
||||||
args := []string{"connection", "import", "type", vpnType, "file", filePath}
|
cmd := exec.Command("nmcli", "connection", "import", "type", vpnType, "file", filePath)
|
||||||
cmd := exec.Command("nmcli", args...)
|
output, err := cmd.CombinedOutput()
|
||||||
output, err = cmd.CombinedOutput()
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
outputStr = string(output)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
allErrors = append(allErrors, fmt.Errorf("%s: %s", vpnType, strings.TrimSpace(string(output))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if len(allErrors) == len(vpnTypes) {
|
||||||
return &VPNImportResult{
|
return &VPNImportResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
|
Error: errors.Join(allErrors...).Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStr := string(output)
|
|
||||||
var connUUID, connName string
|
var connUUID, connName string
|
||||||
|
|
||||||
lines := strings.Split(outputStr, "\n")
|
lines := strings.Split(outputStr, "\n")
|
||||||
|
|||||||
@@ -357,31 +357,51 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
savedSSIDs := make(map[string]bool)
|
savedSSIDs := make(map[string]bool)
|
||||||
autoconnectMap := make(map[string]bool)
|
autoconnectMap := make(map[string]bool)
|
||||||
|
hiddenSSIDs := make(map[string]bool)
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
connSettings, err := conn.GetSettings()
|
connSettings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if connMeta, ok := connSettings["connection"]; ok {
|
connMeta, ok := connSettings["connection"]
|
||||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
if !ok {
|
||||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
continue
|
||||||
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok {
|
}
|
||||||
ssid := string(ssidBytes)
|
|
||||||
savedSSIDs[ssid] = true
|
connType, ok := connMeta["type"].(string)
|
||||||
autoconnect := true
|
if !ok || connType != "802-11-wireless" {
|
||||||
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
continue
|
||||||
autoconnect = ac
|
}
|
||||||
}
|
|
||||||
autoconnectMap[ssid] = autoconnect
|
wifiSettings, ok := connSettings["802-11-wireless"]
|
||||||
}
|
if !ok {
|
||||||
}
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssidBytes, ok := wifiSettings["ssid"].([]byte)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := string(ssidBytes)
|
||||||
|
savedSSIDs[ssid] = true
|
||||||
|
autoconnect := true
|
||||||
|
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
||||||
|
autoconnect = ac
|
||||||
|
}
|
||||||
|
autoconnectMap[ssid] = autoconnect
|
||||||
|
|
||||||
|
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||||
|
hiddenSSIDs[ssid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.stateMutex.RLock()
|
b.stateMutex.RLock()
|
||||||
currentSSID := b.state.WiFiSSID
|
currentSSID := b.state.WiFiSSID
|
||||||
|
wifiConnected := b.state.WiFiConnected
|
||||||
|
wifiSignal := b.state.WiFiSignal
|
||||||
|
wifiBSSID := b.state.WiFiBSSID
|
||||||
b.stateMutex.RUnlock()
|
b.stateMutex.RUnlock()
|
||||||
|
|
||||||
seenSSIDs := make(map[string]*WiFiNetwork)
|
seenSSIDs := make(map[string]*WiFiNetwork)
|
||||||
@@ -444,6 +464,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
Connected: ssid == currentSSID,
|
Connected: ssid == currentSSID,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: maxBitrate / 1000,
|
||||||
@@ -454,6 +475,23 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
networks = append(networks, network)
|
networks = append(networks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wifiConnected && currentSSID != "" {
|
||||||
|
if _, exists := seenSSIDs[currentSSID]; !exists {
|
||||||
|
hiddenNetwork := WiFiNetwork{
|
||||||
|
SSID: currentSSID,
|
||||||
|
BSSID: wifiBSSID,
|
||||||
|
Signal: wifiSignal,
|
||||||
|
Secured: true,
|
||||||
|
Connected: true,
|
||||||
|
Saved: savedSSIDs[currentSSID],
|
||||||
|
Autoconnect: autoconnectMap[currentSSID],
|
||||||
|
Hidden: true,
|
||||||
|
Mode: "infrastructure",
|
||||||
|
}
|
||||||
|
networks = append(networks, hiddenNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortWiFiNetworks(networks)
|
sortWiFiNetworks(networks)
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
@@ -515,40 +553,53 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||||
dev := devInfo.device
|
dev := devInfo.device
|
||||||
w := devInfo.wireless
|
w := devInfo.wireless
|
||||||
apPaths, err := w.GetAccessPoints()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get access points: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetAP gonetworkmanager.AccessPoint
|
var targetAP gonetworkmanager.AccessPoint
|
||||||
for _, ap := range apPaths {
|
var flags, wpaFlags, rsnFlags uint32
|
||||||
ssid, err := ap.GetPropertySSID()
|
|
||||||
if err != nil || ssid != req.SSID {
|
if !req.Hidden {
|
||||||
continue
|
apPaths, err := w.GetAccessPoints()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get access points: %w", err)
|
||||||
}
|
}
|
||||||
targetAP = ap
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetAP == nil {
|
for _, ap := range apPaths {
|
||||||
return fmt.Errorf("access point not found: %s", req.SSID)
|
ssid, err := ap.GetPropertySSID()
|
||||||
}
|
if err != nil || ssid != req.SSID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetAP = ap
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
flags, _ := targetAP.GetPropertyFlags()
|
if targetAP == nil {
|
||||||
wpaFlags, _ := targetAP.GetPropertyWPAFlags()
|
return fmt.Errorf("access point not found: %s", req.SSID)
|
||||||
rsnFlags, _ := targetAP.GetPropertyRSNFlags()
|
}
|
||||||
|
|
||||||
|
flags, _ = targetAP.GetPropertyFlags()
|
||||||
|
wpaFlags, _ = targetAP.GetPropertyWPAFlags()
|
||||||
|
rsnFlags, _ = targetAP.GetPropertyRSNFlags()
|
||||||
|
}
|
||||||
|
|
||||||
const KeyMgmt8021x = uint32(512)
|
const KeyMgmt8021x = uint32(512)
|
||||||
const KeyMgmtPsk = uint32(256)
|
const KeyMgmtPsk = uint32(256)
|
||||||
const KeyMgmtSae = uint32(1024)
|
const KeyMgmtSae = uint32(1024)
|
||||||
|
|
||||||
isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
var isEnterprise, isPsk, isSae, secured bool
|
||||||
isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
|
||||||
isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
|
||||||
|
|
||||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
switch {
|
||||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
case req.Hidden:
|
||||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
secured = req.Password != "" || req.Username != ""
|
||||||
|
isEnterprise = req.Username != ""
|
||||||
|
isPsk = req.Password != "" && !isEnterprise
|
||||||
|
default:
|
||||||
|
isEnterprise = (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
||||||
|
isPsk = (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
||||||
|
isSae = (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
||||||
|
secured = flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||||
|
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||||
|
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||||
|
}
|
||||||
|
|
||||||
if isEnterprise {
|
if isEnterprise {
|
||||||
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
||||||
@@ -567,11 +618,15 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
settings["ipv6"] = map[string]any{"method": "auto"}
|
settings["ipv6"] = map[string]any{"method": "auto"}
|
||||||
|
|
||||||
if secured {
|
if secured {
|
||||||
settings["802-11-wireless"] = map[string]any{
|
wifiSettings := map[string]any{
|
||||||
"ssid": []byte(req.SSID),
|
"ssid": []byte(req.SSID),
|
||||||
"mode": "infrastructure",
|
"mode": "infrastructure",
|
||||||
"security": "802-11-wireless-security",
|
"security": "802-11-wireless-security",
|
||||||
}
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
wifiSettings["hidden"] = true
|
||||||
|
}
|
||||||
|
settings["802-11-wireless"] = wifiSettings
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isEnterprise || req.Username != "":
|
case isEnterprise || req.Username != "":
|
||||||
@@ -658,10 +713,14 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settings["802-11-wireless"] = map[string]any{
|
wifiSettings := map[string]any{
|
||||||
"ssid": []byte(req.SSID),
|
"ssid": []byte(req.SSID),
|
||||||
"mode": "infrastructure",
|
"mode": "infrastructure",
|
||||||
}
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
wifiSettings["hidden"] = true
|
||||||
|
}
|
||||||
|
settings["802-11-wireless"] = wifiSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Interactive {
|
if req.Interactive {
|
||||||
@@ -685,14 +744,23 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
if req.Hidden {
|
||||||
|
_, err = nm.ActivateConnection(conn, dev, nil)
|
||||||
|
} else {
|
||||||
|
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to activate connection: %w", err)
|
return fmt.Errorf("failed to activate connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
||||||
} else {
|
} else {
|
||||||
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
var err error
|
||||||
|
if req.Hidden {
|
||||||
|
_, err = nm.AddAndActivateConnection(settings, dev)
|
||||||
|
} else {
|
||||||
|
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect: %w", err)
|
return fmt.Errorf("failed to connect: %w", err)
|
||||||
}
|
}
|
||||||
@@ -813,6 +881,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
savedSSIDs := make(map[string]bool)
|
savedSSIDs := make(map[string]bool)
|
||||||
autoconnectMap := make(map[string]bool)
|
autoconnectMap := make(map[string]bool)
|
||||||
|
hiddenSSIDs := make(map[string]bool)
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
connSettings, err := conn.GetSettings()
|
connSettings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -846,6 +915,10 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
autoconnect = ac
|
autoconnect = ac
|
||||||
}
|
}
|
||||||
autoconnectMap[ssid] = autoconnect
|
autoconnectMap[ssid] = autoconnect
|
||||||
|
|
||||||
|
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||||
|
hiddenSSIDs[ssid] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var devices []WiFiDevice
|
var devices []WiFiDevice
|
||||||
@@ -939,6 +1012,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
Connected: connected && apSSID == ssid,
|
Connected: connected && apSSID == ssid,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: maxBitrate / 1000,
|
||||||
@@ -949,6 +1023,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
seenSSIDs[apSSID] = &network
|
seenSSIDs[apSSID] = &network
|
||||||
networks = append(networks, network)
|
networks = append(networks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connected && ssid != "" {
|
||||||
|
if _, exists := seenSSIDs[ssid]; !exists {
|
||||||
|
hiddenNetwork := WiFiNetwork{
|
||||||
|
SSID: ssid,
|
||||||
|
BSSID: bssid,
|
||||||
|
Signal: signal,
|
||||||
|
Secured: true,
|
||||||
|
Connected: true,
|
||||||
|
Saved: savedSSIDs[ssid],
|
||||||
|
Autoconnect: autoconnectMap[ssid],
|
||||||
|
Hidden: true,
|
||||||
|
Mode: "infrastructure",
|
||||||
|
Device: name,
|
||||||
|
}
|
||||||
|
networks = append(networks, hiddenNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortWiFiNetworks(networks)
|
sortWiFiNetworks(networks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,19 +150,11 @@ func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
if err := exec.Command("nmcli", "con", "mod", connName,
|
||||||
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority)).Run(); err != nil {
|
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority),
|
||||||
log.Warnf("Failed to set autoconnect-priority for %v: %v", connName, err)
|
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric),
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
|
||||||
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
|
||||||
log.Warnf("Failed to set ipv4.route-metric for %v: %v", connName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("nmcli", "con", "mod", connName,
|
|
||||||
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
|
||||||
log.Warnf("Failed to set ipv6.route-metric for %v: %v", connName, err)
|
log.Warnf("Failed to set priority for %s: %v", connName, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric)
|
log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type WiFiNetwork struct {
|
|||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
Saved bool `json:"saved"`
|
Saved bool `json:"saved"`
|
||||||
Autoconnect bool `json:"autoconnect"`
|
Autoconnect bool `json:"autoconnect"`
|
||||||
|
Hidden bool `json:"hidden"`
|
||||||
Frequency uint32 `json:"frequency"`
|
Frequency uint32 `json:"frequency"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
Rate uint32 `json:"rate"`
|
Rate uint32 `json:"rate"`
|
||||||
@@ -127,6 +128,7 @@ type ConnectionRequest struct {
|
|||||||
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
|
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
|
||||||
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
|
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
|
||||||
Interactive bool `json:"interactive,omitempty"`
|
Interactive bool `json:"interactive,omitempty"`
|
||||||
|
Hidden bool `json:"hidden,omitempty"`
|
||||||
Device string `json:"device,omitempty"`
|
Device string `json:"device,omitempty"`
|
||||||
EAPMethod string `json:"eapMethod,omitempty"`
|
EAPMethod string `json:"eapMethod,omitempty"`
|
||||||
Phase2Auth string `json:"phase2Auth,omitempty"`
|
Phase2Auth string `json:"phase2Auth,omitempty"`
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func HandleList(conn net.Conn, req models.Request) {
|
|||||||
Dependencies: p.Dependencies,
|
Dependencies: p.Dependencies,
|
||||||
Installed: installed,
|
Installed: installed,
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||||
|
RequiresDMS: p.RequiresDMS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
|
|||||||
Dependencies: plugin.Dependencies,
|
Dependencies: plugin.Dependencies,
|
||||||
FirstParty: strings.HasPrefix(plugin.Repo, "https://github.com/AvengeMedia"),
|
FirstParty: strings.HasPrefix(plugin.Repo, "https://github.com/AvengeMedia"),
|
||||||
HasUpdate: hasUpdate,
|
HasUpdate: hasUpdate,
|
||||||
|
RequiresDMS: plugin.RequiresDMS,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
result = append(result, PluginInfo{
|
result = append(result, PluginInfo{
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func HandleSearch(conn net.Conn, req models.Request) {
|
|||||||
Dependencies: p.Dependencies,
|
Dependencies: p.Dependencies,
|
||||||
Installed: installed,
|
Installed: installed,
|
||||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||||
|
RequiresDMS: p.RequiresDMS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type PluginInfo struct {
|
|||||||
FirstParty bool `json:"firstParty,omitempty"`
|
FirstParty bool `json:"firstParty,omitempty"`
|
||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
HasUpdate bool `json:"hasUpdate,omitempty"`
|
HasUpdate bool `json:"hasUpdate,omitempty"`
|
||||||
|
RequiresDMS string `json:"requires_dms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||||
|
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
@@ -43,6 +45,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Method, "theme.auto.") {
|
||||||
|
if themeModeManager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "theme mode manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
thememode.HandleRequest(conn, req, themeModeManager)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "loginctl.") {
|
if strings.HasPrefix(req.Method, "loginctl.") {
|
||||||
if loginctlManager == nil {
|
if loginctlManager == nil {
|
||||||
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
||||||
@@ -154,6 +165,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Method, "dbus.") {
|
||||||
|
if dbusManager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "dbus manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverDbus.HandleRequest(conn, req, dbusManager, dbusClientID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "clipboard.") {
|
if strings.HasPrefix(req.Method, "clipboard.") {
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "clipboard.getConfig":
|
case "clipboard.getConfig":
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||||
|
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
@@ -65,7 +67,11 @@ var brightnessManager *brightness.Manager
|
|||||||
var wlrOutputManager *wlroutput.Manager
|
var wlrOutputManager *wlroutput.Manager
|
||||||
var evdevManager *evdev.Manager
|
var evdevManager *evdev.Manager
|
||||||
var clipboardManager *clipboard.Manager
|
var clipboardManager *clipboard.Manager
|
||||||
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
|
var themeModeManager *thememode.Manager
|
||||||
|
|
||||||
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||||
var cupsSubscribers syncmap.Map[string, bool]
|
var cupsSubscribers syncmap.Map[string, bool]
|
||||||
@@ -363,6 +369,27 @@ func InitializeClipboardManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitializeDbusManager() error {
|
||||||
|
manager, err := serverDbus.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to initialize dbus manager: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbusManager = manager
|
||||||
|
|
||||||
|
log.Info("DBus manager initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeThemeModeManager() error {
|
||||||
|
manager := thememode.NewManager()
|
||||||
|
themeModeManager = manager
|
||||||
|
|
||||||
|
log.Info("Theme mode automation manager initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleConnection(conn net.Conn) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -440,6 +467,14 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "clipboard")
|
caps = append(caps, "clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if themeModeManager != nil {
|
||||||
|
caps = append(caps, "theme.auto")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbusManager != nil {
|
||||||
|
caps = append(caps, "dbus")
|
||||||
|
}
|
||||||
|
|
||||||
return Capabilities{Capabilities: caps}
|
return Capabilities{Capabilities: caps}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +533,14 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "clipboard")
|
caps = append(caps, "clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if themeModeManager != nil {
|
||||||
|
caps = append(caps, "theme.auto")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbusManager != nil {
|
||||||
|
caps = append(caps, "dbus")
|
||||||
|
}
|
||||||
|
|
||||||
return ServerInfo{
|
return ServerInfo{
|
||||||
APIVersion: APIVersion,
|
APIVersion: APIVersion,
|
||||||
CLIVersion: CLIVersion,
|
CLIVersion: CLIVersion,
|
||||||
@@ -766,6 +809,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldSubscribe("theme.auto") && themeModeManager != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto")
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer themeModeManager.Unsubscribe(clientID + "-theme-auto")
|
||||||
|
|
||||||
|
initialState := themeModeManager.GetState()
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state, ok := <-themeAutoChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
||||||
@@ -1133,6 +1208,31 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldSubscribe("dbus") && dbusManager != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
dbusChan := dbusManager.SubscribeSignals(dbusClientID)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer dbusManager.UnsubscribeSignals(dbusClientID)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-dbusChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "dbus", Data: event}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(eventChan)
|
close(eventChan)
|
||||||
@@ -1198,6 +1298,12 @@ func cleanupManagers() {
|
|||||||
if clipboardManager != nil {
|
if clipboardManager != nil {
|
||||||
clipboardManager.Close()
|
clipboardManager.Close()
|
||||||
}
|
}
|
||||||
|
if dbusManager != nil {
|
||||||
|
dbusManager.Close()
|
||||||
|
}
|
||||||
|
if themeModeManager != nil {
|
||||||
|
themeModeManager.Close()
|
||||||
|
}
|
||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
@@ -1293,6 +1399,15 @@ func Start(printDocs bool) error {
|
|||||||
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
|
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
|
||||||
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
|
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
|
||||||
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
|
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
|
||||||
|
log.Info("Theme automation:")
|
||||||
|
log.Info(" theme.auto.getState - Get current theme automation state")
|
||||||
|
log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)")
|
||||||
|
log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])")
|
||||||
|
log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)")
|
||||||
|
log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)")
|
||||||
|
log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)")
|
||||||
|
log.Info(" theme.auto.trigger - Trigger immediate re-evaluation")
|
||||||
|
log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)")
|
||||||
log.Info("Bluetooth:")
|
log.Info("Bluetooth:")
|
||||||
log.Info(" bluetooth.getState - Get current bluetooth state")
|
log.Info(" bluetooth.getState - Get current bluetooth state")
|
||||||
log.Info(" bluetooth.startDiscovery - Start device discovery")
|
log.Info(" bluetooth.startDiscovery - Start device discovery")
|
||||||
@@ -1450,6 +1565,12 @@ func Start(printDocs bool) error {
|
|||||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := InitializeThemeModeManager(); err != nil {
|
||||||
|
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
}
|
||||||
|
|
||||||
fatalErrChan := make(chan error, 1)
|
fatalErrChan := make(chan error, 1)
|
||||||
if wlrOutputManager != nil {
|
if wlrOutputManager != nil {
|
||||||
go func() {
|
go func() {
|
||||||
@@ -1490,6 +1611,14 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := InitializeDbusManager(); err != nil {
|
||||||
|
log.Warnf("DBus manager unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
log.Info("")
|
log.Info("")
|
||||||
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||||
|
|
||||||
|
|||||||
154
core/internal/server/thememode/handlers.go
Normal file
154
core/internal/server/thememode/handlers.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package thememode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
if manager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "theme mode manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Method {
|
||||||
|
case "theme.auto.getState":
|
||||||
|
handleGetState(conn, req, manager)
|
||||||
|
case "theme.auto.setEnabled":
|
||||||
|
handleSetEnabled(conn, req, manager)
|
||||||
|
case "theme.auto.setMode":
|
||||||
|
handleSetMode(conn, req, manager)
|
||||||
|
case "theme.auto.setSchedule":
|
||||||
|
handleSetSchedule(conn, req, manager)
|
||||||
|
case "theme.auto.setLocation":
|
||||||
|
handleSetLocation(conn, req, manager)
|
||||||
|
case "theme.auto.setUseIPLocation":
|
||||||
|
handleSetUseIPLocation(conn, req, manager)
|
||||||
|
case "theme.auto.trigger":
|
||||||
|
handleTrigger(conn, req, manager)
|
||||||
|
case "theme.auto.subscribe":
|
||||||
|
handleSubscribe(conn, req, manager)
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
models.Respond(conn, req.ID, manager.GetState())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
enabled, err := params.Bool(req.Params, "enabled")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetEnabled(enabled)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
mode, err := params.String(req.Params, "mode")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != "time" && mode != "location" {
|
||||||
|
models.RespondError(conn, req.ID, "invalid mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetMode(mode)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
startHour, err := params.Int(req.Params, "startHour")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startMinute, err := params.Int(req.Params, "startMinute")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endHour, err := params.Int(req.Params, "endHour")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endMinute, err := params.Int(req.Params, "endMinute")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetSchedule(startHour, startMinute, endHour, endMinute)
|
||||||
|
models.Respond(conn, req.ID, manager.GetState())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
lat, err := params.Float(req.Params, "latitude")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lon, err := params.Float(req.Params, "longitude")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetLocation(lat, lon)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
use, err := params.Bool(req.Params, "use")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetUseIPLocation(use)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
manager.TriggerUpdate()
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
clientID := fmt.Sprintf("client-%p", conn)
|
||||||
|
stateChan := manager.Subscribe(clientID)
|
||||||
|
defer manager.Unsubscribe(clientID)
|
||||||
|
|
||||||
|
initialState := manager.GetState()
|
||||||
|
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||||
|
ID: req.ID,
|
||||||
|
Result: &initialState,
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for state := range stateChan {
|
||||||
|
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||||
|
Result: &state,
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
432
core/internal/server/thememode/manager.go
Normal file
432
core/internal/server/thememode/manager.go
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
package thememode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultStartHour = 18
|
||||||
|
defaultStartMinute = 0
|
||||||
|
defaultEndHour = 6
|
||||||
|
defaultEndMinute = 0
|
||||||
|
defaultElevationTwilight = -6.0
|
||||||
|
defaultElevationDaylight = 3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
config Config
|
||||||
|
configMutex sync.RWMutex
|
||||||
|
|
||||||
|
state *State
|
||||||
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
|
subscribers syncmap.Map[string, chan State]
|
||||||
|
|
||||||
|
locationMutex sync.RWMutex
|
||||||
|
cachedIPLat *float64
|
||||||
|
cachedIPLon *float64
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
updateTrigger chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
m := &Manager{
|
||||||
|
config: Config{
|
||||||
|
Enabled: false,
|
||||||
|
Mode: "time",
|
||||||
|
StartHour: defaultStartHour,
|
||||||
|
StartMinute: defaultStartMinute,
|
||||||
|
EndHour: defaultEndHour,
|
||||||
|
EndMinute: defaultEndMinute,
|
||||||
|
ElevationTwilight: defaultElevationTwilight,
|
||||||
|
ElevationDaylight: defaultElevationDaylight,
|
||||||
|
},
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateState(time.Now())
|
||||||
|
|
||||||
|
m.wg.Add(1)
|
||||||
|
go m.schedulerLoop()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetState() State {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
defer m.stateMutex.RUnlock()
|
||||||
|
if m.state == nil {
|
||||||
|
return State{Config: m.getConfig()}
|
||||||
|
}
|
||||||
|
stateCopy := *m.state
|
||||||
|
return stateCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscribe(id string) chan State {
|
||||||
|
ch := make(chan State, 64)
|
||||||
|
m.subscribers.Store(id, ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Unsubscribe(id string) {
|
||||||
|
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||||
|
close(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetEnabled(enabled bool) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.Enabled == enabled {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.Enabled = enabled
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetMode(mode string) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.Mode == mode {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.Mode = mode
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
changed := m.config.StartHour != startHour ||
|
||||||
|
m.config.StartMinute != startMinute ||
|
||||||
|
m.config.EndHour != endHour ||
|
||||||
|
m.config.EndMinute != endMinute
|
||||||
|
if !changed {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.StartHour = startHour
|
||||||
|
m.config.StartMinute = startMinute
|
||||||
|
m.config.EndHour = endHour
|
||||||
|
m.config.EndMinute = endMinute
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetLocation(lat, lon float64) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.Latitude != nil && m.config.Longitude != nil &&
|
||||||
|
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.Latitude = &lat
|
||||||
|
m.config.Longitude = &lon
|
||||||
|
m.config.UseIPLocation = false
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
|
||||||
|
m.locationMutex.Lock()
|
||||||
|
m.cachedIPLat = nil
|
||||||
|
m.cachedIPLon = nil
|
||||||
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetUseIPLocation(use bool) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.UseIPLocation == use {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.UseIPLocation = use
|
||||||
|
if use {
|
||||||
|
m.config.Latitude = nil
|
||||||
|
m.config.Longitude = nil
|
||||||
|
}
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
|
||||||
|
if use {
|
||||||
|
m.locationMutex.Lock()
|
||||||
|
m.cachedIPLat = nil
|
||||||
|
m.cachedIPLon = nil
|
||||||
|
m.locationMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TriggerUpdate() {
|
||||||
|
select {
|
||||||
|
case m.updateTrigger <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(m.stopChan)
|
||||||
|
}
|
||||||
|
m.wg.Wait()
|
||||||
|
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||||
|
close(ch)
|
||||||
|
m.subscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) schedulerLoop() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
|
||||||
|
var timer *time.Timer
|
||||||
|
for {
|
||||||
|
config := m.getConfig()
|
||||||
|
now := time.Now()
|
||||||
|
var isLight bool
|
||||||
|
var next time.Time
|
||||||
|
if config.Enabled {
|
||||||
|
isLight, next = m.computeSchedule(now, config)
|
||||||
|
} else {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
if m.state != nil {
|
||||||
|
isLight = m.state.IsLight
|
||||||
|
}
|
||||||
|
m.stateMutex.RUnlock()
|
||||||
|
next = now.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateStateWithValues(config, isLight, next)
|
||||||
|
|
||||||
|
waitDur := time.Until(next)
|
||||||
|
if !config.Enabled {
|
||||||
|
waitDur = 24 * time.Hour
|
||||||
|
}
|
||||||
|
if waitDur < time.Second {
|
||||||
|
waitDur = time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if timer != nil {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
timer = time.NewTimer(waitDur)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
timer.Stop()
|
||||||
|
return
|
||||||
|
case <-m.updateTrigger:
|
||||||
|
timer.Stop()
|
||||||
|
continue
|
||||||
|
case <-timer.C:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) updateState(now time.Time) {
|
||||||
|
config := m.getConfig()
|
||||||
|
var isLight bool
|
||||||
|
var next time.Time
|
||||||
|
if config.Enabled {
|
||||||
|
isLight, next = m.computeSchedule(now, config)
|
||||||
|
} else {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
if m.state != nil {
|
||||||
|
isLight = m.state.IsLight
|
||||||
|
}
|
||||||
|
m.stateMutex.RUnlock()
|
||||||
|
next = now.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
m.updateStateWithValues(config, isLight, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
|
||||||
|
newState := State{
|
||||||
|
Config: config,
|
||||||
|
IsLight: isLight,
|
||||||
|
NextTransition: next,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
if m.state != nil && statesEqual(m.state, &newState) {
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.state = &newState
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
|
m.notifySubscribers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) notifySubscribers() {
|
||||||
|
state := m.GetState()
|
||||||
|
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||||
|
select {
|
||||||
|
case ch <- state:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getConfig() Config {
|
||||||
|
m.configMutex.RLock()
|
||||||
|
defer m.configMutex.RUnlock()
|
||||||
|
return m.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||||
|
if config.Latitude != nil && config.Longitude != nil {
|
||||||
|
return config.Latitude, config.Longitude
|
||||||
|
}
|
||||||
|
if !config.UseIPLocation {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.locationMutex.RLock()
|
||||||
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
|
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||||
|
m.locationMutex.RUnlock()
|
||||||
|
return lat, lon
|
||||||
|
}
|
||||||
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
|
lat, lon, err := wayland.FetchIPLocation()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.locationMutex.Lock()
|
||||||
|
m.cachedIPLat = lat
|
||||||
|
m.cachedIPLon = lon
|
||||||
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
|
return lat, lon
|
||||||
|
}
|
||||||
|
|
||||||
|
func statesEqual(a, b *State) bool {
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Config == b.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
|
if config.Mode == "location" {
|
||||||
|
return m.computeLocationSchedule(now, config)
|
||||||
|
}
|
||||||
|
return computeTimeSchedule(now, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
|
startMinutes := config.StartHour*60 + config.StartMinute
|
||||||
|
endMinutes := config.EndHour*60 + config.EndMinute
|
||||||
|
currentMinutes := now.Hour()*60 + now.Minute()
|
||||||
|
|
||||||
|
startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
|
||||||
|
endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
|
||||||
|
|
||||||
|
if startMinutes == endMinutes {
|
||||||
|
next := startTime
|
||||||
|
if !next.After(now) {
|
||||||
|
next = next.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
return true, next
|
||||||
|
}
|
||||||
|
|
||||||
|
if startMinutes < endMinutes {
|
||||||
|
if currentMinutes < startMinutes {
|
||||||
|
return true, startTime
|
||||||
|
}
|
||||||
|
if currentMinutes >= endMinutes {
|
||||||
|
return true, startTime.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
return false, endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentMinutes >= startMinutes {
|
||||||
|
return false, endTime.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
if currentMinutes < endMinutes {
|
||||||
|
return false, endTime
|
||||||
|
}
|
||||||
|
return true, startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
|
lat, lon := m.getLocation(config)
|
||||||
|
if lat == nil || lon == nil {
|
||||||
|
currentIsLight := false
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
if m.state != nil {
|
||||||
|
currentIsLight = m.state.IsLight
|
||||||
|
}
|
||||||
|
m.stateMutex.RUnlock()
|
||||||
|
return currentIsLight, now.Add(10 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
|
||||||
|
if cond != wayland.SunNormal {
|
||||||
|
if cond == wayland.SunMidnightSun {
|
||||||
|
return true, startOfNextDay(now)
|
||||||
|
}
|
||||||
|
return false, startOfNextDay(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.Before(times.Sunrise) {
|
||||||
|
return false, times.Sunrise
|
||||||
|
}
|
||||||
|
if now.Before(times.Sunset) {
|
||||||
|
return true, times.Sunset
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDay := startOfNextDay(now)
|
||||||
|
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
||||||
|
if nextCond != wayland.SunNormal {
|
||||||
|
if nextCond == wayland.SunMidnightSun {
|
||||||
|
return true, startOfNextDay(nextDay)
|
||||||
|
}
|
||||||
|
return false, startOfNextDay(nextDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nextTimes.Sunrise
|
||||||
|
}
|
||||||
|
|
||||||
|
func startOfNextDay(t time.Time) time.Time {
|
||||||
|
next := t.Add(24 * time.Hour)
|
||||||
|
return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHourMinute(hour, minute int) bool {
|
||||||
|
if hour < 0 || hour > 23 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if minute < 0 || minute > 59 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
|
||||||
|
if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
|
||||||
|
return errInvalidTime
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errInvalidTime = errors.New("invalid schedule time")
|
||||||
23
core/internal/server/thememode/types.go
Normal file
23
core/internal/server/thememode/types.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package thememode
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
StartHour int `json:"startHour"`
|
||||||
|
StartMinute int `json:"startMinute"`
|
||||||
|
EndHour int `json:"endHour"`
|
||||||
|
EndMinute int `json:"endMinute"`
|
||||||
|
Latitude *float64 `json:"latitude,omitempty"`
|
||||||
|
Longitude *float64 `json:"longitude,omitempty"`
|
||||||
|
UseIPLocation bool `json:"useIPLocation"`
|
||||||
|
ElevationTwilight float64 `json:"elevationTwilight"`
|
||||||
|
ElevationDaylight float64 `json:"elevationDaylight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Config Config `json:"config"`
|
||||||
|
IsLight bool `json:"isLight"`
|
||||||
|
NextTransition time.Time `json:"nextTransition"`
|
||||||
|
}
|
||||||
@@ -626,6 +626,7 @@ func (m *Manager) schedulerLoop() {
|
|||||||
m.schedule.calcDay = time.Time{}
|
m.schedule.calcDay = time.Time{}
|
||||||
m.scheduleMutex.Unlock()
|
m.scheduleMutex.Unlock()
|
||||||
m.recalcSchedule(time.Now())
|
m.recalcSchedule(time.Now())
|
||||||
|
m.updateStateFromSchedule()
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|||||||
@@ -124,27 +124,23 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
sc.drainCmdQueue()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sc.stopChan:
|
case <-sc.stopChan:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
sc.drainCmdQueue()
|
_, err := unix.Poll(pollFds, -1)
|
||||||
|
switch {
|
||||||
n, err := unix.Poll(pollFds, 50)
|
case err == unix.EINTR:
|
||||||
if err != nil {
|
continue
|
||||||
if err == unix.EINTR {
|
case err != nil:
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Errorf("Poll error: %v", err)
|
log.Errorf("Poll error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if pollFds[1].Revents&unix.POLLIN != 0 {
|
if pollFds[1].Revents&unix.POLLIN != 0 {
|
||||||
var buf [64]byte
|
var buf [64]byte
|
||||||
if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN {
|
if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN {
|
||||||
@@ -152,13 +148,13 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pollFds[0].Revents&unix.POLLIN != 0 {
|
if pollFds[0].Revents&unix.POLLIN == 0 {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
continue
|
||||||
if !os.IsTimeout(err) {
|
}
|
||||||
log.Errorf("Wayland connection error: %v", err)
|
|
||||||
return
|
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
||||||
}
|
log.Errorf("Wayland connection error: %v", err)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,12 +172,16 @@ func (sc *SharedContext) drainCmdQueue() {
|
|||||||
|
|
||||||
func (sc *SharedContext) Close() {
|
func (sc *SharedContext) Close() {
|
||||||
close(sc.stopChan)
|
close(sc.stopChan)
|
||||||
|
if _, err := unix.Write(sc.wakeW, []byte{1}); err != nil && err != unix.EAGAIN {
|
||||||
|
log.Errorf("wake pipe write error on close: %v", err)
|
||||||
|
}
|
||||||
sc.wg.Wait()
|
sc.wg.Wait()
|
||||||
|
|
||||||
unix.Close(sc.wakeR)
|
unix.Close(sc.wakeR)
|
||||||
unix.Close(sc.wakeW)
|
unix.Close(sc.wakeW)
|
||||||
|
|
||||||
if sc.display != nil {
|
if sc.display == nil {
|
||||||
sc.display.Context().Close()
|
return
|
||||||
}
|
}
|
||||||
|
sc.display.Context().Close()
|
||||||
}
|
}
|
||||||
|
|||||||
20
core/internal/utils/dbus.go
Normal file
20
core/internal/utils/dbus.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsDBusServiceAvailable(busName string) bool {
|
||||||
|
conn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
||||||
|
var owned bool
|
||||||
|
if err := obj.Call("org.freedesktop.DBus.NameHasOwner", 0, busName).Store(&owned); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return owned
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppChecker interface {
|
type AppChecker interface {
|
||||||
@@ -43,16 +42,3 @@ func AnyCommandExists(cmds ...string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsServiceActive(name string, userService bool) bool {
|
|
||||||
if !CommandExists("systemctl") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"is-active", name}
|
|
||||||
if userService {
|
|
||||||
args = []string{"--user", "is-active", name}
|
|
||||||
}
|
|
||||||
output, _ := exec.Command("systemctl", args...).Output()
|
|
||||||
return strings.EqualFold(strings.TrimSpace(string(output)), "active")
|
|
||||||
}
|
|
||||||
|
|||||||
69
core/pkg/dbusutil/variant.go
Normal file
69
core/pkg/dbusutil/variant.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package dbusutil
|
||||||
|
|
||||||
|
import "github.com/godbus/dbus/v5"
|
||||||
|
|
||||||
|
func As[T any](v dbus.Variant) (T, bool) {
|
||||||
|
val, ok := v.Value().(T)
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func AsOr[T any](v dbus.Variant, def T) T {
|
||||||
|
if val, ok := v.Value().(T); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get[T any](m map[string]dbus.Variant, key string) (T, bool) {
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok {
|
||||||
|
var zero T
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
return As[T](v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOr[T any](m map[string]dbus.Variant, key string, def T) T {
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return AsOr(v, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Normalize(v any) any {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case dbus.Variant:
|
||||||
|
return Normalize(val.Value())
|
||||||
|
case dbus.ObjectPath:
|
||||||
|
return string(val)
|
||||||
|
case []dbus.ObjectPath:
|
||||||
|
result := make([]string, len(val))
|
||||||
|
for i, p := range val {
|
||||||
|
result[i] = string(p)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
case map[string]dbus.Variant:
|
||||||
|
result := make(map[string]any)
|
||||||
|
for k, vv := range val {
|
||||||
|
result[k] = Normalize(vv.Value())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
case []any:
|
||||||
|
result := make([]any, len(val))
|
||||||
|
for i, item := range val {
|
||||||
|
result[i] = Normalize(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeSlice(values []any) []any {
|
||||||
|
result := make([]any, len(values))
|
||||||
|
for i, v := range values {
|
||||||
|
result[i] = Normalize(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
155
core/pkg/dbusutil/variant_test.go
Normal file
155
core/pkg/dbusutil/variant_test.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package dbusutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAs(t *testing.T) {
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant("hello")
|
||||||
|
val, ok := As[string](v)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "hello", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bool", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant(true)
|
||||||
|
val, ok := As[bool](v)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.True(t, val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int32", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant(int32(42))
|
||||||
|
val, ok := As[int32](v)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, int32(42), val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong type", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant("hello")
|
||||||
|
_, ok := As[int](v)
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsOr(t *testing.T) {
|
||||||
|
t.Run("exists", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant("hello")
|
||||||
|
val := AsOr(v, "default")
|
||||||
|
assert.Equal(t, "hello", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong type uses default", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant(123)
|
||||||
|
val := AsOr(v, "default")
|
||||||
|
assert.Equal(t, "default", val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
m := map[string]dbus.Variant{
|
||||||
|
"name": dbus.MakeVariant("test"),
|
||||||
|
"enabled": dbus.MakeVariant(true),
|
||||||
|
"count": dbus.MakeVariant(int32(5)),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("exists", func(t *testing.T) {
|
||||||
|
val, ok := Get[string](m, "name")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "test", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing key", func(t *testing.T) {
|
||||||
|
_, ok := Get[string](m, "missing")
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong type", func(t *testing.T) {
|
||||||
|
_, ok := Get[int](m, "name")
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOr(t *testing.T) {
|
||||||
|
m := map[string]dbus.Variant{
|
||||||
|
"name": dbus.MakeVariant("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("exists", func(t *testing.T) {
|
||||||
|
val := GetOr(m, "name", "default")
|
||||||
|
assert.Equal(t, "test", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing uses default", func(t *testing.T) {
|
||||||
|
val := GetOr(m, "missing", "default")
|
||||||
|
assert.Equal(t, "default", val)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong type uses default", func(t *testing.T) {
|
||||||
|
val := GetOr(m, "name", 42)
|
||||||
|
assert.Equal(t, 42, val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalize(t *testing.T) {
|
||||||
|
t.Run("variant unwrap", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant("hello")
|
||||||
|
result := Normalize(v)
|
||||||
|
assert.Equal(t, "hello", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nested variant", func(t *testing.T) {
|
||||||
|
v := dbus.MakeVariant(dbus.MakeVariant("nested"))
|
||||||
|
result := Normalize(v)
|
||||||
|
assert.Equal(t, "nested", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("object path", func(t *testing.T) {
|
||||||
|
v := dbus.ObjectPath("/org/test")
|
||||||
|
result := Normalize(v)
|
||||||
|
assert.Equal(t, "/org/test", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("object path slice", func(t *testing.T) {
|
||||||
|
v := []dbus.ObjectPath{"/org/a", "/org/b"}
|
||||||
|
result := Normalize(v)
|
||||||
|
assert.Equal(t, []string{"/org/a", "/org/b"}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("variant map", func(t *testing.T) {
|
||||||
|
v := map[string]dbus.Variant{
|
||||||
|
"key": dbus.MakeVariant("value"),
|
||||||
|
}
|
||||||
|
result := Normalize(v)
|
||||||
|
expected := map[string]any{"key": "value"}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("any slice", func(t *testing.T) {
|
||||||
|
v := []any{dbus.MakeVariant("a"), dbus.ObjectPath("/b")}
|
||||||
|
result := Normalize(v)
|
||||||
|
expected := []any{"a", "/b"}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("passthrough primitives", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "hello", Normalize("hello"))
|
||||||
|
assert.Equal(t, 42, Normalize(42))
|
||||||
|
assert.Equal(t, true, Normalize(true))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSlice(t *testing.T) {
|
||||||
|
input := []any{
|
||||||
|
dbus.MakeVariant("a"),
|
||||||
|
dbus.ObjectPath("/b"),
|
||||||
|
"c",
|
||||||
|
}
|
||||||
|
result := NormalizeSlice(input)
|
||||||
|
expected := []any{"a", "/b", "c"}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ func (c *CUPSClient) RejectJobs(printer string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPrinterToClass adds a printer to a class, if the class does not exists it will be crated
|
// AddPrinterToClass adds a printer to a class, if the class does not exists it will be created
|
||||||
func (c *CUPSClient) AddPrinterToClass(class, printer string) error {
|
func (c *CUPSClient) AddPrinterToClass(class, printer string) error {
|
||||||
attributes, err := c.GetPrinterAttributes(class, []string{AttributeMemberURIs})
|
attributes, err := c.GetPrinterAttributes(class, []string{AttributeMemberURIs})
|
||||||
if err != nil && !IsNotExistsError(err) {
|
if err != nil && !IsNotExistsError(err) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell-git | quickshell,
|
quickshell-git | quickshell,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Provides: dms
|
Provides: dms
|
||||||
Conflicts: dms
|
Conflicts: dms
|
||||||
Replaces: dms
|
Replaces: dms
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Conflicts: dms-git
|
Conflicts: dms-git
|
||||||
Replaces: dms-git
|
Replaces: dms-git
|
||||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ Recommends: cava
|
|||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: quickshell-git
|
Recommends: quickshell-git
|
||||||
Recommends: wl-clipboard
|
|
||||||
|
|
||||||
# Recommended system packages
|
# Recommended system packages
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
|
|||||||
@@ -24,10 +24,8 @@ Requires: dms-cli = %{version}-%{release}
|
|||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ in
|
|||||||
]
|
]
|
||||||
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
|
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
|
||||||
++ lib.optional cfg.enableAudioWavelength pkgs.cava
|
++ lib.optional cfg.enableAudioWavelength pkgs.cava
|
||||||
++ lib.optional cfg.enableCalendarEvents pkgs.khal;
|
++ lib.optional cfg.enableCalendarEvents pkgs.khal
|
||||||
|
++ lib.optional cfg.enableClipboardPaste pkgs.wtype;
|
||||||
|
|
||||||
plugins = lib.mapAttrs (name: plugin: {
|
plugins = lib.mapAttrs (name: plugin: {
|
||||||
source = plugin.src;
|
source = plugin.src;
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ let
|
|||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
|
compositorPackage =
|
||||||
|
let
|
||||||
|
configured = lib.attrByPath [ "programs" cfg.compositor.name "package" ] null config;
|
||||||
|
in
|
||||||
|
if configured != null then configured else builtins.getAttr cfg.compositor.name pkgs;
|
||||||
|
|
||||||
cacheDir = "/var/lib/dms-greeter";
|
cacheDir = "/var/lib/dms-greeter";
|
||||||
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
|
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
|
||||||
export PATH=$PATH:${
|
export PATH=$PATH:${
|
||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
config.programs.${cfg.compositor.name}.package
|
compositorPackage
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -64,6 +70,7 @@ in
|
|||||||
"niri"
|
"niri"
|
||||||
"hyprland"
|
"hyprland"
|
||||||
"sway"
|
"sway"
|
||||||
|
"labwc"
|
||||||
];
|
];
|
||||||
description = "Compositor to run greeter in";
|
description = "Compositor to run greeter in";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ in
|
|||||||
default = hasPluginSettings;
|
default = hasPluginSettings;
|
||||||
description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.'';
|
description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.target = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = config.wayland.systemd.target;
|
||||||
|
defaultText = lib.literalExpression "config.wayland.systemd.target";
|
||||||
|
description = "Systemd target to bind to.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -84,8 +91,8 @@ in
|
|||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
Unit = {
|
Unit = {
|
||||||
Description = "DankMaterialShell";
|
Description = "DankMaterialShell";
|
||||||
PartOf = [ config.wayland.systemd.target ];
|
PartOf = [ cfg.systemd.target ];
|
||||||
After = [ config.wayland.systemd.target ];
|
After = [ cfg.systemd.target ];
|
||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
@@ -93,7 +100,7 @@ in
|
|||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
Install.WantedBy = [ config.wayland.systemd.target ];
|
Install.WantedBy = [ cfg.systemd.target ];
|
||||||
};
|
};
|
||||||
|
|
||||||
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
|
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ in
|
|||||||
imports = [
|
imports = [
|
||||||
(import ./options.nix args)
|
(import ./options.nix args)
|
||||||
];
|
];
|
||||||
|
options.programs.dank-material-shell.systemd.target = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Systemd target to bind to.";
|
||||||
|
default = "graphical-session.target";
|
||||||
|
};
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
description = "DankMaterialShell";
|
description = "DankMaterialShell";
|
||||||
path = lib.mkForce [ ];
|
path = lib.mkForce [ ];
|
||||||
|
|
||||||
partOf = [ "graphical-session.target" ];
|
partOf = [ cfg.systemd.target ];
|
||||||
after = [ "graphical-session.target" ];
|
after = [ cfg.systemd.target ];
|
||||||
wantedBy = [ "graphical-session.target" ];
|
wantedBy = [ cfg.systemd.target ];
|
||||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ in
|
|||||||
description = "Add calendar events support via khal";
|
description = "Add calendar events support via khal";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enableClipboardPaste = lib.mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Adds needed dependencies for directly pasting items from the clipboard history.";
|
||||||
|
};
|
||||||
|
|
||||||
quickshell = {
|
quickshell = {
|
||||||
package = lib.mkPackageOption dmsPkgs "quickshell" {
|
package = lib.mkPackageOption dmsPkgs "quickshell" {
|
||||||
extraDescription = "The quickshell package to use (defaults to be built from source, due to unreleased features used by DMS).";
|
extraDescription = "The quickshell package to use (defaults to be built from source, due to unreleased features used by DMS).";
|
||||||
|
|||||||
@@ -20,12 +20,9 @@ Requires: accountsservice
|
|||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: quickshell-git
|
Recommends: quickshell-git
|
||||||
Recommends: wl-clipboard
|
|
||||||
|
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ Requires: dgop
|
|||||||
|
|
||||||
# Core utilities (Highly recommended for DMS functionality)
|
# Core utilities (Highly recommended for DMS functionality)
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
Recommends: wl-clipboard
|
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell-git | quickshell,
|
quickshell-git | quickshell,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -29,8 +28,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Provides: dms
|
Provides: dms
|
||||||
Conflicts: dms
|
Conflicts: dms
|
||||||
Replaces: dms
|
Replaces: dms
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Depends: ${misc:Depends},
|
|||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
cava,
|
cava,
|
||||||
cliphist,
|
|
||||||
danksearch,
|
danksearch,
|
||||||
dgop,
|
dgop,
|
||||||
matugen,
|
matugen,
|
||||||
@@ -28,8 +27,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-layouts,
|
qml6-module-qtquick-layouts,
|
||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct,
|
qt6ct
|
||||||
wl-clipboard
|
|
||||||
Conflicts: dms-git
|
Conflicts: dms-git
|
||||||
Replaces: dms-git
|
Replaces: dms-git
|
||||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766651565,
|
"lastModified": 1769018530,
|
||||||
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
|
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
|
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
17
flake.nix
17
flake.nix
@@ -47,6 +47,7 @@
|
|||||||
kirigami.unwrapped
|
kirigami.unwrapped
|
||||||
sonnet
|
sonnet
|
||||||
qtmultimedia
|
qtmultimedia
|
||||||
|
qtimageformats
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -61,11 +62,13 @@
|
|||||||
(builtins.substring 6 2 longDate)
|
(builtins.substring 6 2 longDate)
|
||||||
];
|
];
|
||||||
version =
|
version =
|
||||||
pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION))
|
let
|
||||||
+ "+date="
|
rawVersion = pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION));
|
||||||
+ mkDate (self.lastModifiedDate or "19700101")
|
cleanVersion = builtins.replaceStrings [ " " ] [ "" ] rawVersion;
|
||||||
+ "_"
|
dateSuffix = "+date=" + mkDate (self.lastModifiedDate or "19700101");
|
||||||
+ (self.shortRev or "dirty");
|
revSuffix = "_" + (self.shortRev or "dirty");
|
||||||
|
in
|
||||||
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.buildGoModule (
|
dms-shell = pkgs.buildGoModule (
|
||||||
@@ -76,14 +79,14 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
pname = "dms-shell";
|
pname = "dms-shell";
|
||||||
src = ./core;
|
src = ./core;
|
||||||
vendorHash = "sha256-9CnZFtjXXWYELRiBX2UbZvWopnl9Y1ILuK+xP6YQZ9U=";
|
vendorHash = "sha256-kWHB/FN6Z2Ydh+VvNrDnbg18RuJSDAle4DHDAP4NpNk=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X main.Version=${version}"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Spicy Miso
|
Saffron Bloom
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var facts: [
|
|
||||||
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
|
|
||||||
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
|
|
||||||
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
|
|
||||||
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
|
|
||||||
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
|
|
||||||
"There's a nebula out there that's actually colder than empty space itself.",
|
|
||||||
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
|
|
||||||
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
|
|
||||||
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
|
|
||||||
"Distant galaxies can move away from us faster than light because space itself is stretching.",
|
|
||||||
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
|
|
||||||
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
|
|
||||||
"A day on Venus lasts longer than its entire year around the Sun.",
|
|
||||||
"On Mercury, the time between sunrises is 176 Earth days long.",
|
|
||||||
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
|
|
||||||
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
|
|
||||||
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
|
|
||||||
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
|
|
||||||
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
|
|
||||||
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
|
|
||||||
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
|
|
||||||
"Counting to a billion at one number per second would take over 31 years.",
|
|
||||||
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
|
|
||||||
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
|
|
||||||
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
|
|
||||||
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
|
|
||||||
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
|
|
||||||
"Only around 5% of galaxies are ever reachable—even at light-speed.",
|
|
||||||
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
|
|
||||||
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
|
|
||||||
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
|
|
||||||
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
|
|
||||||
"The Moon moves 3.8 centimeters farther from Earth every year.",
|
|
||||||
"The universe creates 275 million new stars every single day.",
|
|
||||||
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
|
|
||||||
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
|
|
||||||
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
|
|
||||||
]
|
|
||||||
|
|
||||||
function getRandomFact() {
|
|
||||||
return facts[Math.floor(Math.random() * facts.length)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,9 @@ const KEY_MAP = {
|
|||||||
16777349: "XF86AudioMedia",
|
16777349: "XF86AudioMedia",
|
||||||
16777350: "XF86AudioRecord",
|
16777350: "XF86AudioRecord",
|
||||||
16842798: "XF86MonBrightnessUp",
|
16842798: "XF86MonBrightnessUp",
|
||||||
|
16777394: "XF86MonBrightnessUp",
|
||||||
16842797: "XF86MonBrightnessDown",
|
16842797: "XF86MonBrightnessDown",
|
||||||
|
16777395: "XF86MonBrightnessDown",
|
||||||
16842800: "XF86KbdBrightnessUp",
|
16842800: "XF86KbdBrightnessUp",
|
||||||
16842799: "XF86KbdBrightnessDown",
|
16842799: "XF86KbdBrightnessDown",
|
||||||
16842796: "XF86PowerOff",
|
16842796: "XF86PowerOff",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user