mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-27 15:02:50 -05:00
Compare commits
2 Commits
master
...
da006b883e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da006b883e | ||
|
|
e71fb09cbd |
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Crashes or unexpected behaviors
|
||||||
|
title: ""
|
||||||
|
labels: "bug"
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- If your issue is related to ICONS
|
||||||
|
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||||
|
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||||
|
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
||||||
|
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||||
|
|
||||||
|
## Compositor
|
||||||
|
|
||||||
|
- [ ] niri
|
||||||
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
|
- [ ] Other (specify)
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
<!-- Arch, Fedora, Debian, etc. -->
|
||||||
|
|
||||||
|
## dms version
|
||||||
|
|
||||||
|
<!-- Output of dms version command -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- Brief description of the issue -->
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
<!-- Describe what you expected to happen -->
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
<!-- Please provide detailed steps to reproduce the issue -->
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Error Messages/Logs
|
||||||
|
|
||||||
|
<!-- Please include any error messages, stack traces, or relevant logs -->
|
||||||
|
<!-- you can get a log file with the following steps:
|
||||||
|
dms kill
|
||||||
|
mkdir ~/dms_logs
|
||||||
|
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
|
||||||
|
|
||||||
|
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
```
|
||||||
|
Paste error messages or logs here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots/Recordings
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots or screen recordings -->
|
||||||
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,96 +0,0 @@
|
|||||||
name: Bug Report
|
|
||||||
description: Crashes or unexpected behaviors
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## DankMaterialShell Bug Report
|
|
||||||
Limit your report to one issue per submission unless closely related
|
|
||||||
- type: checkboxes
|
|
||||||
id: compositor
|
|
||||||
attributes:
|
|
||||||
label: Compositor
|
|
||||||
options:
|
|
||||||
- label: Niri
|
|
||||||
- label: Hyprland
|
|
||||||
- label: MangoWC (dwl)
|
|
||||||
- label: Sway
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
id: distribution
|
|
||||||
attributes:
|
|
||||||
label: Distribution
|
|
||||||
options:
|
|
||||||
- label: Arch Linux
|
|
||||||
- label: CachyOS
|
|
||||||
- label: Fedora
|
|
||||||
- label: NixOS
|
|
||||||
- label: Debian
|
|
||||||
- label: Ubuntu
|
|
||||||
- label: Gentoo
|
|
||||||
- label: OpenSUSE
|
|
||||||
- label: Other (specify below)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: distribution_other
|
|
||||||
attributes:
|
|
||||||
label: If Other, please specify
|
|
||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: dms_doctor
|
|
||||||
attributes:
|
|
||||||
label: dms doctor -v
|
|
||||||
description: Output of `dms doctor -v` command
|
|
||||||
placeholder: Paste the output of `dms doctor -v` here
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: Brief description of the issue
|
|
||||||
placeholder: What happened?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected_behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected Behavior
|
|
||||||
description: What did you expect to happen?
|
|
||||||
placeholder: Describe the expected behavior
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: steps_to_reproduce
|
|
||||||
attributes:
|
|
||||||
label: Steps to Reproduce & Installation Method
|
|
||||||
description: Please provide detailed steps to reproduce the issue
|
|
||||||
placeholder: |
|
|
||||||
1. ...
|
|
||||||
2. ...
|
|
||||||
3. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Error Messages/Logs
|
|
||||||
description: Please include any error messages, stack traces, or relevant logs
|
|
||||||
placeholder: |
|
|
||||||
Paste error messages or logs here
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots/Recordings
|
|
||||||
description: If applicable, add screenshots or screen recordings
|
|
||||||
placeholder: Attach images or videos here
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Request a Feature
|
||||||
|
about: New widgets, new widget behavior, etc.
|
||||||
|
title: ""
|
||||||
|
labels: "enhancement"
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
|
||||||
|
<!-- Brief description of the feature requested -->
|
||||||
|
|
||||||
|
## Use Case
|
||||||
|
|
||||||
|
<!-- Explain the purpose of this feature/why it'd be useful to you -->
|
||||||
|
|
||||||
|
## Compositor
|
||||||
|
|
||||||
|
Is this feature specific to one compositor?
|
||||||
|
|
||||||
|
- [ ] All compositors
|
||||||
|
- [ ] niri
|
||||||
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!-- If you have any ideas for how to implement this, please share! -->
|
||||||
|
|
||||||
|
## Alternatives/Existing Solutions
|
||||||
|
|
||||||
|
<!-- Include any similar/pre-existing products that solve this problem -->
|
||||||
55
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
55
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,55 +0,0 @@
|
|||||||
name: Feature Request
|
|
||||||
description: Suggest a new feature or improvement for DMS. Keep features focused on a single topic with clear benefits, examples, etc. Avoid vague or broad requests, they will be closed.
|
|
||||||
labels:
|
|
||||||
- enhancement
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## DankMaterialShell Feature Request
|
|
||||||
- type: textarea
|
|
||||||
id: feature_description
|
|
||||||
attributes:
|
|
||||||
label: Feature Description
|
|
||||||
description: Brief description of the feature requested
|
|
||||||
placeholder: What feature would you like to see?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: use_case
|
|
||||||
attributes:
|
|
||||||
label: Use Case
|
|
||||||
description: Explain the purpose of this feature/why it'd be useful to you
|
|
||||||
placeholder: Why is this feature important?
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
id: compositor
|
|
||||||
attributes:
|
|
||||||
label: Compositor(s)
|
|
||||||
description: Is this feature specific to one or more compositors?
|
|
||||||
options:
|
|
||||||
- label: All compositors
|
|
||||||
- label: Niri
|
|
||||||
- label: Hyprland
|
|
||||||
- label: MangoWC (dwl)
|
|
||||||
- label: Sway
|
|
||||||
- label: Other (specify below)
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: proposed_solution
|
|
||||||
attributes:
|
|
||||||
label: Proposed Solution
|
|
||||||
description: If you have any ideas for how to implement this, please share!
|
|
||||||
placeholder: Suggest a solution or approach
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: Alternatives/Existing Solutions
|
|
||||||
description: Include any similar/pre-existing products that solve this problem
|
|
||||||
placeholder: List alternatives or existing solutions
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Request Assistance or Support
|
||||||
|
about: Help with installation, usage, or general questions.
|
||||||
|
title: ""
|
||||||
|
labels: "support"
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compositor
|
||||||
|
|
||||||
|
- [ ] niri
|
||||||
|
- [ ] Hyprland
|
||||||
|
- [ ] dwl (MangoWC)
|
||||||
|
- [ ] sway
|
||||||
|
- [ ] other
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
<!-- Arch, Fedora, Debian, etc. -->
|
||||||
|
|
||||||
|
## dms version
|
||||||
|
|
||||||
|
<!-- Output of dms version command -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- Brief description of the support needed -->
|
||||||
|
|
||||||
|
## Solutions Tried
|
||||||
|
|
||||||
|
<!-- Describe what you've tried so far -->
|
||||||
|
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
|
||||||
|
|
||||||
|
## Configuration Details
|
||||||
|
|
||||||
|
<!-- Include any configuration if relevant -->
|
||||||
|
|
||||||
|
## Screenshots/Recordings
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots or screen recordings -->
|
||||||
69
.github/ISSUE_TEMPLATE/support_request.yml
vendored
69
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -1,69 +0,0 @@
|
|||||||
name: Support Request
|
|
||||||
description: Help with installation, usage, or general questions about DankMaterialShell
|
|
||||||
labels:
|
|
||||||
- support
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## DankMaterialShell Support Request
|
|
||||||
- type: checkboxes
|
|
||||||
id: compositor
|
|
||||||
attributes:
|
|
||||||
label: Compositor
|
|
||||||
options:
|
|
||||||
- label: Niri
|
|
||||||
- label: Hyprland
|
|
||||||
- label: MangoWC (dwl)
|
|
||||||
- label: Sway
|
|
||||||
- label: Other (specify below)
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: distribution
|
|
||||||
attributes:
|
|
||||||
label: Distribution
|
|
||||||
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
|
||||||
placeholder: Your Linux distribution
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: dms_doctor
|
|
||||||
attributes:
|
|
||||||
label: dms doctor -v
|
|
||||||
description: Output of `dms doctor -v` command
|
|
||||||
placeholder: Paste the output of `dms doctor -v` here
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: Brief description of the support needed
|
|
||||||
placeholder: What do you need help with?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: solutions_tried
|
|
||||||
attributes:
|
|
||||||
label: Solutions Tried
|
|
||||||
description: Describe what you've tried so far (commands, documentation, etc.)
|
|
||||||
placeholder: List steps or resources you've already tried
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: configuration
|
|
||||||
attributes:
|
|
||||||
label: Configuration Details
|
|
||||||
description: Include any relevant configuration if relevant
|
|
||||||
placeholder: Add configuration or environment info
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots/Recordings
|
|
||||||
description: If applicable, add screenshots or screen recordings
|
|
||||||
placeholder: Attach images or videos here
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
9
.github/workflows/go-ci.yml
vendored
9
.github/workflows/go-ci.yml
vendored
@@ -28,15 +28,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install flatpak
|
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
|
||||||
|
|
||||||
- name: Add flathub
|
|
||||||
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
||||||
|
|
||||||
- name: Add a flatpak that mutagen could support
|
|
||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
14
.github/workflows/prek.yml
vendored
14
.github/workflows/prek.yml
vendored
@@ -11,19 +11,5 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install flatpak
|
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
|
||||||
|
|
||||||
- name: Add flathub
|
|
||||||
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
||||||
|
|
||||||
- name: Add a flatpak that mutagen could support
|
|
||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: core/go.mod
|
|
||||||
|
|
||||||
- name: run pre-commit hooks
|
- name: run pre-commit hooks
|
||||||
uses: j178/prek-action@v1
|
uses: j178/prek-action@v1
|
||||||
|
|||||||
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,14 +4,13 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to update"
|
description: "Package to update (dms, dms-git, or all)"
|
||||||
required: true
|
required: false
|
||||||
type: choice
|
default: "all"
|
||||||
options:
|
tag_version:
|
||||||
- dms
|
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
||||||
- dms-git
|
required: false
|
||||||
- all
|
default: ""
|
||||||
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
|
||||||
@@ -57,9 +56,8 @@ 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() {
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\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 "")
|
||||||
|
|
||||||
@@ -75,8 +73,8 @@ jobs:
|
|||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
# Tag selected or pushed - always update stable package
|
# Tag push - 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
|
||||||
@@ -106,12 +104,7 @@ 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")
|
||||||
if check_dms_stable; then
|
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
||||||
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
|
||||||
@@ -136,9 +129,6 @@ 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
|
||||||
@@ -171,19 +161,12 @@ jobs:
|
|||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
# Tag push event - use the pushed tag
|
||||||
# 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 "Using tag from GITHUB_REF: $VERSION"
|
echo "Triggered by tag: $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
|
||||||
@@ -193,28 +176,22 @@ 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
|
||||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
# For explicit dms selection, require tag_version
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${{ github.event.inputs.tag_version }}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
echo "Using specified tag: $VERSION"
|
||||||
else
|
else
|
||||||
# Auto-detect latest release for dms
|
echo "ERROR: tag_version is required when package=dms"
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
exit 1
|
||||||
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
|
||||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
# For "all", auto-detect if tag_version not specified
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${{ github.event.inputs.tag_version }}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
echo "Using specified tag: $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 "")
|
||||||
@@ -229,7 +206,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 }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; 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
|
||||||
@@ -238,9 +215,6 @@ 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
|
||||||
|
|||||||
20
.github/workflows/stable.yml
vendored
20
.github/workflows/stable.yml
vendored
@@ -5,27 +5,15 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-stable:
|
update-stable:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Create GitHub App token
|
- uses: actions/checkout@v4
|
||||||
id: app_token
|
|
||||||
uses: actions/create-github-app-token@v1
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.APP_ID }}
|
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Push to stable branch
|
- name: Push to stable branch
|
||||||
env:
|
run: git push origin HEAD:refs/heads/stable --force
|
||||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
|
||||||
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -108,5 +108,3 @@ bin/
|
|||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
quickshell/dms-plugins
|
|
||||||
__pycache__
|
|
||||||
|
|||||||
@@ -10,11 +10,3 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: go-mod-tidy
|
|
||||||
name: go mod tidy
|
|
||||||
entry: bash -c 'cd core && go mod tidy'
|
|
||||||
language: system
|
|
||||||
files: ^core/.*\.(go|mod|sum)$
|
|
||||||
pass_filenames: false
|
|
||||||
|
|||||||
15
CHANGELOG.MD
15
CHANGELOG.MD
@@ -1,14 +1,5 @@
|
|||||||
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
|
||||||
@@ -24,9 +15,3 @@ This file is more of a quick reference so I know what to account for before next
|
|||||||
- new IPC targets
|
- new IPC targets
|
||||||
- Initial RTL support/i18n
|
- Initial RTL support/i18n
|
||||||
- Theme registry
|
- Theme registry
|
||||||
- Notification persistence & history
|
|
||||||
- **BREAKING** vscode theme needs re-installed
|
|
||||||
- dms doctor cmd
|
|
||||||
- niri/hypr/mango gaps/window/border overrides
|
|
||||||
- settings search
|
|
||||||
- notification display ops on lock screen
|
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -43,6 +43,7 @@ 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:
|
||||||
|
|||||||
@@ -68,9 +68,3 @@ packages:
|
|||||||
outpkg: mocks_wlclient
|
outpkg: mocks_wlclient
|
||||||
interfaces:
|
interfaces:
|
||||||
WaylandDisplay:
|
WaylandDisplay:
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/utils"
|
|
||||||
outpkg: mocks_utils
|
|
||||||
interfaces:
|
|
||||||
AppChecker:
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
||||||
|
|
||||||
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
||||||
for range sepLen {
|
for i := 0; i < sepLen; i++ {
|
||||||
fmt.Print("─")
|
fmt.Print("─")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
@@ -1,300 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
|
||||||
_ "golang.org/x/image/bmp"
|
|
||||||
_ "golang.org/x/image/tiff"
|
|
||||||
_ "golang.org/x/image/webp"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
@@ -52,7 +36,6 @@ var (
|
|||||||
clipCopyForeground bool
|
clipCopyForeground bool
|
||||||
clipCopyPasteOnce bool
|
clipCopyPasteOnce bool
|
||||||
clipCopyType string
|
clipCopyType string
|
||||||
clipCopyDownload bool
|
|
||||||
clipJSONOutput bool
|
clipJSONOutput bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -159,42 +142,19 @@ var (
|
|||||||
clipConfigNoClearStartup bool
|
clipConfigNoClearStartup bool
|
||||||
clipConfigDisabled bool
|
clipConfigDisabled bool
|
||||||
clipConfigEnabled bool
|
clipConfigEnabled bool
|
||||||
|
clipConfigDisableHistory bool
|
||||||
|
clipConfigEnableHistory bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var clipExportCmd = &cobra.Command{
|
|
||||||
Use: "export [file]",
|
|
||||||
Short: "Export clipboard history to JSON",
|
|
||||||
Long: "Export clipboard history to JSON file. If no file specified, writes to stdout.",
|
|
||||||
Run: runClipExport,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipImportCmd = &cobra.Command{
|
|
||||||
Use: "import <file>",
|
|
||||||
Short: "Import clipboard history from JSON",
|
|
||||||
Long: "Import clipboard history from JSON file exported by 'dms cl export'.",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runClipImport,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipMigrateCmd = &cobra.Command{
|
|
||||||
Use: "cliphist-migrate [db-path]",
|
|
||||||
Short: "Migrate from cliphist",
|
|
||||||
Long: "Migrate clipboard history from cliphist. Uses default cliphist path if not specified.",
|
|
||||||
Run: runClipMigrate,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipMigrateDelete bool
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
|
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
||||||
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file")
|
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "C", false, "Copy entry to clipboard")
|
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "c", false, "Copy entry to clipboard")
|
||||||
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
||||||
@@ -207,24 +167,23 @@ func init() {
|
|||||||
clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)")
|
clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard tracking")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard manager entirely")
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard manager")
|
||||||
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisableHistory, "disable-history", false, "Disable clipboard history persistence")
|
||||||
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnableHistory, "enable-history", false, "Enable clipboard history persistence")
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
||||||
|
|
||||||
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
|
||||||
|
|
||||||
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
||||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
|
|
||||||
switch {
|
if len(args) > 0 {
|
||||||
case len(args) > 0:
|
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
default:
|
} else {
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,67 +191,11 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipCopyDownload {
|
|
||||||
filePath, err := downloadToTempFile(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("download: %v", err)
|
|
||||||
}
|
|
||||||
if err := copyFileToClipboard(filePath); err != nil {
|
|
||||||
log.Fatalf("copy file: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipCopyType == "__multi__" {
|
|
||||||
offers, err := parseMultiOffers(data)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("parse multi offers: %v", err)
|
|
||||||
}
|
|
||||||
if err := clipboard.CopyMulti(offers, true, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("copy multi: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
|
|
||||||
var offers []clipboard.Offer
|
|
||||||
pos := 0
|
|
||||||
|
|
||||||
for pos < len(data) {
|
|
||||||
mimeEnd := bytes.IndexByte(data[pos:], 0)
|
|
||||||
if mimeEnd == -1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
mimeType := string(data[pos : pos+mimeEnd])
|
|
||||||
pos += mimeEnd + 1
|
|
||||||
|
|
||||||
lenEnd := bytes.IndexByte(data[pos:], 0)
|
|
||||||
if lenEnd == -1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
dataLen, err := strconv.Atoi(string(data[pos : pos+lenEnd]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse length: %w", err)
|
|
||||||
}
|
|
||||||
pos += lenEnd + 1
|
|
||||||
|
|
||||||
if pos+dataLen > len(data) {
|
|
||||||
return nil, fmt.Errorf("data truncated")
|
|
||||||
}
|
|
||||||
offerData := data[pos : pos+dataLen]
|
|
||||||
pos += dataLen
|
|
||||||
|
|
||||||
offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData})
|
|
||||||
}
|
|
||||||
|
|
||||||
return offers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipPaste(cmd *cobra.Command, args []string) {
|
func runClipPaste(cmd *cobra.Command, args []string) {
|
||||||
data, _, err := clipboard.Paste()
|
data, _, err := clipboard.Paste()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -449,13 +352,16 @@ func runClipGet(cmd *cobra.Command, args []string) {
|
|||||||
req := models.Request{
|
req := models.Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "clipboard.copyEntry",
|
Method: "clipboard.copyEntry",
|
||||||
Params: map[string]any{"id": id},
|
Params: map[string]any{
|
||||||
|
"id": id,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
resp, err := sendServerRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
log.Fatalf("Error: %s", resp.Error)
|
||||||
}
|
}
|
||||||
@@ -681,6 +587,12 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
|
|||||||
if clipConfigEnabled {
|
if clipConfigEnabled {
|
||||||
params["disabled"] = false
|
params["disabled"] = false
|
||||||
}
|
}
|
||||||
|
if clipConfigDisableHistory {
|
||||||
|
params["disableHistory"] = true
|
||||||
|
}
|
||||||
|
if clipConfigEnableHistory {
|
||||||
|
params["disableHistory"] = false
|
||||||
|
}
|
||||||
|
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
fmt.Println("No config options specified")
|
fmt.Println("No config options specified")
|
||||||
@@ -704,230 +616,3 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
fmt.Println("Config updated")
|
fmt.Println("Config updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClipExport(cmd *cobra.Command, args []string) {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.getHistory",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get clipboard history: %v", err)
|
|
||||||
}
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("No clipboard history")
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := json.MarshalIndent(resp.Result, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to marshal: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(args[0], out, 0o644); err != nil {
|
|
||||||
log.Fatalf("Failed to write file: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Exported to %s\n", args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipImport(cmd *cobra.Command, args []string) {
|
|
||||||
data, err := os.ReadFile(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to read file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries []map[string]any
|
|
||||||
if err := json.Unmarshal(data, &entries); err != nil {
|
|
||||||
log.Fatalf("Failed to parse JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var imported int
|
|
||||||
for _, entry := range entries {
|
|
||||||
dataStr, ok := entry["data"].(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mimeType, _ := entry["mimeType"].(string)
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "text/plain"
|
|
||||||
}
|
|
||||||
|
|
||||||
var entryData []byte
|
|
||||||
if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
|
|
||||||
entryData = decoded
|
|
||||||
} else {
|
|
||||||
entryData = []byte(dataStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.Store(entryData, mimeType); err != nil {
|
|
||||||
log.Errorf("Failed to store entry: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
imported++
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Imported %d entries\n", imported)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipMigrate(cmd *cobra.Command, args []string) {
|
|
||||||
dbPath := getCliphistPath()
|
|
||||||
if len(args) > 0 {
|
|
||||||
dbPath = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(dbPath); err != nil {
|
|
||||||
log.Fatalf("Cliphist db not found: %s", dbPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{
|
|
||||||
ReadOnly: true,
|
|
||||||
Timeout: 1 * time.Second,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to open cliphist db: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
var migrated int
|
|
||||||
err = db.View(func(tx *bolt.Tx) error {
|
|
||||||
b := tx.Bucket([]byte("b"))
|
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("cliphist bucket not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := b.Cursor()
|
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
||||||
if len(v) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeType := detectMimeType(v)
|
|
||||||
if err := clipboard.Store(v, mimeType); err != nil {
|
|
||||||
log.Errorf("Failed to store entry %d: %v", btoi(k), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
migrated++
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Migration failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Migrated %d entries from cliphist\n", migrated)
|
|
||||||
|
|
||||||
if !clipMigrateDelete {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Close()
|
|
||||||
if err := os.Remove(dbPath); err != nil {
|
|
||||||
log.Errorf("Failed to delete cliphist db: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os.Remove(filepath.Dir(dbPath))
|
|
||||||
fmt.Println("Deleted cliphist db")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCliphistPath() string {
|
|
||||||
cacheDir, err := os.UserCacheDir()
|
|
||||||
if err != nil {
|
|
||||||
return filepath.Join(os.Getenv("HOME"), ".cache", "cliphist", "db")
|
|
||||||
}
|
|
||||||
return filepath.Join(cacheDir, "cliphist", "db")
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectMimeType(data []byte) string {
|
|
||||||
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
|
|
||||||
return "image/png"
|
|
||||||
}
|
|
||||||
return "text/plain"
|
|
||||||
}
|
|
||||||
|
|
||||||
func btoi(v []byte) uint64 {
|
|
||||||
return binary.BigEndian.Uint64(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadToTempFile(rawURL string) (string, error) {
|
|
||||||
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
|
||||||
return "", fmt.Errorf("invalid URL: %s", rawURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedURL, err := url.Parse(rawURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("parse URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(parsedURL.Path)
|
|
||||||
if ext == "" {
|
|
||||||
ext = ".png"
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
|
||||||
resp, err := client.Get(rawURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("download: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("download failed: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
|
||||||
if idx := strings.Index(contentType, ";"); idx != -1 {
|
|
||||||
contentType = strings.TrimSpace(contentType[:idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(contentType, "image/") && !strings.HasPrefix(contentType, "video/") {
|
|
||||||
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err != nil {
|
|
||||||
return "", fmt.Errorf("not a valid media file (content-type: %s)", contentType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir, err := os.UserCacheDir()
|
|
||||||
if err != nil {
|
|
||||||
cacheDir = "/tmp"
|
|
||||||
}
|
|
||||||
clipDir := filepath.Join(cacheDir, "dms", "clipboard")
|
|
||||||
if err := os.MkdirAll(clipDir, 0o755); err != nil {
|
|
||||||
return "", fmt.Errorf("create cache dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
|
|
||||||
if err := os.WriteFile(filePath, data, 0o644); err != nil {
|
|
||||||
return "", fmt.Errorf("write file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFileToClipboard(filePath string) error {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.copyFile",
|
|
||||||
Params: map[string]any{"filePath": filePath},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("server request: %w", err)
|
|
||||||
}
|
|
||||||
if resp.Error != "" {
|
|
||||||
return fmt.Errorf("server error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc",
|
||||||
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)
|
||||||
@@ -76,13 +77,6 @@ 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",
|
||||||
@@ -517,12 +511,8 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
colorCmd,
|
colorCmd,
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
notifyCmd,
|
|
||||||
genericNotifyActionCmd,
|
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
clipboardCmd,
|
||||||
chromaCmd,
|
|
||||||
doctorCmd,
|
doctorCmd,
|
||||||
configCmd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -13,84 +12,12 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type status string
|
|
||||||
|
|
||||||
const (
|
|
||||||
statusOK status = "ok"
|
|
||||||
statusWarn status = "warn"
|
|
||||||
statusError status = "error"
|
|
||||||
statusInfo status = "info"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s status) IconStyle(styles tui.Styles) (string, lipgloss.Style) {
|
|
||||||
switch s {
|
|
||||||
case statusOK:
|
|
||||||
return "●", styles.Success
|
|
||||||
case statusWarn:
|
|
||||||
return "●", styles.Warning
|
|
||||||
case statusError:
|
|
||||||
return "●", styles.Error
|
|
||||||
default:
|
|
||||||
return "○", styles.Subtle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DoctorStatus struct {
|
|
||||||
Errors []checkResult
|
|
||||||
Warnings []checkResult
|
|
||||||
OK []checkResult
|
|
||||||
Info []checkResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DoctorStatus) Add(r checkResult) {
|
|
||||||
switch r.status {
|
|
||||||
case statusError:
|
|
||||||
ds.Errors = append(ds.Errors, r)
|
|
||||||
case statusWarn:
|
|
||||||
ds.Warnings = append(ds.Warnings, r)
|
|
||||||
case statusOK:
|
|
||||||
ds.OK = append(ds.OK, r)
|
|
||||||
case statusInfo:
|
|
||||||
ds.Info = append(ds.Info, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DoctorStatus) HasIssues() bool {
|
|
||||||
return len(ds.Errors) > 0 || len(ds.Warnings) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DoctorStatus) ErrorCount() int {
|
|
||||||
return len(ds.Errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DoctorStatus) WarningCount() int {
|
|
||||||
return len(ds.Warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DoctorStatus) OKCount() int {
|
|
||||||
return len(ds.OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
|
||||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
|
||||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
|
||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
|
||||||
riverVersionRegex = regexp.MustCompile(`river (\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{
|
||||||
Use: "doctor",
|
Use: "doctor",
|
||||||
Short: "Diagnose DMS installation and dependencies",
|
Short: "Diagnose DMS installation and dependencies",
|
||||||
@@ -98,14 +25,10 @@ var doctorCmd = &cobra.Command{
|
|||||||
Run: runDoctor,
|
Run: runDoctor,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var doctorVerbose bool
|
||||||
doctorVerbose bool
|
|
||||||
doctorJSON bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
|
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
|
||||||
doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type category int
|
type category int
|
||||||
@@ -119,82 +42,23 @@ const (
|
|||||||
catOptionalFeatures
|
catOptionalFeatures
|
||||||
catConfigFiles
|
catConfigFiles
|
||||||
catServices
|
catServices
|
||||||
catEnvironment
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c category) String() string {
|
var categoryNames = []string{
|
||||||
switch c {
|
"System", "Versions", "Installation", "Compositor",
|
||||||
case catSystem:
|
"Quickshell Features", "Optional Features", "Config Files", "Services",
|
||||||
return "System"
|
|
||||||
case catVersions:
|
|
||||||
return "Versions"
|
|
||||||
case catInstallation:
|
|
||||||
return "Installation"
|
|
||||||
case catCompositor:
|
|
||||||
return "Compositor"
|
|
||||||
case catQuickshellFeatures:
|
|
||||||
return "Quickshell Features"
|
|
||||||
case catOptionalFeatures:
|
|
||||||
return "Optional Features"
|
|
||||||
case catConfigFiles:
|
|
||||||
return "Config Files"
|
|
||||||
case catServices:
|
|
||||||
return "Services"
|
|
||||||
case catEnvironment:
|
|
||||||
return "Environment"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
checkNameMaxLength = 21
|
|
||||||
doctorDocsURL = "https://danklinux.com/docs/dankmaterialshell/cli-doctor"
|
|
||||||
)
|
|
||||||
|
|
||||||
type checkResult struct {
|
type checkResult struct {
|
||||||
category category
|
category category
|
||||||
name string
|
name string
|
||||||
status status
|
status string
|
||||||
message string
|
message string
|
||||||
details string
|
details string
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
type checkResultJSON struct {
|
|
||||||
Category string `json:"category"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details string `json:"details,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type doctorOutputJSON struct {
|
|
||||||
Summary struct {
|
|
||||||
Errors int `json:"errors"`
|
|
||||||
Warnings int `json:"warnings"`
|
|
||||||
OK int `json:"ok"`
|
|
||||||
Info int `json:"info"`
|
|
||||||
} `json:"summary"`
|
|
||||||
Results []checkResultJSON `json:"results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r checkResult) toJSON() checkResultJSON {
|
|
||||||
return checkResultJSON{
|
|
||||||
Category: r.category.String(),
|
|
||||||
Name: r.name,
|
|
||||||
Status: string(r.status),
|
|
||||||
Message: r.message,
|
|
||||||
Details: r.details,
|
|
||||||
URL: r.url,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDoctor(cmd *cobra.Command, args []string) {
|
func runDoctor(cmd *cobra.Command, args []string) {
|
||||||
if !doctorJSON {
|
printDoctorHeader()
|
||||||
printDoctorHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
qsFeatures, qsMissingFeatures := checkQuickshellFeatures()
|
qsFeatures, qsMissingFeatures := checkQuickshellFeatures()
|
||||||
|
|
||||||
@@ -207,15 +71,10 @@ func runDoctor(cmd *cobra.Command, args []string) {
|
|||||||
checkOptionalDependencies(),
|
checkOptionalDependencies(),
|
||||||
checkConfigurationFiles(),
|
checkConfigurationFiles(),
|
||||||
checkSystemdServices(),
|
checkSystemdServices(),
|
||||||
checkEnvironmentVars(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if doctorJSON {
|
printResults(results)
|
||||||
printResultsJSON(results)
|
printSummary(results, qsMissingFeatures)
|
||||||
} else {
|
|
||||||
printResults(results)
|
|
||||||
printSummary(results, qsMissingFeatures)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func printDoctorHeader() {
|
func printDoctorHeader() {
|
||||||
@@ -229,52 +88,50 @@ func printDoctorHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkSystemInfo() []checkResult {
|
func checkSystemInfo() []checkResult {
|
||||||
var results []checkResult
|
results := []checkResult{}
|
||||||
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
osInfo, err := distros.GetOSInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status, message, details := statusWarn, fmt.Sprintf("Unknown (%v)", err), ""
|
status, message, details := "warn", fmt.Sprintf("Unknown (%v)", err), ""
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "Unsupported distribution") {
|
if strings.Contains(err.Error(), "Unsupported distribution") {
|
||||||
osRelease := readOSRelease()
|
osRelease := readOSRelease()
|
||||||
switch {
|
if osRelease["ID"] == "nixos" {
|
||||||
case osRelease["ID"] == "nixos":
|
status = "ok"
|
||||||
status = statusOK
|
|
||||||
message = osRelease["PRETTY_NAME"]
|
message = osRelease["PRETTY_NAME"]
|
||||||
if message == "" {
|
if message == "" {
|
||||||
message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"])
|
message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"])
|
||||||
}
|
}
|
||||||
details = "Supported for runtime (install via NixOS module or Flake)"
|
details = "Supported for runtime (install via NixOS module or Flake)"
|
||||||
case osRelease["PRETTY_NAME"] != "":
|
} else if osRelease["PRETTY_NAME"] != "" {
|
||||||
message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"])
|
message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"])
|
||||||
details = "DMS may work but automatic installation is not available"
|
details = "DMS may work but automatic installation is not available"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, checkResult{catSystem, "Operating System", status, message, details, doctorDocsURL + "#operating-system"})
|
results = append(results, checkResult{catSystem, "Operating System", status, message, details})
|
||||||
} else {
|
} else {
|
||||||
status := statusOK
|
status := "ok"
|
||||||
message := osInfo.PrettyName
|
message := osInfo.PrettyName
|
||||||
if message == "" {
|
if message == "" {
|
||||||
message = fmt.Sprintf("%s %s", osInfo.Distribution.ID, osInfo.VersionID)
|
message = fmt.Sprintf("%s %s", osInfo.Distribution.ID, osInfo.VersionID)
|
||||||
}
|
}
|
||||||
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
|
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
|
||||||
status = statusWarn
|
status = "warn"
|
||||||
message += " (version may not be fully supported)"
|
message += " (version may not be fully supported)"
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catSystem, "Operating System", status, message,
|
catSystem, "Operating System", status, message,
|
||||||
fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture),
|
fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture),
|
||||||
doctorDocsURL + "#operating-system",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
arch := runtime.GOARCH
|
arch := runtime.GOARCH
|
||||||
archStatus := statusOK
|
archStatus := "ok"
|
||||||
if arch != "amd64" && arch != "arm64" {
|
if arch != "amd64" && arch != "arm64" {
|
||||||
archStatus = statusError
|
archStatus = "error"
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, "", doctorDocsURL + "#architecture"})
|
results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, ""})
|
||||||
|
|
||||||
waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
|
waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
|
||||||
xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
|
xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
|
||||||
@@ -282,41 +139,21 @@ func checkSystemInfo() []checkResult {
|
|||||||
switch {
|
switch {
|
||||||
case waylandDisplay != "" || xdgSessionType == "wayland":
|
case waylandDisplay != "" || xdgSessionType == "wayland":
|
||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catSystem, "Display Server", statusOK, "Wayland",
|
catSystem, "Display Server", "ok", "Wayland",
|
||||||
fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay),
|
fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay),
|
||||||
doctorDocsURL + "#display-server",
|
|
||||||
})
|
})
|
||||||
case xdgSessionType == "x11":
|
case xdgSessionType == "x11":
|
||||||
results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", "", doctorDocsURL + "#display-server"})
|
results = append(results, checkResult{catSystem, "Display Server", "error", "X11 (DMS requires Wayland)", ""})
|
||||||
default:
|
default:
|
||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)",
|
catSystem, "Display Server", "warn", "Unknown (ensure you're running Wayland)",
|
||||||
fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType),
|
fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType),
|
||||||
doctorDocsURL + "#display-server",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkEnvironmentVars() []checkResult {
|
|
||||||
var results []checkResult
|
|
||||||
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
|
|
||||||
results = append(results, checkEnvVar("QS_ICON_THEME")...)
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkEnvVar(name string) []checkResult {
|
|
||||||
value := os.Getenv(name)
|
|
||||||
if value != "" {
|
|
||||||
return []checkResult{{catEnvironment, name, statusInfo, value, "", doctorDocsURL + "#environment-variables"}}
|
|
||||||
}
|
|
||||||
if doctorVerbose {
|
|
||||||
return []checkResult{{catEnvironment, name, statusInfo, "Not set", "", doctorDocsURL + "#environment-variables"}}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOSRelease() map[string]string {
|
func readOSRelease() map[string]string {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
data, err := os.ReadFile("/etc/os-release")
|
data, err := os.ReadFile("/etc/os-release")
|
||||||
@@ -332,28 +169,18 @@ func readOSRelease() map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkVersions(qsMissingFeatures bool) []checkResult {
|
func checkVersions(qsMissingFeatures bool) []checkResult {
|
||||||
dmsCliPath, _ := os.Executable()
|
|
||||||
dmsCliDetails := ""
|
|
||||||
if doctorVerbose {
|
|
||||||
dmsCliDetails = dmsCliPath
|
|
||||||
}
|
|
||||||
|
|
||||||
results := []checkResult{
|
results := []checkResult{
|
||||||
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
|
{catVersions, "DMS CLI", "info", formatVersion(Version), ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
|
qsVersion, qsStatus := getQuickshellVersionInfo(qsMissingFeatures)
|
||||||
qsDetails := ""
|
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, ""})
|
||||||
if doctorVerbose && qsPath != "" {
|
|
||||||
qsDetails = qsPath
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
|
|
||||||
|
|
||||||
dmsVersion, dmsPath := getDMSShellVersion()
|
dmsVersion, dmsPath := getDMSShellVersion()
|
||||||
if dmsVersion != "" {
|
if dmsVersion != "" {
|
||||||
results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath, doctorDocsURL + "#dms-shell"})
|
results = append(results, checkResult{catVersions, "DMS Shell", "ok", dmsVersion, dmsPath})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install", doctorDocsURL + "#dms-shell"})
|
results = append(results, checkResult{catVersions, "DMS Shell", "error", "Not installed or not detected", "Run 'dms setup' to install"})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -379,34 +206,32 @@ func getDMSShellVersion() (version, path string) {
|
|||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
|
func getQuickshellVersionInfo(missingFeatures bool) (string, string) {
|
||||||
if !utils.CommandExists("qs") {
|
if !utils.CommandExists("qs") {
|
||||||
return "Not installed", statusError, ""
|
return "Not installed", "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
qsPath, _ := exec.LookPath("qs")
|
|
||||||
|
|
||||||
output, err := exec.Command("qs", "--version").Output()
|
output, err := exec.Command("qs", "--version").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "Installed (version check failed)", statusWarn, qsPath
|
return "Installed (version check failed)", "warn"
|
||||||
}
|
}
|
||||||
|
|
||||||
fullVersion := strings.TrimSpace(string(output))
|
fullVersion := strings.TrimSpace(string(output))
|
||||||
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
|
if matches := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`).FindStringSubmatch(fullVersion); len(matches) >= 2 {
|
||||||
if version.CompareVersions(matches[1], "0.2.0") < 0 {
|
if version.CompareVersions(matches[1], "0.2.0") < 0 {
|
||||||
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
|
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), "error"
|
||||||
}
|
}
|
||||||
if missingFeatures {
|
if missingFeatures {
|
||||||
return fullVersion, statusWarn, qsPath
|
return fullVersion, "warn"
|
||||||
}
|
}
|
||||||
return fullVersion, statusOK, qsPath
|
return fullVersion, "ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullVersion, statusWarn, qsPath
|
return fullVersion, "warn"
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkDMSInstallation() []checkResult {
|
func checkDMSInstallation() []checkResult {
|
||||||
var results []checkResult
|
results := []checkResult{}
|
||||||
|
|
||||||
dmsPath := ""
|
dmsPath := ""
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||||
@@ -416,16 +241,16 @@ func checkDMSInstallation() []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dmsPath == "" {
|
if dmsPath == "" {
|
||||||
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
|
return []checkResult{{catInstallation, "DMS Configuration", "error", "Not found", "shell.qml not found in any config path"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath, doctorDocsURL + "#dms-configuration"})
|
results = append(results, checkResult{catInstallation, "DMS Configuration", "ok", "Found", dmsPath})
|
||||||
|
|
||||||
shellQml := filepath.Join(dmsPath, "shell.qml")
|
shellQml := filepath.Join(dmsPath, "shell.qml")
|
||||||
if _, err := os.Stat(shellQml); err != nil {
|
if _, err := os.Stat(shellQml); err != nil {
|
||||||
results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml, doctorDocsURL + "#dms-configuration"})
|
results = append(results, checkResult{catInstallation, "shell.qml", "error", "Missing", shellQml})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml, doctorDocsURL + "#dms-configuration"})
|
results = append(results, checkResult{catInstallation, "shell.qml", "ok", "Present", shellQml})
|
||||||
}
|
}
|
||||||
|
|
||||||
if doctorVerbose {
|
if doctorVerbose {
|
||||||
@@ -438,7 +263,7 @@ func checkDMSInstallation() []checkResult {
|
|||||||
case strings.Contains(dmsPath, ".config"):
|
case strings.Contains(dmsPath, ".config"):
|
||||||
installType = "User config"
|
installType = "User config"
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath, doctorDocsURL + "#dms-configuration"})
|
results = append(results, checkResult{catInstallation, "Install Type", "info", installType, dmsPath})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -446,69 +271,52 @@ func checkDMSInstallation() []checkResult {
|
|||||||
|
|
||||||
func checkWindowManagers() []checkResult {
|
func checkWindowManagers() []checkResult {
|
||||||
compositors := []struct {
|
compositors := []struct {
|
||||||
name, versionCmd, versionArg string
|
name, versionCmd, versionArg, versionRe string
|
||||||
versionRegex *regexp.Regexp
|
commands []string
|
||||||
commands []string
|
|
||||||
}{
|
}{
|
||||||
{"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
{"Hyprland", "hyprctl", "version", `v?(\d+\.\d+\.\d+)`, []string{"hyprland", "Hyprland"}},
|
||||||
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
{"niri", "niri", "--version", `niri (\d+\.\d+)`, []string{"niri"}},
|
||||||
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
{"Sway", "sway", "--version", `sway version (\d+\.\d+)`, []string{"sway"}},
|
||||||
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
{"River", "river", "-version", `river (\d+\.\d+)`, []string{"river"}},
|
||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", `wayfire (\d+\.\d+)`, []string{"wayfire"}},
|
||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
results := []checkResult{}
|
||||||
foundAny := false
|
foundAny := false
|
||||||
|
|
||||||
for _, c := range compositors {
|
for _, c := range compositors {
|
||||||
if !slices.ContainsFunc(c.commands, utils.CommandExists) {
|
if slices.ContainsFunc(c.commands, utils.CommandExists) {
|
||||||
continue
|
foundAny = true
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catCompositor, c.name, "ok",
|
||||||
|
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRe), "",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
foundAny = true
|
|
||||||
var compositorPath string
|
|
||||||
for _, cmd := range c.commands {
|
|
||||||
if path, err := exec.LookPath(cmd); err == nil {
|
|
||||||
compositorPath = path
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
details := ""
|
|
||||||
if doctorVerbose && compositorPath != "" {
|
|
||||||
details = compositorPath
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{
|
|
||||||
catCompositor, c.name, statusOK,
|
|
||||||
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
|
|
||||||
doctorDocsURL + "#compositor-checks",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !foundAny {
|
if !foundAny {
|
||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", "error",
|
||||||
"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-checks",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm := detectRunningWM(); wm != "" {
|
if wm := detectRunningWM(); wm != "" {
|
||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", "info", wm, ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg, regex string) string {
|
||||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
output, err := exec.Command(cmd, arg).Output()
|
||||||
if err != nil && len(output) == 0 {
|
if err != nil {
|
||||||
return "installed"
|
return "installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
outStr := string(output)
|
outStr := string(output)
|
||||||
if matches := regex.FindStringSubmatch(outStr); len(matches) > 1 {
|
if matches := regexp.MustCompile(regex).FindStringSubmatch(outStr); len(matches) > 1 {
|
||||||
ver := matches[1]
|
ver := matches[1]
|
||||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||||
return ver + " (git)"
|
return ver + " (git)"
|
||||||
@@ -591,7 +399,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(testScript, []byte(qmlContent), 0o644); err != nil {
|
if err := os.WriteFile(testScript, []byte(qmlContent), 0644); err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,122 +415,80 @@ ShellRoot {
|
|||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
results := []checkResult{}
|
||||||
missingFeatures := false
|
missingFeatures := false
|
||||||
|
|
||||||
for _, f := range features {
|
for _, f := range features {
|
||||||
available := strings.Contains(outputStr, fmt.Sprintf("FEATURE:%s:OK", f.name))
|
available := strings.Contains(outputStr, fmt.Sprintf("FEATURE:%s:OK", f.name))
|
||||||
status, message := statusOK, "Available"
|
status, message := "ok", "Available"
|
||||||
if !available {
|
if !available {
|
||||||
status, message = statusInfo, "Not available"
|
status, message = "info", "Not available"
|
||||||
missingFeatures = true
|
missingFeatures = true
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc, doctorDocsURL + "#quickshell-features"})
|
results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, missingFeatures
|
return results, missingFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkI2CAvailability() checkResult {
|
|
||||||
ddc, err := brightness.NewDDCBackend()
|
|
||||||
if err != nil {
|
|
||||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "Not available", "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
|
||||||
}
|
|
||||||
defer ddc.Close()
|
|
||||||
|
|
||||||
devices, err := ddc.GetDevices()
|
|
||||||
if err != nil || len(devices) == 0 {
|
|
||||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "No monitors detected", "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(stackResult *network.DetectResult) string {
|
|
||||||
switch stackResult.Backend {
|
|
||||||
case network.BackendNetworkManager:
|
|
||||||
return "NetworkManager"
|
|
||||||
case network.BackendIwd:
|
|
||||||
return "iwd"
|
|
||||||
case network.BackendNetworkd:
|
|
||||||
if stackResult.HasIwd {
|
|
||||||
return "iwd + systemd-networkd"
|
|
||||||
}
|
|
||||||
return "systemd-networkd"
|
|
||||||
case network.BackendConnMan:
|
|
||||||
return "ConnMan"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
results := []checkResult{}
|
||||||
|
|
||||||
optionalFeaturesURL := doctorDocsURL + "#optional-features"
|
if utils.IsServiceActive("accounts-daemon", false) {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "accountsservice", "ok", "Running", "User accounts"})
|
||||||
accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts")
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "accountsservice", "warn", "Not running", "User accounts"})
|
||||||
|
}
|
||||||
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())
|
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
terminalFound := ""
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
for _, term := range terminals {
|
||||||
} else {
|
if utils.CommandExists(term) {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
terminalFound = term
|
||||||
}
|
break
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if terminalFound != "" {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", "ok", terminalFound, ""})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", "warn", "None found", "Install ghostty, kitty, or alacritty"})
|
||||||
|
}
|
||||||
|
|
||||||
deps := []struct {
|
deps := []struct {
|
||||||
name, cmd, desc string
|
name, cmd, altCmd, 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 waveform", false},
|
||||||
{"khal", "khal", "Calendar events", false},
|
{"khal", "khal", "", "Calendar events", false},
|
||||||
{"danksearch", "dsearch", "File search", false},
|
{"Network", "nmcli", "iwctl", "Network management", false},
|
||||||
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
{"danksearch", "dsearch", "", "File search", false},
|
||||||
|
{"loginctl", "loginctl", "", "Session management", false},
|
||||||
|
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range deps {
|
for _, d := range deps {
|
||||||
found := utils.CommandExists(d.cmd)
|
found, foundCmd := utils.CommandExists(d.cmd), d.cmd
|
||||||
|
if !found && d.altCmd != "" {
|
||||||
|
if utils.CommandExists(d.altCmd) {
|
||||||
|
found, foundCmd = true, d.altCmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
if found {
|
||||||
case found:
|
message := "Installed"
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL})
|
switch foundCmd {
|
||||||
case d.important:
|
case "nmcli":
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL})
|
message = "NetworkManager"
|
||||||
default:
|
case "iwctl":
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL})
|
message = "iwd"
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, d.name, "ok", message, d.desc})
|
||||||
|
} else if d.important {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, d.name, "warn", "Missing", d.desc})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, d.name, "info", "Not installed", d.desc})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,33 +496,19 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkConfigurationFiles() []checkResult {
|
func checkConfigurationFiles() []checkResult {
|
||||||
configDir, _ := os.UserConfigDir()
|
|
||||||
cacheDir, _ := os.UserCacheDir()
|
|
||||||
dmsDir := "DankMaterialShell"
|
|
||||||
|
|
||||||
configFiles := []struct{ name, path string }{
|
configFiles := []struct{ name, path string }{
|
||||||
{"settings.json", filepath.Join(configDir, dmsDir, "settings.json")},
|
{"Settings", filepath.Join(utils.XDGConfigHome(), "DankMaterialShell", "settings.json")},
|
||||||
{"clsettings.json", filepath.Join(configDir, dmsDir, "clsettings.json")},
|
{"Session", filepath.Join(utils.XDGStateHome(), "DankMaterialShell", "session.json")},
|
||||||
{"plugin_settings.json", filepath.Join(configDir, dmsDir, "plugin_settings.json")},
|
{"Colors", filepath.Join(utils.XDGCacheHome(), "DankMaterialShell", "dms-colors.json")},
|
||||||
{"session.json", filepath.Join(utils.XDGStateHome(), dmsDir, "session.json")},
|
|
||||||
{"dms-colors.json", filepath.Join(cacheDir, dmsDir, "dms-colors.json")},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
results := []checkResult{}
|
||||||
for _, cf := range configFiles {
|
for _, cf := range configFiles {
|
||||||
info, err := os.Stat(cf.path)
|
if _, err := os.Stat(cf.path); err == nil {
|
||||||
if err != nil {
|
results = append(results, checkResult{catConfigFiles, cf.name, "ok", "Present", cf.path})
|
||||||
results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path, doctorDocsURL + "#config-files"})
|
} else {
|
||||||
continue
|
results = append(results, checkResult{catConfigFiles, cf.name, "info", "Not yet created", cf.path})
|
||||||
}
|
}
|
||||||
|
|
||||||
status := statusOK
|
|
||||||
message := "Present"
|
|
||||||
if info.Mode().Perm()&0o200 == 0 {
|
|
||||||
status = statusWarn
|
|
||||||
message += " (read-only)"
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
|
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
@@ -766,35 +518,31 @@ func checkSystemdServices() []checkResult {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
results := []checkResult{}
|
||||||
|
|
||||||
dmsState := getServiceState("dms", true)
|
dmsState := getServiceState("dms", true)
|
||||||
if !dmsState.exists {
|
if !dmsState.exists {
|
||||||
results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"})
|
results = append(results, checkResult{catServices, "dms.service", "info", "Not installed", "Optional user service"})
|
||||||
} else {
|
} else {
|
||||||
status, message := statusOK, dmsState.enabled
|
status, message := "ok", dmsState.enabled
|
||||||
if dmsState.active != "" {
|
if dmsState.active != "" {
|
||||||
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
|
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
|
||||||
}
|
}
|
||||||
switch {
|
if dmsState.enabled == "disabled" {
|
||||||
case dmsState.enabled == "disabled":
|
status, message = "warn", "Disabled"
|
||||||
status, message = statusWarn, "Disabled"
|
|
||||||
case dmsState.active == "failed" || dmsState.active == "inactive":
|
|
||||||
status = statusError
|
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
|
results = append(results, checkResult{catServices, "dms.service", status, message, ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
greetdState := getServiceState("greetd", false)
|
greetdState := getServiceState("greetd", false)
|
||||||
switch {
|
if greetdState.exists {
|
||||||
case greetdState.exists:
|
status := "ok"
|
||||||
status := statusOK
|
|
||||||
if greetdState.enabled == "disabled" {
|
if greetdState.enabled == "disabled" {
|
||||||
status = statusInfo
|
status = "info"
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, "", doctorDocsURL + "#services"})
|
results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, ""})
|
||||||
case doctorVerbose:
|
} else if doctorVerbose {
|
||||||
results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service", doctorDocsURL + "#services"})
|
results = append(results, checkResult{catServices, "greetd", "info", "Not installed", "Optional greeter service"})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -841,85 +589,67 @@ func printResults(results []checkResult) {
|
|||||||
if currentCategory != -1 {
|
if currentCategory != -1 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s\n", styles.Bold.Render(r.category.String()))
|
fmt.Printf(" %s\n", styles.Bold.Render(categoryNames[r.category]))
|
||||||
currentCategory = r.category
|
currentCategory = r.category
|
||||||
}
|
}
|
||||||
printResultLine(r, styles)
|
printResultLine(r, styles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printResultsJSON(results []checkResult) {
|
|
||||||
var ds DoctorStatus
|
|
||||||
for _, r := range results {
|
|
||||||
ds.Add(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
output := doctorOutputJSON{}
|
|
||||||
output.Summary.Errors = ds.ErrorCount()
|
|
||||||
output.Summary.Warnings = ds.WarningCount()
|
|
||||||
output.Summary.OK = ds.OKCount()
|
|
||||||
output.Summary.Info = len(ds.Info)
|
|
||||||
|
|
||||||
output.Results = make([]checkResultJSON, 0, len(results))
|
|
||||||
for _, r := range results {
|
|
||||||
output.Results = append(output.Results, r.toJSON())
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
if err := encoder.Encode(output); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printResultLine(r checkResult, styles tui.Styles) {
|
func printResultLine(r checkResult, styles tui.Styles) {
|
||||||
icon, style := r.status.IconStyle(styles)
|
icon, style := "○", styles.Subtle
|
||||||
|
switch r.status {
|
||||||
|
case "ok":
|
||||||
|
icon, style = "●", styles.Success
|
||||||
|
case "warn":
|
||||||
|
icon, style = "●", styles.Warning
|
||||||
|
case "error":
|
||||||
|
icon, style = "●", styles.Error
|
||||||
|
}
|
||||||
|
|
||||||
name := r.name
|
name := r.name
|
||||||
nameLen := len(name)
|
if len(name) > 18 {
|
||||||
|
name = name[:17] + "…"
|
||||||
if nameLen > checkNameMaxLength {
|
|
||||||
name = name[:checkNameMaxLength-1] + "…"
|
|
||||||
nameLen = checkNameMaxLength
|
|
||||||
}
|
}
|
||||||
dots := strings.Repeat("·", checkNameMaxLength-nameLen)
|
dots := strings.Repeat("·", 19-len(name))
|
||||||
|
|
||||||
fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message)
|
fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message)
|
||||||
|
|
||||||
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) {
|
||||||
theme := tui.TerminalTheme()
|
theme := tui.TerminalTheme()
|
||||||
styles := tui.NewStyles(theme)
|
styles := tui.NewStyles(theme)
|
||||||
|
|
||||||
var ds DoctorStatus
|
errors, warnings, ok := 0, 0, 0
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
ds.Add(r)
|
switch r.status {
|
||||||
|
case "error":
|
||||||
|
errors++
|
||||||
|
case "warn":
|
||||||
|
warnings++
|
||||||
|
case "ok":
|
||||||
|
ok++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf(" %s\n", styles.Subtle.Render("──────────────────────────────────────"))
|
fmt.Printf(" %s\n", styles.Subtle.Render("──────────────────────────────────────"))
|
||||||
|
|
||||||
if !ds.HasIssues() {
|
if errors == 0 && warnings == 0 {
|
||||||
fmt.Printf(" %s\n", styles.Success.Render("✓ All checks passed!"))
|
fmt.Printf(" %s\n", styles.Success.Render("✓ All checks passed!"))
|
||||||
} else {
|
} else {
|
||||||
var parts []string
|
parts := []string{}
|
||||||
|
if errors > 0 {
|
||||||
if ds.ErrorCount() > 0 {
|
parts = append(parts, styles.Error.Render(fmt.Sprintf("%d error(s)", errors)))
|
||||||
parts = append(parts, styles.Error.Render(fmt.Sprintf("%d error(s)", ds.ErrorCount())))
|
|
||||||
}
|
}
|
||||||
if ds.WarningCount() > 0 {
|
if warnings > 0 {
|
||||||
parts = append(parts, styles.Warning.Render(fmt.Sprintf("%d warning(s)", ds.WarningCount())))
|
parts = append(parts, styles.Warning.Render(fmt.Sprintf("%d warning(s)", warnings)))
|
||||||
}
|
}
|
||||||
parts = append(parts, styles.Success.Render(fmt.Sprintf("%d ok", ds.OKCount())))
|
parts = append(parts, styles.Success.Render(fmt.Sprintf("%d ok", ok)))
|
||||||
fmt.Printf(" %s\n", strings.Join(parts, ", "))
|
fmt.Printf(" %s\n", strings.Join(parts, ", "))
|
||||||
|
|
||||||
if qsMissingFeatures {
|
if qsMissingFeatures {
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ func updateDMSBinary() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
version := ""
|
version := ""
|
||||||
for line := range strings.SplitSeq(string(output), "\n") {
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
if strings.Contains(line, "\"tag_name\"") {
|
if strings.Contains(line, "\"tag_name\"") {
|
||||||
parts := strings.Split(line, "\"")
|
parts := strings.Split(line, "\"")
|
||||||
if len(parts) >= 4 {
|
if len(parts) >= 4 {
|
||||||
@@ -443,7 +443,7 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
decompressedPath := filepath.Join(tempDir, "dms")
|
decompressedPath := filepath.Join(tempDir, "dms")
|
||||||
|
|
||||||
if err := os.Chmod(decompressedPath, 0o755); err != nil {
|
if err := os.Chmod(decompressedPath, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ func checkGroupExists(groupName string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.SplitSeq(string(data), "\n")
|
lines := strings.Split(string(data), "\n")
|
||||||
for line := range lines {
|
for _, line := range lines {
|
||||||
if strings.HasPrefix(line, groupName+":") {
|
if strings.HasPrefix(line, groupName+":") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -521,7 +521,7 @@ func enableGreeter() error {
|
|||||||
newConfig := strings.Join(finalLines, "\n")
|
newConfig := strings.Join(finalLines, "\n")
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
tmpFile := "/tmp/greetd-config.toml"
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,8 +592,8 @@ func checkGreeterStatus() error {
|
|||||||
if data, err := os.ReadFile(configPath); err == nil {
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
configContent := string(data)
|
configContent := string(data)
|
||||||
if strings.Contains(configContent, "dms-greeter") {
|
if strings.Contains(configContent, "dms-greeter") {
|
||||||
lines := strings.SplitSeq(configContent, "\n")
|
lines := strings.Split(configContent, "\n")
|
||||||
for line := range lines {
|
for _, line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||||
parts := strings.SplitN(trimmed, "=", 2)
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
|||||||
@@ -57,14 +57,12 @@ var keybindsRemoveCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
|
||||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||||
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
||||||
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)
|
||||||
@@ -112,21 +110,12 @@ func initializeProviders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeybindsList(cmd *cobra.Command, _ []string) {
|
func runKeybindsList(_ *cobra.Command, _ []string) {
|
||||||
providerList := keybinds.GetDefaultRegistry().List()
|
providerList := keybinds.GetDefaultRegistry().List()
|
||||||
asJSON, _ := cmd.Flags().GetBool("json")
|
|
||||||
|
|
||||||
if asJSON {
|
|
||||||
output, _ := json.Marshal(providerList)
|
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(providerList) == 0 {
|
if len(providerList) == 0 {
|
||||||
fmt.Fprintln(os.Stdout, "No providers available")
|
fmt.Fprintln(os.Stdout, "No providers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, "Available providers:")
|
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||||
for _, name := range providerList {
|
for _, name := range providerList {
|
||||||
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||||
@@ -212,9 +201,6 @@ 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 {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,16 +29,9 @@ var matugenQueueCmd = &cobra.Command{
|
|||||||
Run: runMatugenQueue,
|
Run: runMatugenQueue,
|
||||||
}
|
}
|
||||||
|
|
||||||
var matugenCheckCmd = &cobra.Command{
|
|
||||||
Use: "check",
|
|
||||||
Short: "Check which template apps are detected",
|
|
||||||
Run: runMatugenCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
matugenCmd.AddCommand(matugenQueueCmd)
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
matugenCmd.AddCommand(matugenCheckCmd)
|
|
||||||
|
|
||||||
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||||
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||||
@@ -82,7 +74,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
ConfigDir: configDir,
|
ConfigDir: configDir,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Value: value,
|
Value: value,
|
||||||
Mode: matugen.ColorMode(mode),
|
Mode: mode,
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
@@ -170,12 +162,3 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Timeout waiting for theme generation")
|
log.Fatalf("Timeout waiting for theme generation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatugenCheck(cmd *cobra.Command, args []string) {
|
|
||||||
checks := matugen.CheckTemplates(nil)
|
|
||||||
data, err := json.Marshal(checks)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to marshal check results: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println(string(data))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
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,7 +7,9 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,9 +20,11 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +38,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
|
return nil // <-- Guard statement
|
||||||
}
|
}
|
||||||
|
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
@@ -46,18 +50,15 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
||||||
if len(getAllDMSPIDs()) == 0 {
|
statePath := strings.TrimSpace(string(data))
|
||||||
os.Remove(configStateFile)
|
shellPath := filepath.Join(statePath, "shell.qml")
|
||||||
} else {
|
|
||||||
statePath := strings.TrimSpace(string(data))
|
|
||||||
shellPath := filepath.Join(statePath, "shell.qml")
|
|
||||||
|
|
||||||
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
||||||
log.Debug("Using config from active session state file: %s", statePath)
|
log.Debug("Using config from active session state file: %s", statePath)
|
||||||
configPath = statePath
|
configPath = statePath
|
||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil
|
return nil // <-- Guard statement
|
||||||
}
|
} else {
|
||||||
os.Remove(configStateFile)
|
os.Remove(configStateFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,3 +73,18 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,14 +87,20 @@ func newDPMSClient() (*dpmsClient, error) {
|
|||||||
switch e.Interface {
|
switch e.Interface {
|
||||||
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
||||||
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
||||||
version := min(e.Version, 1)
|
version := e.Version
|
||||||
|
if version > 1 {
|
||||||
|
version = 1
|
||||||
|
}
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
||||||
c.powerMgr = powerMgr
|
c.powerMgr = powerMgr
|
||||||
}
|
}
|
||||||
|
|
||||||
case "wl_output":
|
case "wl_output":
|
||||||
output := wlclient.NewOutput(c.ctx)
|
output := wlclient.NewOutput(c.ctx)
|
||||||
version := min(e.Version, 4)
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
outputID := fmt.Sprintf("output-%d", output.ID())
|
outputID := fmt.Sprintf("output-%d", output.ID())
|
||||||
state := &outputState{
|
state := &outputState{
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -186,10 +184,8 @@ func runShellInteractive(session bool) {
|
|||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
@@ -375,7 +371,13 @@ func killShell() {
|
|||||||
|
|
||||||
func runShellDaemon(session bool) {
|
func runShellDaemon(session bool) {
|
||||||
isSessionManaged = session
|
isSessionManaged = session
|
||||||
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
|
isDaemonChild := false
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if arg == "--daemon-child" {
|
||||||
|
isDaemonChild = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !isDaemonChild {
|
if !isDaemonChild {
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||||
@@ -426,10 +428,8 @@ func runShellDaemon(session bool) {
|
|||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
@@ -531,20 +531,12 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
|
||||||
out, err := exec.Command("qs", "ipc", "--help").Output()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(string(out), "--any-display")
|
|
||||||
})
|
|
||||||
|
|
||||||
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
targets := make(ipcTargets)
|
targets := make(ipcTargets)
|
||||||
var currentTarget string
|
var currentTarget string
|
||||||
for line := range strings.SplitSeq(output, "\n") {
|
for _, line := range strings.Split(output, "\n") {
|
||||||
if after, ok := strings.CutPrefix(line, "target "); ok {
|
if strings.HasPrefix(line, "target ") {
|
||||||
currentTarget = strings.TrimSpace(after)
|
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
||||||
targets[currentTarget] = make(map[string][]string)
|
targets[currentTarget] = make(map[string][]string)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
@@ -569,11 +561,7 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getShellIPCCompletions(args []string, _ string) []string {
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
var targets ipcTargets
|
var targets ipcTargets
|
||||||
|
|
||||||
@@ -618,20 +606,16 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
log.Error("IPC command requires arguments")
|
||||||
return
|
log.Info("Usage: dms ipc <command> [args...]")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if args[0] != "call" {
|
if args[0] != "call" {
|
||||||
args = append([]string{"call"}, args...)
|
args = append([]string{"call"}, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
|
||||||
cmdArgs = append(cmdArgs, args...)
|
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@@ -641,45 +625,3 @@ 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, ", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,7 +36,13 @@ func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
||||||
isKnownState := slices.Contains(knownStates, stateStr)
|
isKnownState := false
|
||||||
|
for _, known := range knownStates {
|
||||||
|
if stateStr == known {
|
||||||
|
isKnownState = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !isKnownState {
|
if !isKnownState {
|
||||||
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
||||||
|
|||||||
36
core/go.mod
36
core/go.mod
@@ -4,37 +4,33 @@ 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.23.1
|
|
||||||
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
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.0
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||||
github.com/pilebones/go-udev v0.9.1
|
github.com/pilebones/go-udev v0.9.1
|
||||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.1
|
||||||
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-20260112195511-716be5621a96
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||||
golang.org/x/image v0.35.0
|
golang.org/x/image v0.34.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.8.0 // indirect
|
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.1 // 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-20260114122816-19306b749ecc // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // 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
|
||||||
@@ -42,21 +38,21 @@ 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.47.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
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.3.3 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
github.com/charmbracelet/x/ansi v0.11.2 // 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-20260123133532-f99a98e81ce9
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805
|
||||||
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
|
||||||
@@ -70,7 +66,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.40.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/text v0.33.0
|
golang.org/x/text v0.32.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
96
core/go.sum
96
core/go.sum
@@ -4,14 +4,6 @@ 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.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
|
||||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
|
||||||
github.com/alecthomas/repr v0.5.2/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=
|
||||||
@@ -24,38 +16,36 @@ 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.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
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.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||||
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.8.0 h1:/z8v+H+4XLluJKS7rAc7uHZTalT5Z+1430ld3lePSRI=
|
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||||
github.com/clipperhouse/displaywidth v0.8.0/go.mod h1:UpOXiIKep+TohQYwvAAM/VDU8v3Z5rnWTxiwueR0XvQ=
|
github.com/clipperhouse/displaywidth v0.6.0/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.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
github.com/clipperhouse/uax29/v2 v2.4.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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
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=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
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/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
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=
|
||||||
@@ -66,24 +56,22 @@ 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-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
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/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9 h1:VzdR70t+SMjYnBgnbtNpq4ElZAAovLPMG+GFX8OBRtM=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9/go.mod h1:EWlxLBkiFCzXNCadvt05fT9PCAE2sUedgDsvUUIo18s=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||||
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.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.0/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=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
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=
|
||||||
@@ -124,14 +112,14 @@ 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-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/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.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
@@ -139,43 +127,35 @@ 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=
|
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/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
|
||||||
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
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=
|
||||||
|
|||||||
@@ -330,163 +330,3 @@ func selectPreferredMimeType(mimes []string) string {
|
|||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Offer struct {
|
|
||||||
MimeType string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
|
||||||
return copyMultiFork(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
return copyMultiServe(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, offer := range offers {
|
|
||||||
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
|
||||||
if _, err := stdin.Write(offer.Data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write offer data: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
ctx := display.Context()
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get registry: %w", err)
|
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
source, err := dataControlMgr.CreateDataSource()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create data source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
offerMap := make(map[string][]byte)
|
|
||||||
for _, offer := range offers {
|
|
||||||
if err := source.Offer(offer.MimeType); err != nil {
|
|
||||||
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
|
||||||
}
|
|
||||||
offerMap[offer.MimeType] = offer.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
|
||||||
pasted := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
defer syscall.Close(e.Fd)
|
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
|
||||||
file.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case pasted <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
|
||||||
close(cancelled)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := device.SetSelection(source); err != nil {
|
|
||||||
return fmt.Errorf("set selection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-cancelled:
|
|
||||||
return nil
|
|
||||||
case <-pasted:
|
|
||||||
if pasteOnce {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := ctx.Dispatch(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|||||||
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbPath, err := GetDBPath()
|
dbPath, err := getDBPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get db path: %w", err)
|
return fmt.Errorf("get db path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{Timeout: 1 * time.Second})
|
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open db: %w", err)
|
return fmt.Errorf("open db: %w", err)
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDBPath() (string, error) {
|
func getDBPath() (string, error) {
|
||||||
cacheDir, err := os.UserCacheDir()
|
cacheDir, err := os.UserCacheDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
@@ -121,31 +121,12 @@ func GetDBPath() (string, error) {
|
|||||||
cacheDir = filepath.Join(homeDir, ".cache")
|
cacheDir = filepath.Join(homeDir, ".cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
|
dbDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||||
newPath := filepath.Join(newDir, "db")
|
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
||||||
|
|
||||||
if _, err := os.Stat(newPath); err == nil {
|
|
||||||
return newPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
oldDir := filepath.Join(cacheDir, "dms-clipboard")
|
|
||||||
oldPath := filepath.Join(oldDir, "db")
|
|
||||||
|
|
||||||
if _, err := os.Stat(oldPath); err == nil {
|
|
||||||
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := os.Rename(oldPath, newPath); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
os.Remove(oldDir)
|
|
||||||
return newPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return newPath, nil
|
|
||||||
|
return filepath.Join(dbDir, "db"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||||
|
|||||||
@@ -221,7 +221,10 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
|
|
||||||
case client.OutputInterfaceName:
|
case client.OutputInterfaceName:
|
||||||
output := client.NewOutput(p.ctx)
|
output := client.NewOutput(p.ctx)
|
||||||
version := min(e.Version, 4)
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
p.outputsMu.Lock()
|
p.outputsMu.Lock()
|
||||||
p.outputs[e.Name] = &Output{
|
p.outputs[e.Name] = &Output{
|
||||||
@@ -236,14 +239,20 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
|
|
||||||
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
||||||
version := min(e.Version, 4)
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
||||||
p.layerShell = layerShell
|
p.layerShell = layerShell
|
||||||
}
|
}
|
||||||
|
|
||||||
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
||||||
version := min(e.Version, 3)
|
version := e.Version
|
||||||
|
if version > 3 {
|
||||||
|
version = 3
|
||||||
|
}
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
||||||
p.screencopy = screencopy
|
p.screencopy = screencopy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1157,7 +1157,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
|
|||||||
rOff, bOff = 2, 0
|
rOff, bOff = 2, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for row := range fontH {
|
for row := 0; row < fontH; row++ {
|
||||||
yy := y + row
|
yy := y + row
|
||||||
if yy < 0 || yy >= height {
|
if yy < 0 || yy >= height {
|
||||||
continue
|
continue
|
||||||
@@ -1165,7 +1165,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
|
|||||||
rowPattern := g[row]
|
rowPattern := g[row]
|
||||||
dstRowOff := yy * stride
|
dstRowOff := yy * stride
|
||||||
|
|
||||||
for colIdx := range fontW {
|
for colIdx := 0; colIdx < fontW; colIdx++ {
|
||||||
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
|
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
|
|||||||
const goroutines = 50
|
const goroutines = 50
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := range goroutines {
|
for i := 0; i < goroutines; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
@@ -34,21 +34,21 @@ func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
|||||||
const goroutines = 30
|
const goroutines = 30
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := range goroutines / 2 {
|
for i := 0; i < goroutines/2; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
s.SetScale(int32(id%3 + 1))
|
s.SetScale(int32(id%3 + 1))
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 2 {
|
for i := 0; i < goroutines/2; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
scale := s.Scale()
|
scale := s.Scale()
|
||||||
assert.GreaterOrEqual(t, scale, int32(1))
|
assert.GreaterOrEqual(t, scale, int32(1))
|
||||||
}
|
}
|
||||||
@@ -65,21 +65,21 @@ func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
|||||||
const goroutines = 20
|
const goroutines = 20
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for i := range goroutines / 2 {
|
for i := 0; i < goroutines/2; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 2 {
|
for i := 0; i < goroutines/2; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
w, h := s.LogicalSize()
|
w, h := s.LogicalSize()
|
||||||
_ = w
|
_ = w
|
||||||
_ = h
|
_ = h
|
||||||
@@ -97,31 +97,31 @@ func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
|||||||
const goroutines = 30
|
const goroutines = 30
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for range goroutines / 3 {
|
for i := 0; i < goroutines/3; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
s.OnPointerButton(0x110, 1)
|
s.OnPointerButton(0x110, 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 3 {
|
for i := 0; i < goroutines/3; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
s.OnKey(1, 1)
|
s.OnKey(1, 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 3 {
|
for i := 0; i < goroutines/3; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
picked, cancelled := s.IsDone()
|
picked, cancelled := s.IsDone()
|
||||||
_ = picked
|
_ = picked
|
||||||
_ = cancelled
|
_ = cancelled
|
||||||
@@ -139,11 +139,11 @@ func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
|||||||
const goroutines = 20
|
const goroutines = 20
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for range goroutines {
|
for i := 0; i < goroutines; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
_ = s.IsReady()
|
_ = s.IsReady()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -159,11 +159,11 @@ func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
|||||||
const goroutines = 20
|
const goroutines = 20
|
||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for range goroutines {
|
for i := 0; i < goroutines; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range iterations {
|
for j := 0; j < iterations; j++ {
|
||||||
s.SwapBuffers()
|
s.SwapBuffers()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig, dmsDir)
|
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
||||||
} else {
|
} else {
|
||||||
@@ -185,7 +185,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -209,18 +209,16 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
{"layout.kdl", NiriLayoutConfig},
|
{"layout.kdl", NiriLayoutConfig},
|
||||||
{"alttab.kdl", NiriAlttabConfig},
|
{"alttab.kdl", NiriAlttabConfig},
|
||||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.kdl", ""},
|
|
||||||
{"cursor.kdl", ""},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 and is not empty to preserve user modifications
|
// Skip if file already exists to preserve user modifications
|
||||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
if _, err := os.Stat(path); err == nil {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
@@ -238,7 +236,7 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -254,14 +252,14 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0o644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -276,12 +274,12 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
themesDir := filepath.Dir(colorResult.Path)
|
themesDir := filepath.Dir(colorResult.Path)
|
||||||
if err := os.MkdirAll(themesDir, 0o755); err != nil {
|
if err := os.MkdirAll(themesDir, 0755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0o644); err != nil {
|
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
||||||
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
||||||
return results, colorResult.Error
|
return results, colorResult.Error
|
||||||
}
|
}
|
||||||
@@ -302,7 +300,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -318,14 +316,14 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0o644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -339,7 +337,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0o644); err != nil {
|
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
|
||||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
return results, themeResult.Error
|
return results, themeResult.Error
|
||||||
}
|
}
|
||||||
@@ -353,7 +351,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0o644); err != nil {
|
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
|
||||||
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
||||||
return results, tabsResult.Error
|
return results, tabsResult.Error
|
||||||
}
|
}
|
||||||
@@ -374,7 +372,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -390,14 +388,14 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0o644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -411,7 +409,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0o644); err != nil {
|
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
|
||||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
return results, themeResult.Error
|
return results, themeResult.Error
|
||||||
}
|
}
|
||||||
@@ -423,31 +421,24 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
||||||
|
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
||||||
|
// Regular expression to match output sections (including commented ones)
|
||||||
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
|
|
||||||
|
// Find all output sections in the existing config
|
||||||
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
if len(existingOutputs) == 0 {
|
if len(existingOutputs) == 0 {
|
||||||
|
// No output sections to merge
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputsPath := filepath.Join(dmsDir, "outputs.kdl")
|
// Remove the example output section from the new config
|
||||||
if _, err := os.Stat(outputsPath); err != nil {
|
|
||||||
var outputsContent strings.Builder
|
|
||||||
for _, output := range existingOutputs {
|
|
||||||
outputsContent.WriteString(output)
|
|
||||||
outputsContent.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
|
|
||||||
} else {
|
|
||||||
cd.log("Migrated output sections to dms/outputs.kdl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
|
// Find where to insert the output sections (after the input section)
|
||||||
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
||||||
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
||||||
|
|
||||||
@@ -455,6 +446,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
|||||||
return "", fmt.Errorf("could not find insertion point for output sections")
|
return "", fmt.Errorf("could not find insertion point for output sections")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert after the first closing brace (end of input section)
|
||||||
insertPos := inputMatches[0][1]
|
insertPos := inputMatches[0][1]
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
@@ -479,17 +471,11 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
|
||||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
|
||||||
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
|
||||||
return result, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingConfig string
|
var existingConfig string
|
||||||
if _, err := os.Stat(result.Path); err == nil {
|
if _, err := os.Stat(result.Path); err == nil {
|
||||||
cd.log("Found existing Hyprland configuration")
|
cd.log("Found existing Hyprland configuration")
|
||||||
@@ -503,7 +489,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -529,7 +515,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir)
|
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
||||||
} else {
|
} else {
|
||||||
@@ -538,50 +524,18 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
|
||||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
|
||||||
return result, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Deployed = true
|
result.Deployed = true
|
||||||
cd.log("Successfully deployed Hyprland configuration")
|
cd.log("Successfully deployed Hyprland configuration")
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
||||||
configs := []struct {
|
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
||||||
name string
|
|
||||||
content string
|
|
||||||
}{
|
|
||||||
{"colors.conf", HyprColorsConfig},
|
|
||||||
{"layout.conf", HyprLayoutConfig},
|
|
||||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
|
||||||
{"outputs.conf", ""},
|
|
||||||
{"cursor.conf", ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cfg := range configs {
|
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
|
||||||
// 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))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
|
||||||
}
|
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
|
||||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
@@ -589,20 +543,6 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputsPath := filepath.Join(dmsDir, "outputs.conf")
|
|
||||||
if _, err := os.Stat(outputsPath); err != nil {
|
|
||||||
var outputsContent strings.Builder
|
|
||||||
for _, monitor := range existingMonitors {
|
|
||||||
outputsContent.WriteString(monitor)
|
|
||||||
outputsContent.WriteString("\n")
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
|
||||||
} else {
|
|
||||||
cd.log("Migrated monitor sections to dms/outputs.conf")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,7 @@ layout {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
|
||||||
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig, tmpDir)
|
|
||||||
|
|
||||||
if tt.wantError {
|
if tt.wantError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -220,9 +219,9 @@ func TestConfigDeploymentFlow(t *testing.T) {
|
|||||||
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
||||||
existingContent := "# Old config\nfont-size = 14\n"
|
existingContent := "# Old config\nfont-size = 14\n"
|
||||||
ghosttyPath := getGhosttyPath()
|
ghosttyPath := getGhosttyPath()
|
||||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0o644)
|
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results, err := cd.deployGhosttyConfig()
|
results, err := cd.deployGhosttyConfig()
|
||||||
@@ -363,8 +362,7 @@ input {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
|
||||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
|
||||||
|
|
||||||
if tt.wantError {
|
if tt.wantError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -408,7 +406,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), "source = ./dms/binds.conf")
|
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "exec-once = ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -422,9 +420,9 @@ general {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
@@ -444,7 +442,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), "source = ./dms/binds.conf")
|
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -461,7 +459,10 @@ 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, "source = ./dms/binds.conf")
|
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||||
|
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
||||||
|
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||||
|
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
@@ -600,9 +601,9 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
|
|||||||
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
||||||
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||||
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
||||||
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0o755)
|
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0o644)
|
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results, err := cd.deployAlacrittyConfig()
|
results, err := cd.deployAlacrittyConfig()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func LocateDMSConfig() (string, error) {
|
|||||||
dataDirs = "/usr/local/share:/usr/share"
|
dataDirs = "/usr/local/share:/usr/share"
|
||||||
}
|
}
|
||||||
|
|
||||||
for dir := range strings.SplitSeq(dataDirs, ":") {
|
for _, dir := range strings.Split(dataDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func LocateDMSConfig() (string, error) {
|
|||||||
configDirs = "/etc/xdg"
|
configDirs = "/etc/xdg"
|
||||||
}
|
}
|
||||||
|
|
||||||
for dir := range strings.SplitSeq(configDirs, ":") {
|
for _, dir := range strings.Split(configDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
# === 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
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# ! Auto-generated file. Do not edit directly.
|
|
||||||
# Remove source = ./dms/colors.conf from your config to override.
|
|
||||||
|
|
||||||
$primary = rgb(d0bcff)
|
|
||||||
$outline = rgb(948f99)
|
|
||||||
$error = rgb(f2b8b5)
|
|
||||||
|
|
||||||
general {
|
|
||||||
col.active_border = $primary
|
|
||||||
col.inactive_border = $outline
|
|
||||||
}
|
|
||||||
|
|
||||||
group {
|
|
||||||
col.border_active = $primary
|
|
||||||
col.border_inactive = $outline
|
|
||||||
col.border_locked_active = $error
|
|
||||||
col.border_locked_inactive = $outline
|
|
||||||
|
|
||||||
groupbar {
|
|
||||||
col.active = $primary
|
|
||||||
col.inactive = $outline
|
|
||||||
col.locked_active = $error
|
|
||||||
col.locked_inactive = $outline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Auto-generated by DMS - do not edit manually
|
|
||||||
|
|
||||||
general {
|
|
||||||
gaps_in = 4
|
|
||||||
gaps_out = 4
|
|
||||||
border_size = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
decoration {
|
|
||||||
rounding = 12
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,10 @@ input {
|
|||||||
general {
|
general {
|
||||||
gaps_in = 5
|
gaps_in = 5
|
||||||
gaps_out = 5
|
gaps_out = 5
|
||||||
border_size = 2
|
border_size = 0 # off in niri
|
||||||
|
|
||||||
|
col.active_border = rgba(707070ff)
|
||||||
|
col.inactive_border = rgba(d0d0d0ff)
|
||||||
|
|
||||||
layout = dwindle
|
layout = dwindle
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,7 @@ decoration {
|
|||||||
rounding = 12
|
rounding = 12
|
||||||
|
|
||||||
active_opacity = 1.0
|
active_opacity = 1.0
|
||||||
inactive_opacity = 1.0
|
inactive_opacity = 0.9
|
||||||
|
|
||||||
shadow {
|
shadow {
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -81,37 +84,196 @@ master {
|
|||||||
misc {
|
misc {
|
||||||
disable_hyprland_logo = true
|
disable_hyprland_logo = true
|
||||||
disable_splash_rendering = true
|
disable_splash_rendering = true
|
||||||
|
vrr = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# WINDOW RULES
|
# WINDOW RULES
|
||||||
# ==================
|
# ==================
|
||||||
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
|
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
|
||||||
|
|
||||||
windowrule = rounding 12, match:class ^(org\.gnome\.)
|
windowrulev2 = rounding 12, class:^(org\.gnome\.)
|
||||||
|
windowrulev2 = noborder, class:^(org\.gnome\.)
|
||||||
|
|
||||||
windowrule = tile on, match:class ^(gnome-control-center)$
|
windowrulev2 = tile, class:^(gnome-control-center)$
|
||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
windowrulev2 = tile, class:^(pavucontrol)$
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
windowrulev2 = tile, class:^(nm-connection-editor)$
|
||||||
|
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
windowrulev2 = float, class:^(gnome-calculator)$
|
||||||
windowrule = float on, match:class ^(galculator)$
|
windowrulev2 = float, class:^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrulev2 = float, class:^(blueman-manager)$
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
|
||||||
windowrule = float on, match:class ^(steam)$
|
windowrulev2 = float, class:^(steam)$
|
||||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
windowrulev2 = float, class:^(xdg-desktop-portal)$
|
||||||
|
|
||||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
|
||||||
windowrule = float on, match:class ^(zoom)$
|
windowrulev2 = noborder, class:^(Alacritty)$
|
||||||
|
windowrulev2 = noborder, class:^(zen)$
|
||||||
|
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
|
||||||
|
windowrulev2 = noborder, class:^(kitty)$
|
||||||
|
|
||||||
|
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
||||||
|
windowrulev2 = float, class:^(zoom)$
|
||||||
|
|
||||||
# DMS windows floating by default
|
# DMS windows floating by default
|
||||||
# ! Hyprland doesn't size these windows correctly so disabling by default here
|
windowrulev2 = float, class:^(org.quickshell)$
|
||||||
# windowrule = float on, match:class ^(org.quickshell)$
|
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
layerrule = noanim, ^(quickshell)$
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
# ==================
|
||||||
source = ./dms/outputs.conf
|
# KEYBINDINGS
|
||||||
source = ./dms/layout.conf
|
# ==================
|
||||||
source = ./dms/cursor.conf
|
$mod = SUPER
|
||||||
source = ./dms/binds.conf
|
|
||||||
|
# === 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
|
||||||
|
|
||||||
|
# === 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
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
// === System & Overview ===
|
// === System & Overview ===
|
||||||
Mod+D repeat=false { toggle-overview; }
|
Mod+D repeat=false { toggle-overview; }
|
||||||
@@ -15,8 +20,6 @@ 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";
|
||||||
}
|
}
|
||||||
@@ -48,18 +51,6 @@ binds {
|
|||||||
XF86AudioMicMute allow-when-locked=true {
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||||
}
|
}
|
||||||
XF86AudioPause allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
|
||||||
}
|
|
||||||
XF86AudioPlay allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
|
||||||
}
|
|
||||||
XF86AudioPrev allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "mpris" "previous";
|
|
||||||
}
|
|
||||||
XF86AudioNext allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "mpris" "next";
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Brightness Controls ===
|
// === Brightness Controls ===
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
@@ -133,11 +124,6 @@ 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; }
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
// ! Auto-generated file. Do not edit directly.
|
// ! DO NOT EDIT !
|
||||||
// Remove `include "dms/colors.kdl"` from your config to override.
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
background-color "transparent"
|
background-color "transparent"
|
||||||
|
|
||||||
focus-ring {
|
focus-ring {
|
||||||
active-color "#d0bcff"
|
active-color "#9dcbfb"
|
||||||
inactive-color "#948f99"
|
inactive-color "#8c9199"
|
||||||
urgent-color "#f2b8b5"
|
urgent-color "#ffb4ab"
|
||||||
}
|
}
|
||||||
|
|
||||||
border {
|
border {
|
||||||
active-color "#d0bcff"
|
active-color "#9dcbfb"
|
||||||
inactive-color "#948f99"
|
inactive-color "#8c9199"
|
||||||
urgent-color "#f2b8b5"
|
urgent-color "#ffb4ab"
|
||||||
}
|
}
|
||||||
|
|
||||||
shadow {
|
shadow {
|
||||||
@@ -21,19 +23,19 @@ layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tab-indicator {
|
tab-indicator {
|
||||||
active-color "#d0bcff"
|
active-color "#9dcbfb"
|
||||||
inactive-color "#948f99"
|
inactive-color "#8c9199"
|
||||||
urgent-color "#f2b8b5"
|
urgent-color "#ffb4ab"
|
||||||
}
|
}
|
||||||
|
|
||||||
insert-hint {
|
insert-hint {
|
||||||
color "#d0bcff80"
|
color "#9dcbfb80"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
highlight {
|
highlight {
|
||||||
active-color "#4f378b"
|
active-color "#124a73"
|
||||||
urgent-color "#f2b8b5"
|
urgent-color "#ffb4ab"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ window-rule {
|
|||||||
match app-id="kitty"
|
match app-id="kitty"
|
||||||
draw-border-with-background false
|
draw-border-with-background false
|
||||||
}
|
}
|
||||||
|
window-rule {
|
||||||
|
match is-active=false
|
||||||
|
opacity 0.9
|
||||||
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||||
match app-id="zoom"
|
match app-id="zoom"
|
||||||
@@ -269,5 +273,3 @@ include "dms/colors.kdl"
|
|||||||
include "dms/layout.kdl"
|
include "dms/layout.kdl"
|
||||||
include "dms/alttab.kdl"
|
include "dms/alttab.kdl"
|
||||||
include "dms/binds.kdl"
|
include "dms/binds.kdl"
|
||||||
include "dms/outputs.kdl"
|
|
||||||
include "dms/cursor.kdl"
|
|
||||||
|
|||||||
@@ -4,12 +4,3 @@ import _ "embed"
|
|||||||
|
|
||||||
//go:embed embedded/hyprland.conf
|
//go:embed embedded/hyprland.conf
|
||||||
var HyprlandConfig string
|
var HyprlandConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-colors.conf
|
|
||||||
var HyprColorsConfig string
|
|
||||||
|
|
||||||
//go:embed embedded/hypr-layout.conf
|
|
||||||
var HyprLayoutConfig string
|
|
||||||
|
|
||||||
//go:embed embedded/hypr-binds.conf
|
|
||||||
var HyprBindsConfig string
|
|
||||||
|
|||||||
@@ -199,6 +199,31 @@ 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)
|
||||||
@@ -320,7 +345,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
}
|
}
|
||||||
|
|
||||||
step := 0.5
|
step := 0.5
|
||||||
for range 120 {
|
for i := 0; i < 120; i++ {
|
||||||
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
||||||
cand := labToHex(Lf, af, bf)
|
cand := labToHex(Lf, af, bf)
|
||||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
@@ -331,59 +356,6 @@ 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
|
||||||
@@ -397,29 +369,6 @@ 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)
|
||||||
@@ -440,9 +389,6 @@ 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
|
||||||
@@ -464,136 +410,115 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
}
|
}
|
||||||
palette.Color0 = NewColorInfo(bgColor)
|
palette.Color0 = NewColorInfo(bgColor)
|
||||||
|
|
||||||
baseSat := math.Max(ph.S, 0.5)
|
hueShift := (hsv.H - 0.6) * 0.12
|
||||||
baseVal := math.Max(ph.V, 0.5)
|
satBoost := 1.15
|
||||||
|
|
||||||
redH := blendHue(0.0, ph.H, 0.12)
|
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
||||||
greenH := blendHue(0.33, ph.H, 0.10)
|
var redColor string
|
||||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
if opts.IsLight {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
accentTarget := secondaryTarget * 0.7
|
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||||
|
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 {
|
||||||
redS := math.Min(baseSat*1.2, 1.0)
|
palette.Color7 = NewColorInfo("#1a1a1a")
|
||||||
redV := baseVal * 0.95
|
palette.Color8 = NewColorInfo("#2e2e2e")
|
||||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
|
||||||
|
|
||||||
greenS := math.Min(baseSat*1.3, 1.0)
|
|
||||||
greenV := baseVal * 0.75
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
|
||||||
|
|
||||||
yellowS := math.Min(baseSat*1.5, 1.0)
|
|
||||||
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 {
|
||||||
redS := math.Min(baseSat*1.1, 1.0)
|
palette.Color7 = NewColorInfo("#abb2bf")
|
||||||
redV := math.Min(baseVal*1.15, 1.0)
|
palette.Color8 = NewColorInfo("#5c6370")
|
||||||
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
}
|
||||||
|
|
||||||
greenS := math.Min(baseSat*1.0, 1.0)
|
if opts.IsLight {
|
||||||
greenV := math.Min(baseVal*1.0, 1.0)
|
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, 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))
|
||||||
|
}
|
||||||
|
|
||||||
yellowS := math.Min(baseSat*1.1, 1.0)
|
if opts.IsLight {
|
||||||
yellowV := math.Min(baseVal*1.25, 1.0)
|
palette.Color15 = NewColorInfo("#1a1a1a")
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
} else {
|
||||||
|
palette.Color15 = NewColorInfo("#ffffff")
|
||||||
// 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,19 +366,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color15 is now derived from primary, so just verify it's a valid color
|
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
||||||
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
||||||
color15Lum := Luminance(result.Color15.Hex)
|
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
||||||
if tt.opts.IsLight {
|
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -588,10 +579,6 @@ 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 {
|
||||||
@@ -671,7 +658,7 @@ func TestContrastAlgorithmComparison(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
differentCount := 0
|
differentCount := 0
|
||||||
for i := range 16 {
|
for i := 0; i < 16; i++ {
|
||||||
if wcagColors[i].Hex != dpsColors[i].Hex {
|
if wcagColors[i].Hex != dpsColors[i].Hex {
|
||||||
differentCount++
|
differentCount++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -41,9 +40,6 @@ 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 {
|
||||||
@@ -518,9 +514,12 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
if slices.Contains(dmsDepencies, pkg) {
|
for _, dep := range dmsDepencies {
|
||||||
deps = append(deps, pkg)
|
if pkg == dep {
|
||||||
isDep = true
|
deps = append(deps, pkg)
|
||||||
|
isDep = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !isDep {
|
if !isDep {
|
||||||
others = append(others, pkg)
|
others = append(others, pkg)
|
||||||
@@ -546,7 +545,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
||||||
if err := os.MkdirAll(envDir, 0o755); err != nil {
|
if err := os.MkdirAll(envDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +555,7 @@ TERMINAL=%s
|
|||||||
`, terminalCmd)
|
`, terminalCmd)
|
||||||
|
|
||||||
envFile := filepath.Join(envDir, "90-dms.conf")
|
envFile := filepath.Join(envDir, "90-dms.conf")
|
||||||
if err := os.WriteFile(envFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write environment config: %w", err)
|
return fmt.Errorf("failed to write environment config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +594,7 @@ func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
||||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +605,7 @@ Requires=graphical-session.target
|
|||||||
After=graphical-session.target
|
After=graphical-session.target
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0o755)
|
os.MkdirAll(dmsPath, 0755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -55,7 +55,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0o644)
|
os.WriteFile(testFile, []byte("test"), 0644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0o755)
|
os.MkdirAll(dmsPath, 0755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -99,7 +99,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0o644)
|
os.WriteFile(testFile, []byte("test"), 0644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
||||||
@@ -125,7 +125,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0o755)
|
os.MkdirAll(dmsPath, 0755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|||||||
@@ -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: "sdegler/hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type ManualPackageInstaller struct {
|
|||||||
|
|
||||||
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
||||||
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
||||||
lines := strings.SplitSeq(output, "\n")
|
lines := strings.Split(output, "\n")
|
||||||
for line := range lines {
|
for _, line := range lines {
|
||||||
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
||||||
parts := strings.Split(line, "refs/tags/")
|
parts := strings.Split(line, "refs/tags/")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
@@ -103,12 +103,12 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -160,10 +160,10 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
||||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
||||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -237,12 +237,12 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -273,7 +273,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildDir := tmpDir + "/build"
|
buildDir := tmpDir + "/build"
|
||||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,12 +343,12 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -406,12 +406,12 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(dmsPath)
|
configDir := filepath.Dir(dmsPath)
|
||||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ 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},
|
||||||
@@ -116,7 +117,6 @@ 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"},
|
||||||
}
|
}
|
||||||
@@ -540,12 +540,12 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -576,7 +576,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildDir := tmpDir + "/build"
|
buildDir := tmpDir + "/build"
|
||||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
450
core/internal/dms/app.go
Normal file
450
core/internal/dms/app.go
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
//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()
|
||||||
|
}
|
||||||
|
}
|
||||||
267
core/internal/dms/app_distro.go
Normal file
267
core/internal/dms/app_distro.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
//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()
|
||||||
|
}
|
||||||
|
}
|
||||||
143
core/internal/dms/detector.go
Normal file
143
core/internal/dms/detector.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
54
core/internal/dms/handlers_common.go
Normal file
54
core/internal/dms/handlers_common.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
392
core/internal/dms/handlers_features.go
Normal file
392
core/internal/dms/handlers_features.go
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
61
core/internal/dms/handlers_mainmenu.go
Normal file
61
core/internal/dms/handlers_mainmenu.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
377
core/internal/dms/plugins_handlers.go
Normal file
377
core/internal/dms/plugins_handlers.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
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}
|
||||||
|
}
|
||||||
|
}
|
||||||
367
core/internal/dms/plugins_views.go
Normal file
367
core/internal/dms/plugins_views.go
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
152
core/internal/dms/views_common.go
Normal file
152
core/internal/dms/views_common.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
529
core/internal/dms/views_features.go
Normal file
529
core/internal/dms/views_features.go
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
@@ -235,7 +235,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
for _, dir := range parentDirs {
|
for _, dir := range parentDirs {
|
||||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dir.path, 0o755); err != nil {
|
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -295,7 +295,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
for _, dir := range configDirs {
|
for _, dir := range configDirs {
|
||||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dir.path, 0o755); err != nil {
|
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -355,14 +355,14 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
|
|||||||
for _, link := range symlinks {
|
for _, link := range symlinks {
|
||||||
sourceDir := filepath.Dir(link.source)
|
sourceDir := filepath.Dir(link.source)
|
||||||
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(link.source, []byte("{}"), 0644); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -455,7 +455,7 @@ user = "greeter"
|
|||||||
newConfig := strings.Join(finalLines, "\n")
|
newConfig := strings.Join(finalLines, "\n")
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
tmpFile := "/tmp/greetd-config.toml"
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func DefaultDiscoveryConfig() *DiscoveryConfig {
|
|||||||
|
|
||||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
if configDirs != "" {
|
if configDirs != "" {
|
||||||
for dir := range strings.SplitSeq(configDirs, ":") {
|
for _, dir := range strings.Split(configDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
searchPaths = append(searchPaths, filepath.Join(dir, "DankMaterialShell", "cheatsheets"))
|
searchPaths = append(searchPaths, filepath.Join(dir, "DankMaterialShell", "cheatsheets"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ func TestFindJSONFiles(t *testing.T) {
|
|||||||
txtFile := filepath.Join(tmpDir, "readme.txt")
|
txtFile := filepath.Join(tmpDir, "readme.txt")
|
||||||
subdir := filepath.Join(tmpDir, "subdir")
|
subdir := filepath.Join(tmpDir, "subdir")
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create file1: %v", err)
|
t.Fatalf("Failed to create file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create file2: %v", err)
|
t.Fatalf("Failed to create file2: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(txtFile, []byte("text"), 0o644); err != nil {
|
if err := os.WriteFile(txtFile, []byte("text"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create txt file: %v", err)
|
t.Fatalf("Failed to create txt file: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
if err := os.MkdirAll(subdir, 0755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +143,10 @@ func TestFindJSONFilesMultiplePaths(t *testing.T) {
|
|||||||
file1 := filepath.Join(tmpDir1, "app1.json")
|
file1 := filepath.Join(tmpDir1, "app1.json")
|
||||||
file2 := filepath.Join(tmpDir2, "app2.json")
|
file2 := filepath.Join(tmpDir2, "app2.json")
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create file1: %v", err)
|
t.Fatalf("Failed to create file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create file2: %v", err)
|
t.Fatalf("Failed to create file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ func TestAutoDiscoverProviders(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
file := filepath.Join(tmpDir, "testapp.json")
|
file := filepath.Join(tmpDir, "testapp.json")
|
||||||
if err := os.WriteFile(file, []byte(jsonContent), 0o644); err != nil {
|
if err := os.WriteFile(file, []byte(jsonContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ func TestAutoDiscoverProvidersNoFactory(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
file := filepath.Join(tmpDir, "test.json")
|
file := filepath.Join(tmpDir, "test.json")
|
||||||
if err := os.WriteFile(file, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(file, []byte("{}"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,93 +2,45 @@ 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 = defaultHyprlandConfigDir()
|
configPath = "$HOME/.config/hypr"
|
||||||
}
|
}
|
||||||
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) {
|
||||||
result, err := ParseHyprlandKeysWithDMS(h.configPath)
|
section, err := ParseHyprlandKeys(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.dmsBindsIncluded = result.DMSBindsIncluded
|
|
||||||
h.parsed = true
|
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
h.convertSection(section, "", categorizedBinds)
|
||||||
|
|
||||||
sheet := &keybinds.CheatSheet{
|
return &keybinds.CheatSheet{
|
||||||
Title: "Hyprland Keybinds",
|
Title: "Hyprland Keybinds",
|
||||||
Provider: h.Name(),
|
Provider: h.Name(),
|
||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
DMSBindsIncluded: result.DMSBindsIncluded,
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
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) HasDMSBindsIncluded() bool {
|
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||||
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
|
||||||
@@ -96,12 +48,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, conflicts)
|
bind := h.convertKeybind(&kb, currentSubcat)
|
||||||
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, conflicts)
|
h.convertSection(&child, currentSubcat, categorizedBinds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,8 +85,8 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
|
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
|
||||||
keyStr := h.formatKey(kb)
|
key := h.formatKey(kb)
|
||||||
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
|
|
||||||
@@ -142,33 +94,12 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
desc = rawAction
|
desc = rawAction
|
||||||
}
|
}
|
||||||
|
|
||||||
source := "config"
|
return keybinds.Keybind{
|
||||||
if strings.Contains(kb.Source, "dms/binds.conf") {
|
Key: key,
|
||||||
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 {
|
||||||
@@ -184,314 +115,3 @@ 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), 0o755); 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), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,8 +23,6 @@ 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 {
|
||||||
@@ -34,36 +32,14 @@ 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(configDir string) *HyprlandParser {
|
func NewHyprlandParser() *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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +195,71 @@ 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]
|
||||||
return p.parseBindLine(line)
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
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 {
|
||||||
@@ -280,348 +320,9 @@ func (p *HyprlandParser) ParseKeys() *HyprlandSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
|
func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
|
||||||
parser := NewHyprlandParser(path)
|
parser := NewHyprlandParser()
|
||||||
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)
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ bind = SUPER, right, movefocus, r
|
|||||||
bind = SUPER, T, exec, kitty # Terminal
|
bind = SUPER, T, exec, kitty # Terminal
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ bind = SUPER, B, exec, app2
|
|||||||
#/# = SUPER, C, exec, app3 # Custom comment
|
#/# = SUPER, C, exec, app3 # Custom comment
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,14 +278,14 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
|||||||
content1 := "bind = SUPER, Q, killactive\n"
|
content1 := "bind = SUPER, Q, killactive\n"
|
||||||
content2 := "bind = SUPER, T, exec, kitty\n"
|
content2 := "bind = SUPER, T, exec, kitty\n"
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte(content1), 0o644); err != nil {
|
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write file1: %v", err)
|
t.Fatalf("Failed to write file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil {
|
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -328,13 +328,13 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "test.conf")
|
configFile := filepath.Join(tmpSubdir, "test.conf")
|
||||||
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %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)
|
||||||
@@ -381,7 +381,7 @@ bind = SUPER, Q, killactive
|
|||||||
bind = SUPER, T, exec, kitty
|
bind = SUPER, T, exec, kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,126 +394,3 @@ 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,30 +7,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewHyprlandProvider(t *testing.T) {
|
func TestNewHyprlandProvider(t *testing.T) {
|
||||||
t.Run("custom path", func(t *testing.T) {
|
tests := []struct {
|
||||||
p := NewHyprlandProvider("/custom/path")
|
name string
|
||||||
if p == nil {
|
configPath string
|
||||||
t.Fatal("NewHyprlandProvider returned nil")
|
wantPath string
|
||||||
}
|
}{
|
||||||
if p.configPath != "/custom/path" {
|
{
|
||||||
t.Errorf("configPath = %q, want %q", p.configPath, "/custom/path")
|
name: "custom path",
|
||||||
}
|
configPath: "/custom/path",
|
||||||
})
|
wantPath: "/custom/path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty path defaults",
|
||||||
|
configPath: "",
|
||||||
|
wantPath: "$HOME/.config/hypr",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("empty path defaults", func(t *testing.T) {
|
for _, tt := range tests {
|
||||||
p := NewHyprlandProvider("")
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if p == nil {
|
p := NewHyprlandProvider(tt.configPath)
|
||||||
t.Fatal("NewHyprlandProvider returned nil")
|
if p == nil {
|
||||||
}
|
t.Fatal("NewHyprlandProvider returned nil")
|
||||||
configDir, err := os.UserConfigDir()
|
}
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UserConfigDir failed: %v", err)
|
if p.configPath != tt.wantPath {
|
||||||
}
|
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) {
|
||||||
@@ -57,7 +62,7 @@ bind = SUPER, T, exec, kitty # Terminal
|
|||||||
bind = SUPER, 1, workspace, 1
|
bind = SUPER, 1, workspace, 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +109,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, "hyprland.conf")
|
configFile := filepath.Join(tmpDir, "test.conf")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -134,7 +139,7 @@ func TestFormatKey(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) {
|
||||||
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +163,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, "hyprland.conf")
|
configFile := filepath.Join(tmpDir, "test.conf")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -189,7 +194,7 @@ func TestDescriptionFallback(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) {
|
||||||
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestNewJSONFileProvider(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.json")
|
testFile := filepath.Join(tmpDir, "test.json")
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(testFile, []byte("{}"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ func TestJSONFileProviderGetCheatSheet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ func TestJSONFileProviderGetCheatSheetNoProvider(t *testing.T) {
|
|||||||
"binds": {}
|
"binds": {}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ func TestJSONFileProviderFlatArrayBackwardsCompat(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ func TestJSONFileProviderInvalidJSON(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "invalid.json")
|
testFile := filepath.Join(tmpDir, "invalid.json")
|
||||||
|
|
||||||
if err := os.WriteFile(testFile, []byte("not valid json"), 0o644); err != nil {
|
if err := os.WriteFile(testFile, []byte("not valid json"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,94 +2,46 @@ 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 = defaultMangoWCConfigDir()
|
configPath = "$HOME/.config/mango"
|
||||||
}
|
}
|
||||||
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) {
|
||||||
result, err := ParseMangoWCKeysWithDMS(m.configPath)
|
keybinds_list, err := ParseMangoWCKeys(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 result.Keybinds {
|
for _, kb := range keybinds_list {
|
||||||
category := m.categorizeByCommand(kb.Command)
|
category := m.categorizeByCommand(kb.Command)
|
||||||
bind := m.convertKeybind(&kb, result.ConflictingConfigs)
|
bind := m.convertKeybind(&kb)
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
sheet := &keybinds.CheatSheet{
|
return &keybinds.CheatSheet{
|
||||||
Title: "MangoWC Keybinds",
|
Title: "MangoWC Keybinds",
|
||||||
Provider: m.Name(),
|
Provider: m.Name(),
|
||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
DMSBindsIncluded: result.DMSBindsIncluded,
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -130,8 +82,8 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[string]*MangoWCKeyBinding) keybinds.Keybind {
|
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
|
||||||
keyStr := m.formatKey(kb)
|
key := m.formatKey(kb)
|
||||||
rawAction := m.formatRawAction(kb.Command, kb.Params)
|
rawAction := m.formatRawAction(kb.Command, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
|
|
||||||
@@ -139,31 +91,11 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
|||||||
desc = rawAction
|
desc = rawAction
|
||||||
}
|
}
|
||||||
|
|
||||||
source := "config"
|
return keybinds.Keybind{
|
||||||
if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
|
Key: key,
|
||||||
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 {
|
||||||
@@ -179,264 +111,3 @@ 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), 0o755); 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), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,40 +21,17 @@ 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(configDir string) *MangoWCParser {
|
func NewMangoWCParser() *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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,297 +294,9 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
|
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
|
||||||
parser := NewMangoWCParser(path)
|
parser := NewMangoWCParser()
|
||||||
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)
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ bind=Ctrl,1,view,1,0
|
|||||||
bind=Ctrl,2,view,2,0
|
bind=Ctrl,2,view,2,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,14 +276,14 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
|||||||
content1 := "bind=ALT,q,killclient,\n"
|
content1 := "bind=ALT,q,killclient,\n"
|
||||||
content2 := "bind=Alt,t,spawn,kitty\n"
|
content2 := "bind=Alt,t,spawn,kitty\n"
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte(content1), 0o644); err != nil {
|
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write file1: %v", err)
|
t.Fatalf("Failed to write file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil {
|
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -300,11 +300,11 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
|
|||||||
|
|
||||||
content := "bind=ALT,q,killclient,\n"
|
content := "bind=ALT,q,killclient,\n"
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -347,13 +347,13 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "config.conf")
|
configFile := filepath.Join(tmpSubdir, "config.conf")
|
||||||
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %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)
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ bind=ALT,q,killclient,
|
|||||||
bind=Alt,t,spawn,kitty
|
bind=Alt,t,spawn,kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -458,7 +458,7 @@ bind=Ctrl,2,view,2,0
|
|||||||
bind=Ctrl,3,view,3,0
|
bind=Ctrl,3,view,3,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,8 @@ func TestMangoWCProviderName(t *testing.T) {
|
|||||||
|
|
||||||
func TestMangoWCProviderDefaultPath(t *testing.T) {
|
func TestMangoWCProviderDefaultPath(t *testing.T) {
|
||||||
provider := NewMangoWCProvider("")
|
provider := NewMangoWCProvider("")
|
||||||
configDir, err := os.UserConfigDir()
|
if provider.configPath != "$HOME/.config/mango" {
|
||||||
if err != nil {
|
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/mango")
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +174,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, nil)
|
result := provider.convertKeybind(tt.keybind)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -223,7 +214,7 @@ bind=SUPER,n,switch_layout
|
|||||||
bind=ALT+SHIFT,X,incgaps,1
|
bind=ALT+SHIFT,X,incgaps,1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +276,7 @@ bind=ALT,Left,focusdir,left
|
|||||||
bind=Ctrl,1,view,1,0
|
bind=Ctrl,1,view,1,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,15 +187,7 @@ func (n *NiriProvider) formatRawAction(action string, args []string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
quotedArgs := make([]string, len(args))
|
return action + " " + strings.Join(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 {
|
||||||
@@ -235,7 +227,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
|
|||||||
|
|
||||||
overridePath := n.GetOverridePath()
|
overridePath := n.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,15 +293,9 @@ 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: action,
|
Action: n.formatRawAction(kb.Action, kb.Args),
|
||||||
Description: kb.Description,
|
Description: kb.Description,
|
||||||
Options: n.extractOptions(child),
|
Options: n.extractOptions(child),
|
||||||
}
|
}
|
||||||
@@ -319,42 +305,6 @@ 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)
|
||||||
@@ -485,7 +435,7 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(overridePath, []byte(content), 0o644)
|
return os.WriteFile(overridePath, []byte(content), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) getBindSortPriority(action string) int {
|
func (n *NiriProvider) getBindSortPriority(action string) int {
|
||||||
@@ -511,9 +461,16 @@ func (n *NiriProvider) getBindSortPriority(action string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dmsWarningHeader = `// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
||||||
if len(binds) == 0 {
|
if len(binds) == 0 {
|
||||||
return "binds {}\n"
|
return dmsWarningHeader + "binds {}\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
var regularBinds, recentWindowsBinds []*overrideBind
|
var regularBinds, recentWindowsBinds []*overrideBind
|
||||||
@@ -540,6 +497,7 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString(dmsWarningHeader)
|
||||||
sb.WriteString("binds {\n")
|
sb.WriteString("binds {\n")
|
||||||
for _, bind := range regularBinds {
|
for _, bind := range regularBinds {
|
||||||
n.writeBindNode(&sb, bind, " ")
|
n.writeBindNode(&sb, bind, " ")
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestNiriParseBasicBinds(t *testing.T) {
|
|||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
func TestNiriParseInclude(t *testing.T) {
|
func TestNiriParseInclude(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "dms")
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +165,10 @@ include "dms/binds.kdl"
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ include "dms/binds.kdl"
|
|||||||
func TestNiriParseIncludeOverride(t *testing.T) {
|
func TestNiriParseIncludeOverride(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "dms")
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,10 +202,10 @@ include "dms/binds.kdl"
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,10 +246,10 @@ include "other.kdl"
|
|||||||
include "config.kdl"
|
include "config.kdl"
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(otherConfig, []byte(otherContent), 0o644); err != nil {
|
if err := os.WriteFile(otherConfig, []byte(otherContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write other config: %v", err)
|
t.Fatalf("Failed to write other config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ func TestNiriParseMissingInclude(t *testing.T) {
|
|||||||
}
|
}
|
||||||
include "nonexistent/file.kdl"
|
include "nonexistent/file.kdl"
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "custom")
|
subDir := filepath.Join(tmpDir, "custom")
|
||||||
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,10 +409,10 @@ binds {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +508,7 @@ func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
|
|||||||
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ func TestNiriParseQuotedStringArgs(t *testing.T) {
|
|||||||
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
|||||||
Alt+Tab { next-window scope="output"; }
|
Alt+Tab { next-window scope="output"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const testHeader = `// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
func TestNiriProviderName(t *testing.T) {
|
func TestNiriProviderName(t *testing.T) {
|
||||||
provider := NewNiriProvider("")
|
provider := NewNiriProvider("")
|
||||||
if provider.Name() != "niri" {
|
if provider.Name() != "niri" {
|
||||||
@@ -27,7 +34,7 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
|
|||||||
Mod+Shift+E { quit; }
|
Mod+Shift+E { quit; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +128,6 @@ 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"},
|
||||||
@@ -199,7 +204,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "empty binds",
|
name: "empty binds",
|
||||||
binds: map[string]*overrideBind{},
|
binds: map[string]*overrideBind{},
|
||||||
expected: "binds {}\n",
|
expected: testHeader + "binds {}\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple spawn bind",
|
name: "simple spawn bind",
|
||||||
@@ -210,7 +215,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Description: "Open Terminal",
|
Description: "Open Terminal",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -224,7 +229,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Description: "Application Launcher",
|
Description: "Application Launcher",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -238,7 +243,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Options: map[string]any{"allow-when-locked": true},
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -252,7 +257,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Description: "Close Window",
|
Description: "Close Window",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -265,7 +270,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Action: "next-window",
|
Action: "next-window",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
}
|
}
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
@@ -312,7 +317,7 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write temp file: %v", err)
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,58 +331,6 @@ 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, 0o755); err != nil {
|
|
||||||
t.Fatalf("Failed to create dms directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
|
||||||
if err := os.WriteFile(bindsFile, []byte(content), 0o644); 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")
|
||||||
@@ -428,7 +381,7 @@ recent-windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +422,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Focus Workspace 1",
|
Description: "Focus Workspace 1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -483,7 +436,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Focus Workspace 10",
|
Description: "Focus Workspace 10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -497,7 +450,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Adjust Column Width -10%",
|
Description: "Adjust Column Width -10%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -511,7 +464,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Adjust Column Width +10%",
|
Description: "Adjust Column Width +10%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: `binds {
|
expected: testHeader + `binds {
|
||||||
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -540,7 +493,7 @@ func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := provider.generateBindsContent(binds)
|
content := provider.generateBindsContent(binds)
|
||||||
expected := `binds {
|
expected := testHeader + `binds {
|
||||||
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -561,7 +514,7 @@ func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := provider.generateBindsContent(binds)
|
content := provider.generateBindsContent(binds)
|
||||||
expected := `binds {
|
expected := testHeader + `binds {
|
||||||
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -582,7 +535,7 @@ func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := provider.generateBindsContent(binds)
|
content := provider.generateBindsContent(binds)
|
||||||
expected := `binds {
|
expected := testHeader + `binds {
|
||||||
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -621,7 +574,7 @@ func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write temp file: %v", err)
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ bindsym $mod+t exec $term
|
|||||||
bindsym $mod+d exec $menu
|
bindsym $mod+d exec $menu
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ bindsym $mod+Right focus right
|
|||||||
bindsym $mod+t exec kitty # Terminal
|
bindsym $mod+t exec kitty # Terminal
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ func TestSwayReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "config")
|
configFile := filepath.Join(tmpSubdir, "config")
|
||||||
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ bindsym Mod4+q kill
|
|||||||
bindsym Mod4+t exec kitty
|
bindsym Mod4+t exec kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ bindsym $mod+2 workspace number 2
|
|||||||
bindsym $mod+Shift+1 move container to workspace number 1
|
bindsym $mod+Shift+1 move container to workspace number 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ bindsym $mod+s layout stacking
|
|||||||
bindsym $mod+w layout tabbed
|
bindsym $mod+w layout tabbed
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ bindsym $mod+f fullscreen toggle
|
|||||||
bindsym $mod+1 workspace number 1
|
bindsym $mod+1 workspace number 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,64 +16,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ColorMode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ColorModeDark ColorMode = "dark"
|
|
||||||
ColorModeLight ColorMode = "light"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TemplateKind int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TemplateKindNormal TemplateKind = iota
|
|
||||||
TemplateKindTerminal
|
|
||||||
TemplateKindGTK
|
|
||||||
TemplateKindVSCode
|
|
||||||
)
|
|
||||||
|
|
||||||
type TemplateDef struct {
|
|
||||||
ID string
|
|
||||||
Commands []string
|
|
||||||
Flatpaks []string
|
|
||||||
ConfigFile string
|
|
||||||
Kind TemplateKind
|
|
||||||
RunUnconditionally bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var templateRegistry = []TemplateDef{
|
|
||||||
{ID: "gtk", Kind: TemplateKindGTK, RunUnconditionally: true},
|
|
||||||
{ID: "niri", Commands: []string{"niri"}, ConfigFile: "niri.toml"},
|
|
||||||
{ID: "hyprland", Commands: []string{"Hyprland"}, ConfigFile: "hyprland.toml"},
|
|
||||||
{ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml"},
|
|
||||||
{ID: "qt5ct", Commands: []string{"qt5ct"}, ConfigFile: "qt5ct.toml"},
|
|
||||||
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
|
||||||
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
|
||||||
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
|
||||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
|
||||||
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
|
||||||
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
|
||||||
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
|
||||||
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
|
|
||||||
{ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal},
|
|
||||||
{ID: "alacritty", Commands: []string{"alacritty"}, ConfigFile: "alacritty.toml", Kind: TemplateKindTerminal},
|
|
||||||
{ID: "wezterm", Commands: []string{"wezterm"}, ConfigFile: "wezterm.toml", Kind: TemplateKindTerminal},
|
|
||||||
{ID: "nvim", Commands: []string{"nvim"}, ConfigFile: "neovim.toml", Kind: TemplateKindTerminal},
|
|
||||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
|
||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
|
||||||
switch *c {
|
|
||||||
case ColorModeDark:
|
|
||||||
return "adw-gtk3-dark"
|
|
||||||
default:
|
|
||||||
return "adw-gtk3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
matugenVersionOnce sync.Once
|
matugenVersionOnce sync.Once
|
||||||
matugenSupportsCOE bool
|
matugenSupportsCOE bool
|
||||||
@@ -85,7 +27,7 @@ type Options struct {
|
|||||||
ConfigDir string
|
ConfigDir string
|
||||||
Kind string
|
Kind string
|
||||||
Value string
|
Value string
|
||||||
Mode ColorMode
|
Mode string
|
||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
@@ -93,7 +35,6 @@ type Options struct {
|
|||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
SkipTemplates string
|
SkipTemplates string
|
||||||
AppChecker utils.AppChecker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColorsOutput struct {
|
type ColorsOutput struct {
|
||||||
@@ -136,7 +77,7 @@ func Run(opts Options) error {
|
|||||||
return fmt.Errorf("value is required")
|
return fmt.Errorf("value is required")
|
||||||
}
|
}
|
||||||
if opts.Mode == "" {
|
if opts.Mode == "" {
|
||||||
opts.Mode = ColorModeDark
|
opts.Mode = "dark"
|
||||||
}
|
}
|
||||||
if opts.MatugenType == "" {
|
if opts.MatugenType == "" {
|
||||||
opts.MatugenType = "scheme-tonal-spot"
|
opts.MatugenType = "scheme-tonal-spot"
|
||||||
@@ -144,11 +85,8 @@ func Run(opts Options) error {
|
|||||||
if opts.IconTheme == "" {
|
if opts.IconTheme == "" {
|
||||||
opts.IconTheme = "System Default"
|
opts.IconTheme = "System Default"
|
||||||
}
|
}
|
||||||
if opts.AppChecker == nil {
|
|
||||||
opts.AppChecker = utils.DefaultAppChecker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(opts.StateDir, 0o755); err != nil {
|
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create state dir: %w", err)
|
return fmt.Errorf("failed to create state dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +145,7 @@ func buildOnce(opts *Options) error {
|
|||||||
importArgs = []string{"--import-json-string", importData}
|
importArgs = []string{"--import-json-string", importData}
|
||||||
|
|
||||||
log.Info("Running matugen color hex with stock color overrides")
|
log.Info("Running matugen color hex with stock color overrides")
|
||||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
args := []string{"color", "hex", primaryDark, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -243,7 +181,7 @@ func buildOnce(opts *Options) error {
|
|||||||
default:
|
default:
|
||||||
args = []string{opts.Kind, opts.Value}
|
args = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
args = append(args, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -282,7 +220,7 @@ func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
|||||||
if strings.TrimSpace(line) == "[config]" {
|
if strings.TrimSpace(line) == "[config]" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cfgFile.WriteString(substituteVars(line, opts.ShellDir) + "\n")
|
cfgFile.WriteString(substituteShellDir(line, opts.ShellDir) + "\n")
|
||||||
}
|
}
|
||||||
cfgFile.WriteString("\n")
|
cfgFile.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -293,34 +231,72 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
if !opts.ShouldSkipTemplate("gtk") {
|
||||||
for _, tmpl := range templateRegistry {
|
switch opts.Mode {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
case "light":
|
||||||
continue
|
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
||||||
}
|
|
||||||
|
|
||||||
switch tmpl.Kind {
|
|
||||||
case TemplateKindGTK:
|
|
||||||
switch opts.Mode {
|
|
||||||
case ColorModeLight:
|
|
||||||
appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml")
|
|
||||||
default:
|
|
||||||
appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
|
|
||||||
}
|
|
||||||
case TemplateKindTerminal:
|
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
|
||||||
case TemplateKindVSCode:
|
|
||||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
|
|
||||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-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, "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, "skip", "gtk3-dark.toml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.ShouldSkipTemplate("niri") {
|
||||||
|
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("qt5ct") {
|
||||||
|
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("qt6ct") {
|
||||||
|
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("firefox") {
|
||||||
|
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("pywalfox") {
|
||||||
|
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("vesktop") {
|
||||||
|
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("equibop") {
|
||||||
|
appendConfig(opts, cfgFile, "equibop", "equibop.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("ghostty") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("kitty") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("foot") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("alacritty") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("wezterm") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("nvim") {
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "nvim", "neovim.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.ShouldSkipTemplate("dgop") {
|
||||||
|
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.ShouldSkipTemplate("kcolorscheme") {
|
||||||
|
appendConfig(opts, cfgFile, "skip", "kcolorscheme.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.ShouldSkipTemplate("vscode") {
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
}
|
||||||
|
|
||||||
if opts.RunUserTemplates {
|
if opts.RunUserTemplates {
|
||||||
if data, err := os.ReadFile(userConfigPath); err == nil {
|
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||||
templatesSection := extractTOMLSection(string(data), "[templates]", "")
|
templatesSection := extractTOMLSection(string(data), "[templates]", "")
|
||||||
@@ -347,34 +323,28 @@ output_path = '%s'
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendConfig(
|
func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
|
||||||
opts *Options,
|
|
||||||
cfgFile *os.File,
|
|
||||||
checkCmd []string,
|
|
||||||
checkFlatpaks []string,
|
|
||||||
fileName string,
|
|
||||||
) {
|
|
||||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfgFile.WriteString(substituteVars(string(data), opts.ShellDir))
|
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir))
|
||||||
cfgFile.WriteString("\n")
|
cfgFile.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkCmd []string, checkFlatpaks []string, fileName string) {
|
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
|
||||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
@@ -385,7 +355,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
|||||||
content := string(data)
|
content := string(data)
|
||||||
|
|
||||||
if !opts.TerminalsAlwaysDark {
|
if !opts.TerminalsAlwaysDark {
|
||||||
cfgFile.WriteString(substituteVars(content, opts.ShellDir))
|
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||||
cfgFile.WriteString("\n")
|
cfgFile.WriteString("\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -414,7 +384,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
|||||||
|
|
||||||
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
||||||
tmpPath := filepath.Join(tmpDir, templateName)
|
tmpPath := filepath.Join(tmpDir, templateName)
|
||||||
if err := os.WriteFile(tmpPath, []byte(modified), 0o644); err != nil {
|
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,32 +393,14 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
|||||||
fmt.Sprintf("'%s'", tmpPath))
|
fmt.Sprintf("'%s'", tmpPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgFile.WriteString(substituteVars(content, opts.ShellDir))
|
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||||
cfgFile.WriteString("\n")
|
cfgFile.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
|
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
|
||||||
// Both nil is treated as "skip check" / unconditionally run
|
if _, err := os.Stat(extDir); err != nil {
|
||||||
if checkCmd == nil && checkFlatpaks == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if checkCmd != nil && checker.AnyCommandExists(checkCmd...) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if checkFlatpaks != nil && checker.AnyFlatpakExists(checkFlatpaks...) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
|
|
||||||
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err != nil || len(matches) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
extDir := matches[0]
|
|
||||||
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
||||||
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
||||||
input_path = '%s/vscode-color-theme-default.json'
|
input_path = '%s/vscode-color-theme-default.json'
|
||||||
@@ -468,12 +420,8 @@ output_path = '%s/themes/dankshell-light.json'
|
|||||||
log.Infof("Added %s theme config (extension found at %s)", name, extDir)
|
log.Infof("Added %s theme config (extension found at %s)", name, extDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func substituteVars(content, shellDir string) string {
|
func substituteShellDir(content, shellDir string) string {
|
||||||
result := strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
|
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
|
||||||
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
|
||||||
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
|
||||||
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractTOMLSection(content, startMarker, endMarker string) string {
|
func extractTOMLSection(content, startMarker, endMarker string) string {
|
||||||
@@ -605,19 +553,19 @@ func extractNestedColor(jsonStr, colorName, variant string) string {
|
|||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDank16Variants(primaryDark, primaryLight, surface string, mode ColorMode) string {
|
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
|
||||||
variantOpts := dank16.VariantOptions{
|
variantOpts := dank16.VariantOptions{
|
||||||
PrimaryDark: primaryDark,
|
PrimaryDark: primaryDark,
|
||||||
PrimaryLight: primaryLight,
|
PrimaryLight: primaryLight,
|
||||||
Background: surface,
|
Background: surface,
|
||||||
UseDPS: true,
|
UseDPS: true,
|
||||||
IsLightMode: mode == ColorModeLight,
|
IsLightMode: mode == "light",
|
||||||
}
|
}
|
||||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
return dank16.GenerateVariantJSON(variantColors)
|
return dank16.GenerateVariantJSON(variantColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshGTK(configDir string, mode ColorMode) {
|
func refreshGTK(configDir, mode string) {
|
||||||
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||||
|
|
||||||
info, err := os.Lstat(gtkCSS)
|
info, err := os.Lstat(gtkCSS)
|
||||||
@@ -643,7 +591,7 @@ func refreshGTK(configDir string, mode ColorMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
||||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalTerminals() {
|
func signalTerminals() {
|
||||||
@@ -673,9 +621,9 @@ func signalByName(name string, sig syscall.Signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncColorScheme(mode ColorMode) {
|
func syncColorScheme(mode string) {
|
||||||
scheme := "prefer-dark"
|
scheme := "prefer-dark"
|
||||||
if mode == ColorModeLight {
|
if mode == "light" {
|
||||||
scheme = "default"
|
scheme = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,52 +631,3 @@ func syncColorScheme(mode ColorMode) {
|
|||||||
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateCheck struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Detected bool `json:"detected"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
|
||||||
if checker == nil {
|
|
||||||
checker = utils.DefaultAppChecker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
|
||||||
checks := make([]TemplateCheck, 0, len(templateRegistry))
|
|
||||||
|
|
||||||
for _, tmpl := range templateRegistry {
|
|
||||||
detected := false
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tmpl.RunUnconditionally:
|
|
||||||
detected = true
|
|
||||||
case tmpl.Kind == TemplateKindVSCode:
|
|
||||||
detected = checkVSCodeExtension(homeDir)
|
|
||||||
default:
|
|
||||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
|
||||||
}
|
|
||||||
|
|
||||||
checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected})
|
|
||||||
}
|
|
||||||
|
|
||||||
return checks
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVSCodeExtension(homeDir string) bool {
|
|
||||||
extDirs := []string{
|
|
||||||
filepath.Join(homeDir, ".vscode/extensions"),
|
|
||||||
filepath.Join(homeDir, ".vscode-oss/extensions"),
|
|
||||||
filepath.Join(homeDir, ".config/Code - OSS/extensions"),
|
|
||||||
filepath.Join(homeDir, ".cursor/extensions"),
|
|
||||||
filepath.Join(homeDir, ".windsurf/extensions"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, extDir := range extDirs {
|
|
||||||
pattern := filepath.Join(extDir, "danklinux.dms-theme-*")
|
|
||||||
if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,394 +0,0 @@
|
|||||||
package matugen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAppendConfigBinaryExists(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "test config content"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
|
||||||
mockChecker.EXPECT().AnyCommandExists("sh").Return(true)
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) == 0 {
|
|
||||||
t.Errorf("expected config to be written when binary exists")
|
|
||||||
}
|
|
||||||
if string(output) != testConfig+"\n" {
|
|
||||||
t.Errorf("expected %q, got %q", testConfig+"\n", string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "test config content"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
|
||||||
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
|
||||||
mockChecker.EXPECT().AnyFlatpakExists().Return(false)
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) != 0 {
|
|
||||||
t.Errorf("expected no config when binary doesn't exist, got: %q", string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigFlatpakExists(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "zen config content"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
|
||||||
mockChecker.EXPECT().AnyFlatpakExists("app.zen_browser.zen").Return(true)
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) == 0 {
|
|
||||||
t.Errorf("expected config to be written when flatpak exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "test config content"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
|
||||||
mockChecker.EXPECT().AnyCommandExists().Return(false)
|
|
||||||
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) != 0 {
|
|
||||||
t.Errorf("expected no config when flatpak doesn't exist, got: %q", string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigBothExist(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "zen config content"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
|
||||||
mockChecker.EXPECT().AnyCommandExists("sh").Return(true)
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) == 0 {
|
|
||||||
t.Errorf("expected config to be written when both binary and flatpak exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigNeitherExists(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "test config content"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
|
||||||
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
|
||||||
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) != 0 {
|
|
||||||
t.Errorf("expected no config when neither exists, got: %q", string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigNoChecks(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testConfig := "always include"
|
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, nil, nil, "test.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) == 0 {
|
|
||||||
t.Errorf("expected config to be written when no checks specified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendConfigFileDoesNotExist(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile := filepath.Join(tempDir, "output.toml")
|
|
||||||
cfgFile, err := os.Create(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create output file: %v", err)
|
|
||||||
}
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
opts := &Options{ShellDir: shellDir}
|
|
||||||
|
|
||||||
appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml")
|
|
||||||
|
|
||||||
cfgFile.Close()
|
|
||||||
output, err := os.ReadFile(outFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) != 0 {
|
|
||||||
t.Errorf("expected no config when file doesn't exist, got: %q", string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubstituteVars(t *testing.T) {
|
|
||||||
configDir := utils.XDGConfigHome()
|
|
||||||
dataDir := utils.XDGDataHome()
|
|
||||||
cacheDir := utils.XDGCacheHome()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
shellDir string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "substitutes SHELL_DIR",
|
|
||||||
input: "input_path = 'SHELL_DIR/matugen/templates/foo.conf'",
|
|
||||||
shellDir: "/home/user/shell",
|
|
||||||
expected: "input_path = '/home/user/shell/matugen/templates/foo.conf'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "substitutes CONFIG_DIR",
|
|
||||||
input: "output_path = 'CONFIG_DIR/kitty/theme.conf'",
|
|
||||||
shellDir: "/home/user/shell",
|
|
||||||
expected: "output_path = '" + configDir + "/kitty/theme.conf'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "substitutes DATA_DIR",
|
|
||||||
input: "output_path = 'DATA_DIR/color-schemes/theme.colors'",
|
|
||||||
shellDir: "/home/user/shell",
|
|
||||||
expected: "output_path = '" + dataDir + "/color-schemes/theme.colors'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "substitutes CACHE_DIR",
|
|
||||||
input: "output_path = 'CACHE_DIR/wal/colors.json'",
|
|
||||||
shellDir: "/home/user/shell",
|
|
||||||
expected: "output_path = '" + cacheDir + "/wal/colors.json'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "substitutes all dir types",
|
|
||||||
input: "'SHELL_DIR/a' 'CONFIG_DIR/b' 'DATA_DIR/c' 'CACHE_DIR/d'",
|
|
||||||
shellDir: "/shell",
|
|
||||||
expected: "'/shell/a' '" + configDir + "/b' '" + dataDir + "/c' '" + cacheDir + "/d'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no substitution when no placeholders",
|
|
||||||
input: "input_path = '/absolute/path/foo.conf'",
|
|
||||||
shellDir: "/home/user/shell",
|
|
||||||
expected: "input_path = '/absolute/path/foo.conf'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple SHELL_DIR occurrences",
|
|
||||||
input: "'SHELL_DIR/a' and 'SHELL_DIR/b'",
|
|
||||||
shellDir: "/shell",
|
|
||||||
expected: "'/shell/a' and '/shell/b'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "only substitutes quoted paths",
|
|
||||||
input: "SHELL_DIR/unquoted and 'SHELL_DIR/quoted'",
|
|
||||||
shellDir: "/shell",
|
|
||||||
expected: "SHELL_DIR/unquoted and '/shell/quoted'",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
result := substituteVars(tc.input, tc.shellDir)
|
|
||||||
assert.Equal(t, tc.expected, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
|
||||||
|
|
||||||
package mocks_utils
|
|
||||||
|
|
||||||
import mock "github.com/stretchr/testify/mock"
|
|
||||||
|
|
||||||
// MockAppChecker is an autogenerated mock type for the AppChecker type
|
|
||||||
type MockAppChecker struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockAppChecker_Expecter struct {
|
|
||||||
mock *mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_m *MockAppChecker) EXPECT() *MockAppChecker_Expecter {
|
|
||||||
return &MockAppChecker_Expecter{mock: &_m.Mock}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyCommandExists provides a mock function with given fields: cmds
|
|
||||||
func (_m *MockAppChecker) AnyCommandExists(cmds ...string) bool {
|
|
||||||
_va := make([]interface{}, len(cmds))
|
|
||||||
for _i := range cmds {
|
|
||||||
_va[_i] = cmds[_i]
|
|
||||||
}
|
|
||||||
var _ca []interface{}
|
|
||||||
_ca = append(_ca, _va...)
|
|
||||||
ret := _m.Called(_ca...)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for AnyCommandExists")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 bool
|
|
||||||
if rf, ok := ret.Get(0).(func(...string) bool); ok {
|
|
||||||
r0 = rf(cmds...)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockAppChecker_AnyCommandExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyCommandExists'
|
|
||||||
type MockAppChecker_AnyCommandExists_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyCommandExists is a helper method to define mock.On call
|
|
||||||
// - cmds ...string
|
|
||||||
func (_e *MockAppChecker_Expecter) AnyCommandExists(cmds ...interface{}) *MockAppChecker_AnyCommandExists_Call {
|
|
||||||
return &MockAppChecker_AnyCommandExists_Call{Call: _e.mock.On("AnyCommandExists",
|
|
||||||
append([]interface{}{}, cmds...)...)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_AnyCommandExists_Call) Run(run func(cmds ...string)) *MockAppChecker_AnyCommandExists_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
variadicArgs := make([]string, len(args)-0)
|
|
||||||
for i, a := range args[0:] {
|
|
||||||
if a != nil {
|
|
||||||
variadicArgs[i] = a.(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run(variadicArgs...)
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_AnyCommandExists_Call) Return(_a0 bool) *MockAppChecker_AnyCommandExists_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_AnyCommandExists_Call) RunAndReturn(run func(...string) bool) *MockAppChecker_AnyCommandExists_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyFlatpakExists provides a mock function with given fields: flatpaks
|
|
||||||
func (_m *MockAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
|
||||||
_va := make([]interface{}, len(flatpaks))
|
|
||||||
for _i := range flatpaks {
|
|
||||||
_va[_i] = flatpaks[_i]
|
|
||||||
}
|
|
||||||
var _ca []interface{}
|
|
||||||
_ca = append(_ca, _va...)
|
|
||||||
ret := _m.Called(_ca...)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for AnyFlatpakExists")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 bool
|
|
||||||
if rf, ok := ret.Get(0).(func(...string) bool); ok {
|
|
||||||
r0 = rf(flatpaks...)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockAppChecker_AnyFlatpakExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyFlatpakExists'
|
|
||||||
type MockAppChecker_AnyFlatpakExists_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnyFlatpakExists is a helper method to define mock.On call
|
|
||||||
// - flatpaks ...string
|
|
||||||
func (_e *MockAppChecker_Expecter) AnyFlatpakExists(flatpaks ...interface{}) *MockAppChecker_AnyFlatpakExists_Call {
|
|
||||||
return &MockAppChecker_AnyFlatpakExists_Call{Call: _e.mock.On("AnyFlatpakExists",
|
|
||||||
append([]interface{}{}, flatpaks...)...)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_AnyFlatpakExists_Call) Run(run func(flatpaks ...string)) *MockAppChecker_AnyFlatpakExists_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
variadicArgs := make([]string, len(args)-0)
|
|
||||||
for i, a := range args[0:] {
|
|
||||||
if a != nil {
|
|
||||||
variadicArgs[i] = a.(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run(variadicArgs...)
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_AnyFlatpakExists_Call) Return(_a0 bool) *MockAppChecker_AnyFlatpakExists_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_AnyFlatpakExists_Call) RunAndReturn(run func(...string) bool) *MockAppChecker_AnyFlatpakExists_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandExists provides a mock function with given fields: cmd
|
|
||||||
func (_m *MockAppChecker) CommandExists(cmd string) bool {
|
|
||||||
ret := _m.Called(cmd)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for CommandExists")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 bool
|
|
||||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
|
||||||
r0 = rf(cmd)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockAppChecker_CommandExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommandExists'
|
|
||||||
type MockAppChecker_CommandExists_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandExists is a helper method to define mock.On call
|
|
||||||
// - cmd string
|
|
||||||
func (_e *MockAppChecker_Expecter) CommandExists(cmd interface{}) *MockAppChecker_CommandExists_Call {
|
|
||||||
return &MockAppChecker_CommandExists_Call{Call: _e.mock.On("CommandExists", cmd)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_CommandExists_Call) Run(run func(cmd string)) *MockAppChecker_CommandExists_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_CommandExists_Call) Return(_a0 bool) *MockAppChecker_CommandExists_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_CommandExists_Call) RunAndReturn(run func(string) bool) *MockAppChecker_CommandExists_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlatpakExists provides a mock function with given fields: name
|
|
||||||
func (_m *MockAppChecker) FlatpakExists(name string) bool {
|
|
||||||
ret := _m.Called(name)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for FlatpakExists")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 bool
|
|
||||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
|
||||||
r0 = rf(name)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockAppChecker_FlatpakExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FlatpakExists'
|
|
||||||
type MockAppChecker_FlatpakExists_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlatpakExists is a helper method to define mock.On call
|
|
||||||
// - name string
|
|
||||||
func (_e *MockAppChecker_Expecter) FlatpakExists(name interface{}) *MockAppChecker_FlatpakExists_Call {
|
|
||||||
return &MockAppChecker_FlatpakExists_Call{Call: _e.mock.On("FlatpakExists", name)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_FlatpakExists_Call) Run(run func(name string)) *MockAppChecker_FlatpakExists_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_FlatpakExists_Call) Return(_a0 bool) *MockAppChecker_FlatpakExists_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockAppChecker_FlatpakExists_Call) RunAndReturn(run func(string) bool) *MockAppChecker_FlatpakExists_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockAppChecker creates a new instance of MockAppChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
|
||||||
// The first argument is typically a *testing.T value.
|
|
||||||
func NewMockAppChecker(t interface {
|
|
||||||
mock.TestingT
|
|
||||||
Cleanup(func())
|
|
||||||
}) *MockAppChecker {
|
|
||||||
mock := &MockAppChecker{}
|
|
||||||
mock.Mock.Test(t)
|
|
||||||
|
|
||||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -116,12 +116,12 @@ func (m *Manager) Install(plugin Plugin) error {
|
|||||||
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.fs.MkdirAll(m.pluginsDir, 0o755); err != nil {
|
if err := m.fs.MkdirAll(m.pluginsDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
return fmt.Errorf("failed to create plugins directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||||
if err := m.fs.MkdirAll(reposDir, 0o755); err != nil {
|
if err := m.fs.MkdirAll(reposDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create repos directory: %w", err)
|
return fmt.Errorf("failed to create repos directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ func (m *Manager) Install(plugin Plugin) error {
|
|||||||
|
|
||||||
metaPath := pluginPath + ".meta"
|
metaPath := pluginPath + ".meta"
|
||||||
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
||||||
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0o644); err != nil {
|
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write metadata: %w", err)
|
return fmt.Errorf("failed to write metadata: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func TestIsInstalled(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0o755)
|
err := fs.MkdirAll(pluginPath, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := manager.IsInstalled(plugin)
|
installed, err := manager.IsInstalled(plugin)
|
||||||
@@ -100,7 +100,7 @@ func TestInstall(t *testing.T) {
|
|||||||
cloneCalled = true
|
cloneCalled = true
|
||||||
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
||||||
assert.Equal(t, plugin.Repo, url)
|
assert.Equal(t, plugin.Repo, url)
|
||||||
return fs.MkdirAll(path, 0o755)
|
return fs.MkdirAll(path, 0755)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
manager.gitClient = mockGit
|
manager.gitClient = mockGit
|
||||||
@@ -118,7 +118,7 @@ func TestInstall(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0o755)
|
err := fs.MkdirAll(pluginPath, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = manager.Install(plugin)
|
err = manager.Install(plugin)
|
||||||
@@ -137,7 +137,7 @@ func TestManagerUpdate(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0o755)
|
err := fs.MkdirAll(pluginPath, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pullCalled := false
|
pullCalled := false
|
||||||
@@ -171,7 +171,7 @@ func TestUninstall(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0o755)
|
err := fs.MkdirAll(pluginPath, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = manager.Uninstall(plugin)
|
err = manager.Uninstall(plugin)
|
||||||
@@ -195,14 +195,14 @@ func TestListInstalled(t *testing.T) {
|
|||||||
t.Run("lists installed plugins", func(t *testing.T) {
|
t.Run("lists installed plugins", func(t *testing.T) {
|
||||||
manager, fs, pluginsDir := setupTestManager(t)
|
manager, fs, pluginsDir := setupTestManager(t)
|
||||||
|
|
||||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0o755)
|
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := manager.ListInstalled()
|
installed, err := manager.ListInstalled()
|
||||||
@@ -223,15 +223,15 @@ func TestListInstalled(t *testing.T) {
|
|||||||
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
||||||
manager, fs, pluginsDir := setupTestManager(t)
|
manager, fs, pluginsDir := setupTestManager(t)
|
||||||
|
|
||||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
err := fs.MkdirAll(pluginsDir, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0o755)
|
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := manager.ListInstalled()
|
installed, err := manager.ListInstalled()
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ 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 {
|
||||||
@@ -147,7 +146,7 @@ func (r *Registry) Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +161,7 @@ func (r *Registry) Update() error {
|
|||||||
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) {
|
|||||||
|
|
||||||
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
||||||
pluginsDir := filepath.Join(dir, "plugins")
|
pluginsDir := filepath.Join(dir, "plugins")
|
||||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
err := fs.MkdirAll(pluginsDir, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := json.Marshal(plugin)
|
data, err := json.Marshal(plugin)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,10 +118,10 @@ func TestLoadPlugins(t *testing.T) {
|
|||||||
registry, fs, tmpDir := setupTestRegistry(t)
|
registry, fs, tmpDir := setupTestRegistry(t)
|
||||||
|
|
||||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
err := fs.MkdirAll(pluginsDir, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
@@ -146,7 +146,7 @@ func TestLoadPlugins(t *testing.T) {
|
|||||||
registry, fs, tmpDir := setupTestRegistry(t)
|
registry, fs, tmpDir := setupTestRegistry(t)
|
||||||
|
|
||||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0o755)
|
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
@@ -170,10 +170,10 @@ func TestLoadPlugins(t *testing.T) {
|
|||||||
registry, fs, tmpDir := setupTestRegistry(t)
|
registry, fs, tmpDir := setupTestRegistry(t)
|
||||||
|
|
||||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
err := fs.MkdirAll(pluginsDir, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0o644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
@@ -303,7 +303,7 @@ func TestUpdate(t *testing.T) {
|
|||||||
Distro: []string{"any"},
|
Distro: []string{"any"},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := fs.MkdirAll(tmpDir, 0o755)
|
err := fs.MkdirAll(tmpDir, 0755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pullCalled := false
|
pullCalled := false
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func GetOutputDir() string {
|
|||||||
|
|
||||||
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
||||||
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
||||||
if err := os.MkdirAll(screenshotDir, 0o755); err == nil {
|
if err := os.MkdirAll(screenshotDir, 0755); err == nil {
|
||||||
return screenshotDir
|
return screenshotDir
|
||||||
}
|
}
|
||||||
return xdgPics
|
return xdgPics
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (r *RegionSelector) setupKeyboardHandlers() {
|
|||||||
for _, os := range r.surfaces {
|
for _, os := range r.surfaces {
|
||||||
r.redrawSurface(os)
|
r.redrawSurface(os)
|
||||||
}
|
}
|
||||||
case 28, 57, 96:
|
case 28, 57:
|
||||||
if r.selection.hasSelection {
|
if r.selection.hasSelection {
|
||||||
r.finishSelection()
|
r.finishSelection()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func LoadState() (*PersistentState, error) {
|
|||||||
func SaveState(state *PersistentState) error {
|
func SaveState(state *PersistentState) error {
|
||||||
path := getStateFilePath()
|
path := getStateFilePath()
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ func SaveState(state *PersistentState) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, 0o644)
|
return os.WriteFile(path, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLastRegion() Region {
|
func GetLastRegion() Region {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
|
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
|
||||||
|
|
||||||
target, ok := models.Get[string](req, "target")
|
target, ok := req.Params["target"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
target, ok = models.Get[string](req, "url")
|
target, ok = req.Params["url"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warnf("AppPicker: Invalid target parameter in request")
|
log.Warnf("AppPicker: Invalid target parameter in request")
|
||||||
models.RespondError(conn, req.ID, "invalid target parameter")
|
models.RespondError(conn, req.ID, "invalid target parameter")
|
||||||
@@ -31,11 +31,14 @@ func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
|
|
||||||
event := OpenEvent{
|
event := OpenEvent{
|
||||||
Target: target,
|
Target: target,
|
||||||
RequestType: models.GetOr(req, "requestType", "url"),
|
RequestType: "url",
|
||||||
MimeType: models.GetOr(req, "mimeType", ""),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if categories, ok := models.Get[[]any](req, "categories"); ok {
|
if mimeType, ok := req.Params["mimeType"].(string); ok {
|
||||||
|
event.MimeType = mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
if categories, ok := req.Params["categories"].([]any); ok {
|
||||||
event.Categories = make([]string, 0, len(categories))
|
event.Categories = make([]string, 0, len(categories))
|
||||||
for _, cat := range categories {
|
for _, cat := range categories {
|
||||||
if catStr, ok := cat.(string); ok {
|
if catStr, ok := cat.(string); ok {
|
||||||
@@ -44,6 +47,10 @@ func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if requestType, ok := req.Params["requestType"].(string); ok {
|
||||||
|
event.RequestType = requestType
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("AppPicker: Broadcasting event: %+v", event)
|
log.Infof("AppPicker: Broadcasting event: %+v", event)
|
||||||
manager.RequestOpen(event)
|
manager.RequestOpen(event)
|
||||||
models.Respond(conn, req.ID, "ok")
|
models.Respond(conn, req.ID, "ok")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,15 +110,17 @@ 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 = dbusutil.AsOr(poweredVar, false)
|
m.state.Powered = powered
|
||||||
m.state.Discovering = dbusutil.AsOr(discoveringVar, false)
|
m.state.Discovering = discovering
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -168,20 +169,65 @@ 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 {
|
||||||
return Device{
|
dev := Device{Path: path}
|
||||||
Path: path,
|
|
||||||
Address: dbusutil.GetOr(props, "Address", ""),
|
if v, ok := props["Address"]; ok {
|
||||||
Name: dbusutil.GetOr(props, "Name", ""),
|
if addr, ok := v.Value().(string); ok {
|
||||||
Alias: dbusutil.GetOr(props, "Alias", ""),
|
dev.Address = addr
|
||||||
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 {
|
||||||
@@ -282,13 +328,17 @@ func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant
|
|||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
dirty := false
|
dirty := false
|
||||||
|
|
||||||
if powered, ok := dbusutil.Get[bool](changed, "Powered"); ok {
|
if v, ok := changed["Powered"]; ok {
|
||||||
m.state.Powered = powered
|
if powered, ok := v.Value().(bool); ok {
|
||||||
dirty = true
|
m.state.Powered = powered
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if discovering, ok := dbusutil.Get[bool](changed, "Discovering"); ok {
|
if v, ok := changed["Discovering"]; ok {
|
||||||
m.state.Discovering = discovering
|
if discovering, ok := v.Value().(bool); ok {
|
||||||
dirty = true
|
m.state.Discovering = discovering
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
@@ -299,28 +349,31 @@ 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) {
|
||||||
paired, hasPaired := dbusutil.Get[bool](changed, "Paired")
|
pairedVar, hasPaired := 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 {
|
if paired, ok := pairedVar.Value().(bool); ok {
|
||||||
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
if paired {
|
||||||
if wasPending {
|
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
|
||||||
select {
|
|
||||||
case m.eventQueue <- func() {
|
if wasPending {
|
||||||
time.Sleep(300 * time.Millisecond)
|
select {
|
||||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
case m.eventQueue <- func() {
|
||||||
if err := m.ConnectDevice(devicePath); err != nil {
|
time.Sleep(300 * time.Millisecond)
|
||||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func (b *SysfsBackend) SetBrightnessWithExponent(id string, percent int, exponen
|
|||||||
brightnessPath := filepath.Join(devicePath, "brightness")
|
brightnessPath := filepath.Join(devicePath, "brightness")
|
||||||
|
|
||||||
data := []byte(fmt.Sprintf("%d", value))
|
data := []byte(fmt.Sprintf("%d", value))
|
||||||
if err := os.WriteFile(brightnessPath, data, 0o644); err != nil {
|
if err := os.WriteFile(brightnessPath, data, 0644); err != nil {
|
||||||
return fmt.Errorf("write brightness: %w", err)
|
return fmt.Errorf("write brightness: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,13 +158,13 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,13 +215,13 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
||||||
if err := os.MkdirAll(ledsDir, 0o755); err != nil {
|
if err := os.MkdirAll(ledsDir, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user