mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-27 23:12:49 -05:00
Compare commits
32 Commits
hotfix-1.2
...
hotfix-1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1926db95de | ||
|
|
5ad2a9d704 | ||
|
|
e0ab20dbda | ||
|
|
aadc3111a2 | ||
|
|
741d492084 | ||
|
|
604d55015c | ||
|
|
a4ce39caa5 | ||
|
|
0a82c9877d | ||
|
|
56f5c5eccb | ||
|
|
d20b5adbfa | ||
|
|
10dc86a5dc | ||
|
|
5463aed213 | ||
|
|
f435f0d413 | ||
|
|
521d804763 | ||
|
|
e203ec960a | ||
|
|
830ca10b45 | ||
|
|
4ffa06945a | ||
|
|
b1406fc49a | ||
|
|
f8179167a8 | ||
|
|
32998a5219 | ||
|
|
7fb358bada | ||
|
|
73cf3130e1 | ||
|
|
119b5df6df | ||
|
|
8ede810d32 | ||
|
|
830dd93af5 | ||
|
|
75f28c5ea7 | ||
|
|
6c9b8c590e | ||
|
|
24d9b77307 | ||
|
|
d4be68912c | ||
|
|
a443721000 | ||
|
|
786b097187 | ||
|
|
8ca60c7d2a |
@@ -1,8 +0,0 @@
|
|||||||
[*.sh]
|
|
||||||
# like -i=4
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
[*.nix]
|
|
||||||
# like -i=4
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
69
.githooks/pre-commit
Executable file
69
.githooks/pre-commit
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Go CI checks (when core/ files are staged)
|
||||||
|
# =============================================================================
|
||||||
|
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
||||||
|
|
||||||
|
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||||
|
echo "Go files staged in core/, running CI checks..."
|
||||||
|
cd "$REPO_ROOT/core"
|
||||||
|
|
||||||
|
# Format check
|
||||||
|
echo " Checking gofmt..."
|
||||||
|
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
||||||
|
if [[ -n "$UNFORMATTED" ]]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
echo "$UNFORMATTED"
|
||||||
|
echo ""
|
||||||
|
echo "Run: cd core && gofmt -s -w ."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# golangci-lint
|
||||||
|
if command -v golangci-lint &>/dev/null; then
|
||||||
|
echo " Running golangci-lint..."
|
||||||
|
golangci-lint run ./...
|
||||||
|
else
|
||||||
|
echo " Warning: golangci-lint not installed, skipping lint"
|
||||||
|
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
echo " Running tests..."
|
||||||
|
if ! go test ./... >/dev/null 2>&1; then
|
||||||
|
echo "Tests failed! Run 'go test ./...' for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build checks
|
||||||
|
echo " Building..."
|
||||||
|
mkdir -p bin
|
||||||
|
go build -buildvcs=false -o bin/dms ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
||||||
|
|
||||||
|
echo "All Go CI checks passed!"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# i18n sync check (DISABLED for now)
|
||||||
|
# =============================================================================
|
||||||
|
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||||
|
# if command -v python3 &>/dev/null; then
|
||||||
|
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||||
|
# echo "Translations out of sync"
|
||||||
|
# echo "Run: python3 scripts/i18nsync.py sync"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
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: input
|
|
||||||
id: dms_version
|
|
||||||
attributes:
|
|
||||||
label: dms version
|
|
||||||
description: Output of dms version command
|
|
||||||
placeholder: e.g., 1.2.3
|
|
||||||
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
|
|
||||||
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: input
|
|
||||||
id: dms_version
|
|
||||||
attributes:
|
|
||||||
label: dms version
|
|
||||||
description: Output of dms version command
|
|
||||||
placeholder: e.g., 1.2.3
|
|
||||||
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
|
|
||||||
383
.github/workflows/backup/run-obs.yml.bak
vendored
383
.github/workflows/backup/run-obs.yml.bak
vendored
@@ -1,383 +0,0 @@
|
|||||||
name: Update OBS Packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package:
|
|
||||||
description: "Package to update (dms, dms-git, or all)"
|
|
||||||
required: false
|
|
||||||
default: "all"
|
|
||||||
force_upload:
|
|
||||||
description: "Force upload without version check"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "false"
|
|
||||||
- "true"
|
|
||||||
rebuild_release:
|
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
schedule:
|
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-updates:
|
|
||||||
name: Check for updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
|
||||||
packages: ${{ steps.check.outputs.packages }}
|
|
||||||
version: ${{ steps.check.outputs.version }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install OSC
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y osc
|
|
||||||
|
|
||||||
mkdir -p ~/.config/osc
|
|
||||||
cat > ~/.config/osc/oscrc << EOF
|
|
||||||
[general]
|
|
||||||
apiurl = https://api.opensuse.org
|
|
||||||
|
|
||||||
[https://api.opensuse.org]
|
|
||||||
user = ${{ secrets.OBS_USERNAME }}
|
|
||||||
pass = ${{ secrets.OBS_PASSWORD }}
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/osc/oscrc
|
|
||||||
|
|
||||||
- name: Check for updates
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by tag: $VERSION (always update)"
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
|
||||||
echo "Checking if dms-git source has changed..."
|
|
||||||
|
|
||||||
# Get current commit hash (8 chars to match spec format)
|
|
||||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
|
||||||
|
|
||||||
# Check OBS for last uploaded commit
|
|
||||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
|
||||||
mkdir -p "$OBS_BASE"
|
|
||||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
|
||||||
|
|
||||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
|
||||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
|
||||||
osc up -q 2>/dev/null || true
|
|
||||||
|
|
||||||
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
|
||||||
if [[ -f "dms-git.spec" ]]; then
|
|
||||||
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
|
||||||
|
|
||||||
if [[ -n "$OBS_COMMIT" ]]; then
|
|
||||||
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Could not extract OBS commit, proceeding with update"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No spec file in OBS, proceeding with update"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${{ github.workspace }}"
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 First upload to OBS, update needed"
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
|
||||||
PKG="${{ github.event.inputs.package }}"
|
|
||||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: all packages"
|
|
||||||
else
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: $PKG"
|
|
||||||
fi
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
else
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
update-obs:
|
|
||||||
name: Upload to OBS
|
|
||||||
needs: check-updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: |
|
|
||||||
github.event.inputs.force_upload == 'true' ||
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Generate GitHub App Token
|
|
||||||
id: generate_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:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Check if last commit was automated
|
|
||||||
id: check-loop
|
|
||||||
run: |
|
|
||||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
|
||||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
|
||||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "✅ Last commit was not automated, proceeding"
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Determine packages to update
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: packages
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by tag: $VERSION"
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by schedule: updating git package"
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
else
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
|
||||||
run: |
|
|
||||||
# Get commit info for dms-git versioning
|
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
|
||||||
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
|
||||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
|
||||||
|
|
||||||
# Update version in spec
|
|
||||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
|
||||||
|
|
||||||
# Add changelog entry
|
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
|
||||||
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
|
||||||
|
|
||||||
- name: Update Debian dms-git changelog version
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
|
||||||
run: |
|
|
||||||
# Get commit info for dms-git versioning
|
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
|
||||||
|
|
||||||
# Debian version format: 0.6.2+git2256.9162e314
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
|
||||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
|
||||||
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
|
|
||||||
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
|
||||||
|
|
||||||
# Get current version from changelog
|
|
||||||
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
|
||||||
|
|
||||||
echo "Current Debian version: $CURRENT_VERSION"
|
|
||||||
echo "New version: $NEW_VERSION"
|
|
||||||
|
|
||||||
# Only update if version changed
|
|
||||||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
|
||||||
# Create new changelog entry at top
|
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$TEMP_CHANGELOG" << EOF
|
|
||||||
dms-git ($NEW_VERSION) nightly; urgency=medium
|
|
||||||
|
|
||||||
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Prepend to existing changelog
|
|
||||||
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
|
||||||
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
|
||||||
|
|
||||||
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
|
||||||
else
|
|
||||||
echo "✓ Debian changelog already at version $NEW_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update dms stable version
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
|
||||||
VERSION_NO_V="${VERSION#v}"
|
|
||||||
echo "Updating packaging to version $VERSION_NO_V"
|
|
||||||
|
|
||||||
# Update openSUSE dms spec (stable only)
|
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
# Update openSUSE spec changelog
|
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
|
||||||
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
|
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
# Update Debian _service files (both tar_scm and download_url formats)
|
|
||||||
for service in distro/debian/*/_service; do
|
|
||||||
if [[ -f "$service" ]]; then
|
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
|
||||||
|
|
||||||
# Update download_url paths (for dms stable)
|
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Update Debian changelog for dms stable
|
|
||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$TEMP_CHANGELOG" << EOF
|
|
||||||
dms ($VERSION_NO_V) stable; urgency=medium
|
|
||||||
|
|
||||||
* Update to $VERSION stable release
|
|
||||||
* Bug fixes and improvements
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
|
|
||||||
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
|
|
||||||
|
|
||||||
echo "✓ Updated Debian changelog to $VERSION_NO_V"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.24"
|
|
||||||
|
|
||||||
- name: Install OSC
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y osc
|
|
||||||
|
|
||||||
mkdir -p ~/.config/osc
|
|
||||||
cat > ~/.config/osc/oscrc << EOF
|
|
||||||
[general]
|
|
||||||
apiurl = https://api.opensuse.org
|
|
||||||
|
|
||||||
[https://api.opensuse.org]
|
|
||||||
user = ${{ secrets.OBS_USERNAME }}
|
|
||||||
pass = ${{ secrets.OBS_PASSWORD }}
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/osc/oscrc
|
|
||||||
|
|
||||||
- name: Upload to OBS
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
|
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
|
||||||
run: |
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
MESSAGE="Automated update from GitHub Actions"
|
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
|
||||||
else
|
|
||||||
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Get changed packages
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: changed-packages
|
|
||||||
run: |
|
|
||||||
# Check if there are any changes to commit
|
|
||||||
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog or spec changes to commit"
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
# Get list of changed packages for commit message
|
|
||||||
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
|
|
||||||
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
|
|
||||||
|
|
||||||
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "packages=$PKGS" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Changed packages: $PKGS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit packaging changes
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "dms-ci[bot]"
|
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
|
||||||
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
|
|
||||||
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
|
||||||
git pull --rebase origin master
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
|
||||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
|
||||||
298
.github/workflows/backup/run-ppa.yml.bak
vendored
298
.github/workflows/backup/run-ppa.yml.bak
vendored
@@ -1,298 +0,0 @@
|
|||||||
name: Update PPA Packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package:
|
|
||||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
|
||||||
required: false
|
|
||||||
default: "dms-git"
|
|
||||||
force_upload:
|
|
||||||
description: "Force upload without version check"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "false"
|
|
||||||
- "true"
|
|
||||||
rebuild_release:
|
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
schedule:
|
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-updates:
|
|
||||||
name: Check for updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
|
||||||
packages: ${{ steps.check.outputs.packages }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Check for updates
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
|
||||||
echo "Checking if dms-git source has changed..."
|
|
||||||
|
|
||||||
# Get current commit hash (8 chars to match changelog format)
|
|
||||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
|
||||||
|
|
||||||
# Extract commit hash from changelog
|
|
||||||
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
|
||||||
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
|
||||||
|
|
||||||
if [[ -f "$CHANGELOG_FILE" ]]; then
|
|
||||||
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
|
||||||
|
|
||||||
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
|
||||||
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog file found, proceeding with upload"
|
|
||||||
fi
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
else
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
upload-ppa:
|
|
||||||
name: Upload to PPA
|
|
||||||
needs: check-updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: |
|
|
||||||
github.event.inputs.force_upload == 'true' ||
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Generate GitHub App Token
|
|
||||||
id: generate_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:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Check if last commit was automated
|
|
||||||
id: check-loop
|
|
||||||
run: |
|
|
||||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
|
||||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
|
||||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "✅ Last commit was not automated, proceeding"
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.24"
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
debhelper \
|
|
||||||
devscripts \
|
|
||||||
dput \
|
|
||||||
lftp \
|
|
||||||
build-essential \
|
|
||||||
fakeroot \
|
|
||||||
dpkg-dev
|
|
||||||
|
|
||||||
- name: Configure GPG
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
||||||
run: |
|
|
||||||
echo "$GPG_KEY" | gpg --import
|
|
||||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
|
||||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Determine packages to upload
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: packages
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
|
||||||
PKG="${{ github.event.inputs.package }}"
|
|
||||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: all packages"
|
|
||||||
else
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: $PKG"
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by schedule: uploading git package"
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
# Manual package selection should respect change detection
|
|
||||||
SELECTED_PKG="${{ github.event.inputs.package }}"
|
|
||||||
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
|
|
||||||
|
|
||||||
# Check if manually selected package is in the updated list
|
|
||||||
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
|
|
||||||
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "📦 Manual selection (has updates): $SELECTED_PKG"
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload to PPA
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
|
||||||
echo "No packages selected for upload. Skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build command arguments
|
|
||||||
BUILD_ARGS=()
|
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
|
||||||
BUILD_ARGS+=("$REBUILD_RELEASE")
|
|
||||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms to PPA..."
|
|
||||||
if [ -n "$REBUILD_RELEASE" ]; then
|
|
||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-git to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-greeter to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
|
|
||||||
else
|
|
||||||
# Map package to PPA name
|
|
||||||
case "$PACKAGES" in
|
|
||||||
dms)
|
|
||||||
PPA_NAME="dms"
|
|
||||||
;;
|
|
||||||
dms-git)
|
|
||||||
PPA_NAME="dms-git"
|
|
||||||
;;
|
|
||||||
dms-greeter)
|
|
||||||
PPA_NAME="danklinux"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
PPA_NAME="$PACKAGES"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading $PACKAGES to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Get changed packages
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: changed-packages
|
|
||||||
run: |
|
|
||||||
# Check if there are any changelog changes to commit
|
|
||||||
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog changes to commit"
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
# Get list of changed packages for commit message (deduplicate)
|
|
||||||
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Changed packages: $CHANGED"
|
|
||||||
echo "📋 Debug - Changed files:"
|
|
||||||
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit changelog changes
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "dms-ci[bot]"
|
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
|
||||||
git add distro/ubuntu/*/debian/changelog
|
|
||||||
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
|
||||||
git pull --rebase origin master
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
|
||||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [[ "$PACKAGES" == "dms" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
|
||||||
23
.github/workflows/go-ci.yml
vendored
23
.github/workflows/go-ci.yml
vendored
@@ -28,20 +28,25 @@ 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:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.6
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
|
|||||||
24
.github/workflows/prek.yml
vendored
24
.github/workflows/prek.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: Pre-commit Checks
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
branches: [master, main]
|
|
||||||
jobs:
|
|
||||||
pre-commit-check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
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: run pre-commit hooks
|
|
||||||
uses: j178/prek-action@v1
|
|
||||||
297
.github/workflows/release.yml
vendored
297
.github/workflows/release.yml
vendored
@@ -281,9 +281,6 @@ jobs:
|
|||||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
cp LICENSE CONTRIBUTING.md quickshell/
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
|
|
||||||
# Copy root assets directory to quickshell for systemd service and desktop file
|
|
||||||
cp -r assets quickshell/
|
|
||||||
|
|
||||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
(cd quickshell && tar --exclude='.git' \
|
(cd quickshell && tar --exclude='.git' \
|
||||||
--exclude='.github' \
|
--exclude='.github' \
|
||||||
@@ -398,3 +395,297 @@ jobs:
|
|||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# trigger-obs-update:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: release
|
||||||
|
# env:
|
||||||
|
# TAG: ${{ inputs.tag }}
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
# - name: Install OSC
|
||||||
|
# run: |
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y osc
|
||||||
|
|
||||||
|
# mkdir -p ~/.config/osc
|
||||||
|
# cat > ~/.config/osc/oscrc << EOF
|
||||||
|
# [general]
|
||||||
|
# apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
# [https://api.opensuse.org]
|
||||||
|
# user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
# pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
# EOF
|
||||||
|
# chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
# - name: Update OBS packages
|
||||||
|
# run: |
|
||||||
|
# cd distro
|
||||||
|
# bash scripts/obs-upload.sh dms "Update to ${TAG}"
|
||||||
|
|
||||||
|
# trigger-ppa-update:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: release
|
||||||
|
# env:
|
||||||
|
# TAG: ${{ inputs.tag }}
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
# - name: Install build dependencies
|
||||||
|
# run: |
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y \
|
||||||
|
# debhelper \
|
||||||
|
# devscripts \
|
||||||
|
# dput \
|
||||||
|
# lftp \
|
||||||
|
# build-essential \
|
||||||
|
# fakeroot \
|
||||||
|
# dpkg-dev
|
||||||
|
|
||||||
|
# - name: Configure GPG
|
||||||
|
# env:
|
||||||
|
# GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
# run: |
|
||||||
|
# echo "$GPG_KEY" | gpg --import
|
||||||
|
# GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
# echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# - name: Upload to PPA
|
||||||
|
# run: |
|
||||||
|
# cd distro/ubuntu/ppa
|
||||||
|
# bash create-and-upload.sh ../dms dms questing
|
||||||
|
|
||||||
|
# copr-build:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: release
|
||||||
|
# env:
|
||||||
|
# TAG: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout repository
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
# - name: Determine version
|
||||||
|
# id: version
|
||||||
|
# run: |
|
||||||
|
# VERSION="${TAG#v}"
|
||||||
|
# echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
# echo "Building DMS stable version: $VERSION"
|
||||||
|
|
||||||
|
# - name: Setup build environment
|
||||||
|
# run: |
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y rpm wget curl jq gzip
|
||||||
|
# mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
|
# - name: Download release assets
|
||||||
|
# run: |
|
||||||
|
# VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
# cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
|
# wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||||
|
# echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||||
|
# exit 1
|
||||||
|
# }
|
||||||
|
|
||||||
|
# - name: Generate stable spec file
|
||||||
|
# run: |
|
||||||
|
# VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
# CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
|
||||||
|
# cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
|
# # Spec for DMS stable releases - Generated by GitHub Actions
|
||||||
|
|
||||||
|
# %global debug_package %{nil}
|
||||||
|
# %global version VERSION_PLACEHOLDER
|
||||||
|
# %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
|
|
||||||
|
# Name: dms
|
||||||
|
# Version: %{version}
|
||||||
|
# Release: 1%{?dist}
|
||||||
|
# Summary: %{pkg_summary}
|
||||||
|
|
||||||
|
# License: MIT
|
||||||
|
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
# Source0: dms-qml.tar.gz
|
||||||
|
|
||||||
|
# BuildRequires: gzip
|
||||||
|
# BuildRequires: wget
|
||||||
|
# BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
|
# Requires: (quickshell or quickshell-git)
|
||||||
|
# Requires: accountsservice
|
||||||
|
# Requires: dms-cli = %{version}-%{release}
|
||||||
|
# Requires: dgop
|
||||||
|
|
||||||
|
# Recommends: cava
|
||||||
|
# Recommends: cliphist
|
||||||
|
# Recommends: danksearch
|
||||||
|
# Recommends: matugen
|
||||||
|
# Recommends: wl-clipboard
|
||||||
|
# Recommends: NetworkManager
|
||||||
|
# Recommends: qt6-qtmultimedia
|
||||||
|
# Suggests: qt6ct
|
||||||
|
|
||||||
|
# %description
|
||||||
|
# DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||||
|
# and optimized for the niri and hyprland compositors. Features notifications,
|
||||||
|
# app launcher, wallpaper customization, and fully customizable with plugins.
|
||||||
|
|
||||||
|
# Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||||
|
# process monitoring, notification center, clipboard history, dock, control center,
|
||||||
|
# lock screen, and comprehensive plugin system.
|
||||||
|
|
||||||
|
# %package -n dms-cli
|
||||||
|
# Summary: DankMaterialShell CLI tool
|
||||||
|
# License: MIT
|
||||||
|
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
# %description -n dms-cli
|
||||||
|
# Command-line interface for DankMaterialShell configuration and management.
|
||||||
|
# Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
|
# %prep
|
||||||
|
# %setup -q -c -n dms-qml
|
||||||
|
|
||||||
|
# # Download architecture-specific binaries during build
|
||||||
|
# case "%{_arch}" in
|
||||||
|
# x86_64)
|
||||||
|
# ARCH_SUFFIX="amd64"
|
||||||
|
# ;;
|
||||||
|
# aarch64)
|
||||||
|
# ARCH_SUFFIX="arm64"
|
||||||
|
# ;;
|
||||||
|
# *)
|
||||||
|
# echo "Unsupported architecture: %{_arch}"
|
||||||
|
# exit 1
|
||||||
|
# ;;
|
||||||
|
# esac
|
||||||
|
|
||||||
|
# wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
|
# echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
|
# exit 1
|
||||||
|
# }
|
||||||
|
# gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
|
# chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
|
# %build
|
||||||
|
|
||||||
|
# %install
|
||||||
|
# install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
|
||||||
|
# install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
|
# install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
|
# install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
|
# %{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||||
|
# %{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
|
# %{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
|
# install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
# install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
|
# install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
# install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
|
# cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
|
|
||||||
|
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
|
# rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
|
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
|
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
# echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
||||||
|
|
||||||
|
# %posttrans
|
||||||
|
# if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
|
# rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
|
# rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
|
# rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
|
# fi
|
||||||
|
# # Signal running DMS instances to reload
|
||||||
|
# pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||||
|
|
||||||
|
# %files
|
||||||
|
# %license LICENSE
|
||||||
|
# %doc README.md CONTRIBUTING.md
|
||||||
|
# %{_datadir}/quickshell/dms/
|
||||||
|
# %{_userunitdir}/dms.service
|
||||||
|
# %{_datadir}/applications/dms-open.desktop
|
||||||
|
# %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
# %files -n dms-cli
|
||||||
|
# %{_bindir}/dms
|
||||||
|
# %{_datadir}/bash-completion/completions/dms
|
||||||
|
# %{_datadir}/zsh/site-functions/_dms
|
||||||
|
# %{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
|
# %changelog
|
||||||
|
# * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
||||||
|
# - Stable release VERSION_PLACEHOLDER
|
||||||
|
# - Built from GitHub release
|
||||||
|
# SPECEOF
|
||||||
|
|
||||||
|
# sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
# sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
|
# - name: Build SRPM
|
||||||
|
# id: build
|
||||||
|
# run: |
|
||||||
|
# cd ~/rpmbuild/SPECS
|
||||||
|
# rpmbuild -bs dms.spec
|
||||||
|
|
||||||
|
# SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||||
|
# SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
|
# echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
|
# echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||||
|
# echo "SRPM built: $SRPM_NAME"
|
||||||
|
|
||||||
|
# - name: Upload SRPM artifact
|
||||||
|
# uses: actions/upload-artifact@v4
|
||||||
|
# with:
|
||||||
|
# name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
|
# path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
# retention-days: 90
|
||||||
|
|
||||||
|
# - name: Install Copr CLI
|
||||||
|
# run: |
|
||||||
|
# sudo apt-get install -y python3-pip
|
||||||
|
# pip3 install copr-cli
|
||||||
|
|
||||||
|
# mkdir -p ~/.config
|
||||||
|
# cat > ~/.config/copr << EOF
|
||||||
|
# [copr-cli]
|
||||||
|
# login = ${{ secrets.COPR_LOGIN }}
|
||||||
|
# username = avengemedia
|
||||||
|
# token = ${{ secrets.COPR_TOKEN }}
|
||||||
|
# copr_url = https://copr.fedorainfracloud.org
|
||||||
|
# EOF
|
||||||
|
# chmod 600 ~/.config/copr
|
||||||
|
|
||||||
|
# - name: Upload to Copr
|
||||||
|
# run: |
|
||||||
|
# SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
|
# VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
# echo "Uploading SRPM to avengemedia/dms..."
|
||||||
|
# BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
|
# echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
|
# BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
|
# if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
|
# echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
|
# fi
|
||||||
|
|||||||
216
.github/workflows/run-copr.yml
vendored
216
.github/workflows/run-copr.yml
vendored
@@ -3,17 +3,8 @@ name: DMS Copr Stable Release
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
|
||||||
description: 'Package to build (dms, dms-greeter, or both)'
|
|
||||||
required: false
|
|
||||||
default: 'dms'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- dms
|
|
||||||
- dms-greeter
|
|
||||||
- both
|
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
release:
|
release:
|
||||||
@@ -22,27 +13,8 @@ on:
|
|||||||
default: '1'
|
default: '1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
determine-packages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
packages: ${{ steps.set-packages.outputs.packages }}
|
|
||||||
steps:
|
|
||||||
- name: Set package list
|
|
||||||
id: set-packages
|
|
||||||
run: |
|
|
||||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
|
||||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
|
||||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-and-upload:
|
build-and-upload:
|
||||||
needs: determine-packages
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -67,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||||
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
|
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
||||||
|
|
||||||
- name: Setup build environment
|
- name: Setup build environment
|
||||||
run: |
|
run: |
|
||||||
@@ -98,31 +70,157 @@ jobs:
|
|||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
RELEASE="${{ steps.version.outputs.release }}"
|
RELEASE="${{ steps.version.outputs.release }}"
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
PACKAGE="${{ matrix.package }}"
|
|
||||||
|
|
||||||
# Copy spec file from repository
|
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
|
# Spec for DMS stable releases - Generated by GitHub Actions
|
||||||
|
|
||||||
# Replace placeholders with actual values
|
%global debug_package %{nil}
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
%global version VERSION_PLACEHOLDER
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
|
||||||
|
|
||||||
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
|
Name: dms
|
||||||
|
Version: %{version}
|
||||||
|
Release: RELEASE_PLACEHOLDER%{?dist}
|
||||||
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
Source0: dms-qml.tar.gz
|
||||||
|
|
||||||
|
BuildRequires: gzip
|
||||||
|
BuildRequires: wget
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
|
Requires: (quickshell or quickshell-git)
|
||||||
|
Requires: accountsservice
|
||||||
|
Requires: dms-cli = %{version}-%{release}
|
||||||
|
Requires: dgop
|
||||||
|
|
||||||
|
Recommends: cava
|
||||||
|
Recommends: cliphist
|
||||||
|
Recommends: danksearch
|
||||||
|
Recommends: hyprpicker
|
||||||
|
Recommends: matugen
|
||||||
|
Recommends: wl-clipboard
|
||||||
|
Recommends: NetworkManager
|
||||||
|
Recommends: qt6-qtmultimedia
|
||||||
|
Suggests: qt6ct
|
||||||
|
|
||||||
|
%description
|
||||||
|
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||||
|
and optimized for the niri and hyprland compositors. Features notifications,
|
||||||
|
app launcher, wallpaper customization, and fully customizable with plugins.
|
||||||
|
|
||||||
|
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||||
|
process monitoring, notification center, clipboard history, dock, control center,
|
||||||
|
lock screen, and comprehensive plugin system.
|
||||||
|
|
||||||
|
%package -n dms-cli
|
||||||
|
Summary: DankMaterialShell CLI tool
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
%description -n dms-cli
|
||||||
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
|
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q -c -n dms-qml
|
||||||
|
|
||||||
|
# Download architecture-specific binaries during build
|
||||||
|
# This ensures the correct architecture is used for each build target
|
||||||
|
case "%{_arch}" in
|
||||||
|
x86_64)
|
||||||
|
ARCH_SUFFIX="amd64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
ARCH_SUFFIX="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: %{_arch}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Download dms-cli for target architecture
|
||||||
|
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
|
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
|
chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
|
||||||
|
# Shell completions
|
||||||
|
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
|
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
|
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
|
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||||
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
|
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
|
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
|
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
|
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
%posttrans
|
||||||
|
# Clean up old installation path from previous versions (only if empty)
|
||||||
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
|
# Remove directories only if empty (preserves any user-added files)
|
||||||
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
|
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
|
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Signal running DMS instances to reload (harmless if none running)
|
||||||
|
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||||
|
|
||||||
|
%files
|
||||||
|
%license LICENSE
|
||||||
|
%doc README.md CONTRIBUTING.md
|
||||||
|
%{_datadir}/quickshell/dms/
|
||||||
|
%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
%files -n dms-cli
|
||||||
|
%{_bindir}/dms
|
||||||
|
%{_datadir}/bash-completion/completions/dms
|
||||||
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
||||||
|
- Stable release VERSION_PLACEHOLDER
|
||||||
|
- Built from GitHub release
|
||||||
|
SPECEOF
|
||||||
|
|
||||||
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
|
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Spec file preview ==="
|
echo "=== Spec file preview ==="
|
||||||
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
|
head -40 ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
- name: Build SRPM
|
- name: Build SRPM
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
cd ~/rpmbuild/SPECS
|
cd ~/rpmbuild/SPECS
|
||||||
PACKAGE="${{ matrix.package }}"
|
|
||||||
|
|
||||||
echo "🔨 Building SRPM for ${PACKAGE}..."
|
echo "🔨 Building SRPM..."
|
||||||
rpmbuild -bs ${PACKAGE}.spec
|
rpmbuild -bs dms.spec
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.src.rpm | tail -n 1)
|
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
@@ -136,7 +234,7 @@ jobs:
|
|||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
retention-days: 90
|
retention-days: 90
|
||||||
|
|
||||||
@@ -157,40 +255,23 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ Copr CLI configured"
|
echo "✅ Copr CLI configured"
|
||||||
|
|
||||||
- name: Determine Copr project
|
|
||||||
id: copr_project
|
|
||||||
run: |
|
|
||||||
PACKAGE="${{ matrix.package }}"
|
|
||||||
if [ "$PACKAGE" = "dms" ]; then
|
|
||||||
COPR_PROJECT="avengemedia/dms"
|
|
||||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
|
||||||
COPR_PROJECT="avengemedia/danklinux"
|
|
||||||
else
|
|
||||||
echo "❌ Unknown package: $PACKAGE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
|
|
||||||
echo "✅ Copr project: $COPR_PROJECT"
|
|
||||||
|
|
||||||
- name: Upload to Copr
|
- name: Upload to Copr
|
||||||
run: |
|
run: |
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
|
||||||
PACKAGE="${{ matrix.package }}"
|
|
||||||
|
|
||||||
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
|
echo "🚀 Uploading SRPM to avengemedia/dms..."
|
||||||
echo " SRPM: $(basename $SRPM)"
|
echo " SRPM: $(basename $SRPM)"
|
||||||
echo " Version: $VERSION"
|
echo " Version: $VERSION"
|
||||||
|
|
||||||
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$SRPM" --nowait 2>&1)
|
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
echo "$BUILD_OUTPUT"
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
echo "✅ Build submitted successfully!"
|
echo "✅ Build submitted successfully!"
|
||||||
echo "🔗 https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
|
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
else
|
else
|
||||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||||
fi
|
fi
|
||||||
@@ -198,13 +279,10 @@ jobs:
|
|||||||
- name: Build summary
|
- name: Build summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
PACKAGE="${{ matrix.package }}"
|
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
|
||||||
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
|
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
396
.github/workflows/run-obs.yml
vendored
396
.github/workflows/run-obs.yml
vendored
@@ -4,19 +4,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to update (dms, dms-git, or all)"
|
description: 'Package to update (dms, dms-git, or all)'
|
||||||
required: false
|
required: false
|
||||||
default: "all"
|
default: 'all'
|
||||||
tag_version:
|
|
||||||
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
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
|
||||||
default: ""
|
default: ''
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
@@ -34,114 +33,78 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
env:
|
|
||||||
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
|
|
||||||
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
|
|
||||||
run: |
|
run: |
|
||||||
# Helper function to check dms-git commit
|
|
||||||
check_dms_git() {
|
|
||||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
|
|
||||||
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
|
||||||
|
|
||||||
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
|
||||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to check dms stable tag
|
|
||||||
check_dms_stable() {
|
|
||||||
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
|
|
||||||
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 "")
|
|
||||||
|
|
||||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
|
|
||||||
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main logic
|
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
# 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
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by tag: $VERSION (always update)"
|
echo "Triggered by tag: $VERSION (always update)"
|
||||||
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
# Scheduled run - check dms-git only
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
if check_dms_git; then
|
echo "Checking if dms-git source has changed..."
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match spec format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Check OBS for last uploaded commit
|
||||||
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
|
mkdir -p "$OBS_BASE"
|
||||||
|
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||||
|
|
||||||
|
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||||
|
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||||
|
osc up -q 2>/dev/null || true
|
||||||
|
|
||||||
|
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||||
|
if [[ -f "dms-git.spec" ]]; then
|
||||||
|
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$OBS_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No spec file in OBS, proceeding with update"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 First upload to OBS, update needed"
|
||||||
|
fi
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow trigger
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
PKG="${{ github.event.inputs.package }}"
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
if [[ -n "$REBUILD" ]]; then
|
|
||||||
# Rebuild requested - always proceed
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
|
||||||
# Check each package and build list of those needing updates
|
|
||||||
PACKAGES_TO_UPDATE=()
|
|
||||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
|
||||||
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
|
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "✓ All packages up to date"
|
|
||||||
fi
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
|
||||||
if check_dms_git; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms" ]]; then
|
|
||||||
if check_dms_stable; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
# Unknown package - proceed anyway
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: $PKG"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
# Fallback - proceed
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
@@ -150,7 +113,9 @@ jobs:
|
|||||||
name: Upload to OBS
|
name: Upload to OBS
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: needs.check-updates.outputs.has_updates == 'true'
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -162,57 +127,16 @@ jobs:
|
|||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
# Tag push event - use the pushed tag
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by tag: $VERSION"
|
echo "Triggered by tag: $VERSION"
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
# Scheduled run - dms-git only
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by schedule: updating git package"
|
echo "Triggered by schedule: updating git package"
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
# Determine version for dms stable
|
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
|
||||||
# For explicit dms selection, require tag_version
|
|
||||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
|
||||||
VERSION="${{ github.event.inputs.tag_version }}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using specified tag: $VERSION"
|
|
||||||
else
|
|
||||||
echo "ERROR: tag_version is required when package=dms"
|
|
||||||
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
|
||||||
# For "all", auto-detect if tag_version not specified
|
|
||||||
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
|
|
||||||
VERSION="${{ github.event.inputs.tag_version }}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using specified tag: $VERSION"
|
|
||||||
else
|
|
||||||
# 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 "")
|
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
echo "Auto-detected latest release: $LATEST_TAG"
|
|
||||||
else
|
|
||||||
echo "ERROR: Could not auto-detect latest release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
|
||||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
|
||||||
else
|
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
@@ -220,96 +144,88 @@ jobs:
|
|||||||
- name: Update dms-git spec version
|
- name: Update dms-git spec version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
run: |
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Update version in spec
|
||||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
# Single changelog entry (git snapshots don't need history)
|
# Add changelog entry
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
|
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
{
|
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||||
echo "$LOCAL_SPEC_HEAD"
|
|
||||||
echo "%changelog"
|
|
||||||
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
|
|
||||||
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
|
||||||
} > distro/opensuse/dms-git.spec
|
|
||||||
|
|
||||||
- name: Update Debian dms-git changelog version
|
- name: Update Debian dms-git changelog version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
run: |
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
# Debian version format: 0.6.2+git2256.9162e314
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
# Single changelog entry (git snapshots don't need history)
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
CHANGELOG_DATE=$(date -R)
|
||||||
{
|
|
||||||
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
|
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||||
echo ""
|
|
||||||
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
# Get current version from changelog
|
||||||
echo ""
|
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
echo "Current Debian version: $CURRENT_VERSION"
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Only update if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||||
|
# Create new changelog entry at top
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
|
||||||
|
cat > "$TEMP_CHANGELOG" << EOF
|
||||||
|
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||||
|
|
||||||
|
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Prepend to existing changelog
|
||||||
|
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||||
|
else
|
||||||
|
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Update dms stable version
|
- name: Update dms stable version
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
VERSION_NO_V="${VERSION#v}"
|
VERSION_NO_V="${VERSION#v}"
|
||||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
echo "Updating packaging to version $VERSION_NO_V"
|
||||||
|
|
||||||
# Update spec file
|
# Update openSUSE dms spec (stable only)
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
|
||||||
# Verify the update
|
# Update Debian _service files
|
||||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
|
||||||
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
|
||||||
|
|
||||||
# Single changelog entry (full history on OBS website)
|
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
|
||||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
|
||||||
{
|
|
||||||
echo "$LOCAL_SPEC_HEAD"
|
|
||||||
echo "%changelog"
|
|
||||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
|
||||||
echo "- Update to stable $VERSION release"
|
|
||||||
} > distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
# Update Debian _service files (both tar_scm and download_url formats)
|
|
||||||
for service in distro/debian/*/_service; do
|
for service in distro/debian/*/_service; do
|
||||||
if [[ -f "$service" ]]; then
|
if [[ -f "$service" ]]; then
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
|
||||||
# Update download_url paths (for dms stable)
|
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Update Debian changelog for dms stable (single entry, history on OBS website)
|
|
||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
{
|
|
||||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
|
||||||
echo ""
|
|
||||||
echo " * Update to $VERSION stable release"
|
|
||||||
echo ""
|
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
|
||||||
} > "distro/debian/dms/debian/changelog"
|
|
||||||
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: '1.24'
|
||||||
|
|
||||||
- name: Install OSC
|
- name: Install OSC
|
||||||
run: |
|
run: |
|
||||||
@@ -329,78 +245,32 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to OBS
|
- name: Upload to OBS
|
||||||
env:
|
env:
|
||||||
|
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
|
||||||
run: |
|
run: |
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
|
||||||
echo "✓ No packages need uploading. All up to date!"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
MESSAGE="Automated update from GitHub Actions"
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
# Loop through each package and upload
|
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
||||||
for PKG in $PACKAGES; do
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||||
echo ""
|
else
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||||
echo "Uploading $PKG to OBS..."
|
fi
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
|
||||||
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
|
|
||||||
if [[ "$PKG" == "dms-git" ]]; then
|
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
|
||||||
else
|
|
||||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
|
||||||
run: |
|
run: |
|
||||||
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
|
||||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
for PKG in $PACKAGES; do
|
|
||||||
case "$PKG" in
|
|
||||||
dms)
|
|
||||||
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
dms-git)
|
|
||||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
|
||||||
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
fi
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
274
.github/workflows/run-ppa.yml
vendored
274
.github/workflows/run-ppa.yml
vendored
@@ -4,15 +4,15 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
||||||
required: false
|
required: false
|
||||||
default: "dms-git"
|
default: 'dms-git'
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ''
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
@@ -32,113 +32,41 @@ jobs:
|
|||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
run: |
|
run: |
|
||||||
# Helper function to check dms-git commit
|
|
||||||
check_dms_git() {
|
|
||||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
|
||||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
|
||||||
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
|
||||||
|
|
||||||
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
|
|
||||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to check stable package tag
|
|
||||||
check_stable_package() {
|
|
||||||
local PKG="$1"
|
|
||||||
local PPA_NAME="$2"
|
|
||||||
# Use git ls-remote to find the latest tag, sorted by version (descending)
|
|
||||||
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
|
|
||||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
|
||||||
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
|
||||||
|
|
||||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
|
|
||||||
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main logic
|
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
# Scheduled run - check dms-git only
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
if check_dms_git; then
|
echo "Checking if dms-git source has changed..."
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match changelog format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Extract commit hash from changelog
|
||||||
|
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||||
|
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||||
|
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog file found, proceeding with upload"
|
||||||
|
fi
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow trigger
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
PKG="${{ github.event.inputs.package }}"
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
if [[ -n "$REBUILD" ]]; then
|
|
||||||
# Rebuild requested - always proceed
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
|
||||||
# Check each package and build list of those needing updates
|
|
||||||
PACKAGES_TO_UPDATE=()
|
|
||||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
|
||||||
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
|
|
||||||
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
|
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "✓ All packages up to date"
|
|
||||||
fi
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
|
||||||
if check_dms_git; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms" ]]; then
|
|
||||||
if check_stable_package "dms" "dms"; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
|
||||||
if check_stable_package "dms-greeter" "danklinux"; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
# Unknown package - proceed anyway
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: $PKG"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
# Fallback
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
@@ -147,7 +75,9 @@ jobs:
|
|||||||
name: Upload to PPA
|
name: Upload to PPA
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: needs.check-updates.outputs.has_updates == 'true'
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -158,7 +88,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: '1.24'
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
@@ -184,97 +114,75 @@ jobs:
|
|||||||
- name: Determine packages to upload
|
- name: Determine packages to upload
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
# Use packages determined by check-updates job
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by schedule: uploading git package"
|
echo "Triggered by schedule: uploading git package"
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload to PPA
|
- name: Upload to PPA
|
||||||
|
env:
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
run: |
|
run: |
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
echo "✓ No packages need uploading. All up to date!"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export REBUILD_RELEASE so ppa-build.sh can use it
|
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
|
||||||
export REBUILD_RELEASE
|
|
||||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
|
||||||
# Loop through each package and upload
|
|
||||||
for PKG in $PACKAGES; do
|
|
||||||
# Map package to PPA name
|
|
||||||
case "$PKG" in
|
|
||||||
dms)
|
|
||||||
PPA_NAME="dms"
|
|
||||||
;;
|
|
||||||
dms-git)
|
|
||||||
PPA_NAME="dms-git"
|
|
||||||
;;
|
|
||||||
dms-greeter)
|
|
||||||
PPA_NAME="danklinux"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "⚠️ Unknown package: $PKG, skipping"
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "Uploading $PKG to PPA $PPA_NAME..."
|
echo "Uploading dms to PPA..."
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
if [ -n "$REBUILD_RELEASE" ]; then
|
||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
|
||||||
done
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-git to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-greeter to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
||||||
|
else
|
||||||
|
PPA_NAME="$PACKAGES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PACKAGES to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
|
||||||
run: |
|
run: |
|
||||||
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
|
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||||
else
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||||
for PKG in $PACKAGES; do
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
case "$PKG" in
|
|
||||||
dms)
|
|
||||||
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
dms-git)
|
|
||||||
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
dms-greeter)
|
|
||||||
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
|
||||||
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
31
.github/workflows/stable.yml
vendored
31
.github/workflows/stable.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Update stable branch
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-stable:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Create GitHub App token
|
|
||||||
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:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Push to stable branch
|
|
||||||
env:
|
|
||||||
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
|
|
||||||
4
.github/workflows/update-vendor-hash.yml
vendored
4
.github/workflows/update-vendor-hash.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo "Attempting nix build to get new vendorHash..."
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
if output=$(nix build .#dms-shell 2>&1); then
|
if output=$(nix build .#dmsCli 2>&1); then
|
||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
echo "Verifying build with new vendorHash..."
|
echo "Verifying build with new vendorHash..."
|
||||||
nix build .#dms-shell
|
nix build .#dmsCli
|
||||||
echo "vendorHash updated successfully!"
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
- name: Commit and push vendorHash update
|
- name: Commit and push vendorHash update
|
||||||
|
|||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -96,17 +96,20 @@ go.work
|
|||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
vim/
|
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
|
# Extracted source trees in Ubuntu package directories
|
||||||
|
distro/ubuntu/*/dms-git-repo/
|
||||||
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
quickshell/dms-plugins
|
|
||||||
__pycache__
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- id: check-yaml
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
||||||
rev: v0.10.0.1
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
|
||||||
23
CHANGELOG.MD
23
CHANGELOG.MD
@@ -1,23 +0,0 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
|
||||||
|
|
||||||
# 1.2.0
|
|
||||||
|
|
||||||
- Added clipboard and clipboard history integration
|
|
||||||
- Added swipe to dismiss notification popups and from center
|
|
||||||
- Added paste from clipboard history view - requires wtype
|
|
||||||
- Optimize surface damage of OSD & Toast
|
|
||||||
- Add monitor configurator (niri, Hyprland, MangoWC)
|
|
||||||
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
|
|
||||||
- requires intervention and doc update
|
|
||||||
- Added desktop widget plugins
|
|
||||||
- dev guidance available
|
|
||||||
- builtin clock & dgop widgets
|
|
||||||
- new IPC targets
|
|
||||||
- Initial RTL support/i18n
|
|
||||||
- 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
|
|
||||||
@@ -6,10 +6,10 @@ To contribute fork this repository, make your changes, and open a pull request.
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
Enable pre-commit hooks to catch CI failures before pushing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
prek install
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nix Development Shell
|
### Nix Development Shell
|
||||||
@@ -21,7 +21,6 @@ nix develop
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will provide:
|
This will provide:
|
||||||
|
|
||||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
- Quickshell and required QML packages
|
- Quickshell and required QML packages
|
||||||
- Properly configured QML2_IMPORT_PATH
|
- Properly configured QML2_IMPORT_PATH
|
||||||
@@ -55,20 +54,6 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
### I18n/Localization
|
|
||||||
|
|
||||||
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
|
||||||
|
|
||||||
### GO (`core` directory)
|
### GO (`core` directory)
|
||||||
|
|
||||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ quickshell -p quickshell/
|
|||||||
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|
||||||
# Use in home-manager or NixOS configuration
|
# Use in home-manager or NixOS configuration
|
||||||
imports = [ inputs.dms.homeModules.dank-material-shell ];
|
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=DMS
|
Name=DMS Application Picker
|
||||||
Comment=Select an application to open links and files
|
Comment=Select an application to open links and files
|
||||||
Exec=dms open %u
|
Exec=dms open %u
|
||||||
Icon=danklogo
|
Icon=danklogo
|
||||||
Terminal=false
|
Terminal=false
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Type=dbus
|
|||||||
BusName=org.freedesktop.Notifications
|
BusName=org.freedesktop.Notifications
|
||||||
ExecStart=/usr/bin/dms run --session
|
ExecStart=/usr/bin/dms run --session
|
||||||
ExecReload=/usr/bin/pkill -USR1 -x dms
|
ExecReload=/usr/bin/pkill -USR1 -x dms
|
||||||
Restart=on-failure
|
Restart=always
|
||||||
RestartSec=1.23
|
RestartSec=1.23
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
|
|
||||||
|
|||||||
@@ -102,11 +102,7 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- ineffassign
|
- ineffassign
|
||||||
path: internal/proto/
|
path: internal/proto/
|
||||||
# binary.Write/Read to bytes.Buffer can't fail
|
# binary.Write to bytes.Buffer can't fail
|
||||||
- linters:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
text: "Error return value of `binary\\.(Write|Read)` is not checked"
|
text: "Error return value of `binary\\.Write` is not checked"
|
||||||
# bytes.Reader.Read can't fail (reads from memory)
|
|
||||||
- linters:
|
|
||||||
- errcheck
|
|
||||||
text: "Error return value of `buf\\.Read` is not checked"
|
|
||||||
|
|||||||
@@ -56,21 +56,3 @@ packages:
|
|||||||
outpkg: mocks_version
|
outpkg: mocks_version
|
||||||
interfaces:
|
interfaces:
|
||||||
VersionFetcher:
|
VersionFetcher:
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/wlcontext"
|
|
||||||
outpkg: mocks_wlcontext
|
|
||||||
interfaces:
|
|
||||||
WaylandContext:
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/wlclient"
|
|
||||||
outpkg: mocks_wlclient
|
|
||||||
interfaces:
|
|
||||||
WaylandDisplay:
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/utils"
|
|
||||||
outpkg: mocks_utils
|
|
||||||
interfaces:
|
|
||||||
AppChecker:
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
|
||||||
rev: v2.6.2
|
|
||||||
hooks:
|
|
||||||
- id: golangci-lint-fmt
|
|
||||||
require_serial: true
|
|
||||||
- id: golangci-lint-full
|
|
||||||
- id: golangci-lint-config-verify
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: go-test
|
|
||||||
name: go test
|
|
||||||
entry: go test ./...
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
types: [go]
|
|
||||||
@@ -14,63 +14,34 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
### Wayland Protocols (Client)
|
**Wayland Protocols**
|
||||||
|
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
||||||
|
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
|
||||||
|
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
|
||||||
|
- `wp-viewporter` - Fractional scaling support
|
||||||
|
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
||||||
|
- `ext-workspace-v1` - Workspace protocol support
|
||||||
|
- `wlr-output-management-unstable-v1` - Display configuration
|
||||||
|
|
||||||
All Wayland protocols are consumed as a client - connecting to the compositor.
|
**DBus Interfaces**
|
||||||
|
- NetworkManager/iwd - Network management
|
||||||
|
- logind - Session control and inhibit locks
|
||||||
|
- accountsservice - User account information
|
||||||
|
- CUPS - Printer management
|
||||||
|
- Custom IPC via unix socket (JSON API)
|
||||||
|
|
||||||
| Protocol | Purpose |
|
**Hardware Control**
|
||||||
| ----------------------------------------- | ----------------------------------------------------------- |
|
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
||||||
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
|
- Backlight control - Internal display brightness via `login1` or sysfs
|
||||||
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
|
- LED control - Keyboard/device LED management
|
||||||
| `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
|
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
|
||||||
| `wlr-output-management-unstable-v1` | Display configuration |
|
|
||||||
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
|
|
||||||
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
|
|
||||||
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
|
|
||||||
| `ext-data-control-v1` | Clipboard history and persistence |
|
|
||||||
| `ext-workspace-v1` | Workspace integration |
|
|
||||||
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
|
|
||||||
|
|
||||||
### DBus Interfaces
|
|
||||||
|
|
||||||
**Client (consuming external services):**
|
|
||||||
|
|
||||||
| Interface | Purpose |
|
|
||||||
| -------------------------------- | --------------------------------------------- |
|
|
||||||
| `org.bluez` | Bluetooth management with pairing agent |
|
|
||||||
| `org.freedesktop.NetworkManager` | Network management |
|
|
||||||
| `net.connman.iwd` | iwd Wi-Fi backend |
|
|
||||||
| `org.freedesktop.network1` | systemd-networkd integration |
|
|
||||||
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
|
|
||||||
| `org.freedesktop.Accounts` | User account information |
|
|
||||||
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
|
|
||||||
| CUPS via IPP + D-Bus | Printer management with job notifications |
|
|
||||||
|
|
||||||
**Server (implementing interfaces):**
|
|
||||||
|
|
||||||
| Interface | Purpose |
|
|
||||||
| ----------------------------- | -------------------------------------- |
|
|
||||||
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
|
|
||||||
|
|
||||||
Custom IPC via unix socket (JSON API) for shell communication.
|
|
||||||
|
|
||||||
### Hardware Control
|
|
||||||
|
|
||||||
| Subsystem | Method | Purpose |
|
|
||||||
| --------- | ------------------- | ---------------------------------- |
|
|
||||||
| DDC/CI | I2C direct | External monitor brightness |
|
|
||||||
| Backlight | logind or sysfs | Internal display brightness |
|
|
||||||
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
|
|
||||||
| udev | netlink monitor | Backlight device updates (for OSD) |
|
|
||||||
|
|
||||||
### Plugin System
|
|
||||||
|
|
||||||
|
**Plugin System**
|
||||||
- Plugin registry integration
|
- Plugin registry integration
|
||||||
- Plugin lifecycle management
|
- Plugin lifecycle management
|
||||||
- Settings persistence
|
- Settings persistence
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
- `dms run [-d]` - Start shell (optionally as daemon)
|
- `dms run [-d]` - Start shell (optionally as daemon)
|
||||||
- `dms restart` / `dms kill` - Manage running processes
|
- `dms restart` / `dms kill` - Manage running processes
|
||||||
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||||
@@ -99,7 +70,6 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
|||||||
Requires Go 1.24+
|
Requires Go 1.24+
|
||||||
|
|
||||||
**Development build:**
|
**Development build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make # Build dms CLI
|
make # Build dms CLI
|
||||||
make dankinstall # Build installer
|
make dankinstall # Build installer
|
||||||
@@ -107,7 +77,6 @@ make test # Run tests
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update/greeter features
|
make dist # Build without update/greeter features
|
||||||
```
|
```
|
||||||
@@ -115,7 +84,6 @@ make dist # Build without update/greeter features
|
|||||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
|
|
||||||
**Installation:**
|
**Installation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo make install # Install to /usr/local/bin/dms
|
sudo make install # Install to /usr/local/bin/dms
|
||||||
```
|
```
|
||||||
@@ -123,7 +91,6 @@ sudo make install # Install to /usr/local/bin/dms
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
**Setup pre-commit hooks:**
|
**Setup pre-commit hooks:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
@@ -131,7 +98,6 @@ git config core.hooksPath .githooks
|
|||||||
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
||||||
|
|
||||||
**Regenerating Wayland Protocol Bindings:**
|
**Regenerating Wayland Protocol Bindings:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
||||||
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
||||||
@@ -139,7 +105,6 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Module Structure:**
|
**Module Structure:**
|
||||||
|
|
||||||
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
||||||
- `internal/distros/` - Distribution-specific installation logic
|
- `internal/distros/` - Distribution-specific installation logic
|
||||||
- `internal/proto/` - Wayland protocol bindings
|
- `internal/proto/` - Wayland protocol bindings
|
||||||
|
|||||||
@@ -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,797 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
|
||||||
_ "golang.org/x/image/bmp"
|
|
||||||
_ "golang.org/x/image/tiff"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var clipboardCmd = &cobra.Command{
|
|
||||||
Use: "clipboard",
|
|
||||||
Aliases: []string{"cl"},
|
|
||||||
Short: "Manage clipboard",
|
|
||||||
Long: "Interact with the clipboard manager",
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipCopyCmd = &cobra.Command{
|
|
||||||
Use: "copy [text]",
|
|
||||||
Short: "Copy text to clipboard",
|
|
||||||
Long: "Copy text to clipboard. If no text provided, reads from stdin. Works without server.",
|
|
||||||
Run: runClipCopy,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
clipCopyForeground bool
|
|
||||||
clipCopyPasteOnce bool
|
|
||||||
clipCopyType string
|
|
||||||
clipJSONOutput bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var clipPasteCmd = &cobra.Command{
|
|
||||||
Use: "paste",
|
|
||||||
Short: "Paste text from clipboard",
|
|
||||||
Long: "Paste text from clipboard to stdout. Works without server.",
|
|
||||||
Run: runClipPaste,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipWatchCmd = &cobra.Command{
|
|
||||||
Use: "watch [command]",
|
|
||||||
Short: "Watch clipboard for changes",
|
|
||||||
Long: `Watch clipboard for changes and optionally execute a command.
|
|
||||||
Works like wl-paste --watch. Does not require server.
|
|
||||||
|
|
||||||
If a command is provided, it will be executed each time the clipboard changes,
|
|
||||||
with the clipboard content piped to its stdin.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms cl watch # Print clipboard changes to stdout
|
|
||||||
dms cl watch cat # Same as above
|
|
||||||
dms cl watch notify-send # Send notification on clipboard change`,
|
|
||||||
Run: runClipWatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipHistoryCmd = &cobra.Command{
|
|
||||||
Use: "history",
|
|
||||||
Short: "Show clipboard history",
|
|
||||||
Long: "Show clipboard history with previews (requires server)",
|
|
||||||
Run: runClipHistory,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipGetCmd = &cobra.Command{
|
|
||||||
Use: "get <id>",
|
|
||||||
Short: "Get clipboard entry by ID",
|
|
||||||
Long: "Get full clipboard entry data by ID (requires server). Use --copy to copy it to clipboard.",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runClipGet,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipGetCopy bool
|
|
||||||
|
|
||||||
var clipDeleteCmd = &cobra.Command{
|
|
||||||
Use: "delete <id>",
|
|
||||||
Short: "Delete clipboard entry",
|
|
||||||
Long: "Delete a clipboard history entry by ID (requires server)",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runClipDelete,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipClearCmd = &cobra.Command{
|
|
||||||
Use: "clear",
|
|
||||||
Short: "Clear clipboard history",
|
|
||||||
Long: "Clear all clipboard history (requires server)",
|
|
||||||
Run: runClipClear,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipWatchStore bool
|
|
||||||
|
|
||||||
var clipSearchCmd = &cobra.Command{
|
|
||||||
Use: "search [query]",
|
|
||||||
Short: "Search clipboard history",
|
|
||||||
Long: "Search clipboard history with filters (requires server)",
|
|
||||||
Run: runClipSearch,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
clipSearchLimit int
|
|
||||||
clipSearchOffset int
|
|
||||||
clipSearchMimeType string
|
|
||||||
clipSearchImages bool
|
|
||||||
clipSearchText bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var clipConfigCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Short: "Manage clipboard config",
|
|
||||||
Long: "Get or set clipboard configuration (requires server)",
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipConfigGetCmd = &cobra.Command{
|
|
||||||
Use: "get",
|
|
||||||
Short: "Get clipboard config",
|
|
||||||
Run: runClipConfigGet,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipConfigSetCmd = &cobra.Command{
|
|
||||||
Use: "set",
|
|
||||||
Short: "Set clipboard config",
|
|
||||||
Long: `Set clipboard configuration options.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms cl config set --max-history 200
|
|
||||||
dms cl config set --auto-clear-days 7
|
|
||||||
dms cl config set --clear-at-startup`,
|
|
||||||
Run: runClipConfigSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
clipConfigMaxHistory int
|
|
||||||
clipConfigAutoClearDays int
|
|
||||||
clipConfigClearAtStartup bool
|
|
||||||
clipConfigNoClearStartup bool
|
|
||||||
clipConfigDisabled bool
|
|
||||||
clipConfigEnabled 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() {
|
|
||||||
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().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
|
||||||
|
|
||||||
clipWatchCmd.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().BoolVarP(&clipGetCopy, "copy", "c", false, "Copy entry to clipboard")
|
|
||||||
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
|
||||||
clipSearchCmd.Flags().StringVarP(&clipSearchMimeType, "mime", "m", "", "Filter by MIME type")
|
|
||||||
clipSearchCmd.Flags().BoolVar(&clipSearchImages, "images", false, "Only images")
|
|
||||||
clipSearchCmd.Flags().BoolVar(&clipSearchText, "text", false, "Only text")
|
|
||||||
clipSearchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
|
||||||
|
|
||||||
clipConfigSetCmd.Flags().IntVar(&clipConfigMaxHistory, "max-history", 0, "Max history entries")
|
|
||||||
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(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard tracking")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking")
|
|
||||||
|
|
||||||
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)
|
|
||||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
data = []byte(args[0])
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
data, err = io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("read stdin: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("copy: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipPaste(cmd *cobra.Command, args []string) {
|
|
||||||
data, _, err := clipboard.Paste()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("paste: %v", err)
|
|
||||||
}
|
|
||||||
os.Stdout.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipWatch(cmd *cobra.Command, args []string) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
<-sigCh
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(args) > 0:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
runCommand(args, data)
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
case clipWatchStore:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
if err := clipboard.Store(data, mimeType); err != nil {
|
|
||||||
log.Errorf("store: %v", err)
|
|
||||||
}
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
case clipJSONOutput:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
out := map[string]any{
|
|
||||||
"data": string(data),
|
|
||||||
"mimeType": mimeType,
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"size": len(data),
|
|
||||||
}
|
|
||||||
j, _ := json.Marshal(out)
|
|
||||||
fmt.Println(string(j))
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
os.Stdout.Write(data)
|
|
||||||
os.Stdout.WriteString("\n")
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCommand(args []string, stdin []byte) {
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if len(stdin) == 0 {
|
|
||||||
cmd.Run()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
cmd.Run()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdin = r
|
|
||||||
go func() {
|
|
||||||
w.Write(stdin)
|
|
||||||
w.Close()
|
|
||||||
}()
|
|
||||||
cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipHistory(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 {
|
|
||||||
if clipJSONOutput {
|
|
||||||
fmt.Println("[]")
|
|
||||||
} else {
|
|
||||||
fmt.Println("No clipboard history")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
historyList, ok := (*resp.Result).([]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipJSONOutput {
|
|
||||||
out, _ := json.MarshalIndent(historyList, "", " ")
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(historyList) == 0 {
|
|
||||||
fmt.Println("No clipboard history")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Clipboard History:")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
for _, item := range historyList {
|
|
||||||
entry, ok := item.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uint64(entry["id"].(float64))
|
|
||||||
preview := entry["preview"].(string)
|
|
||||||
timestamp := entry["timestamp"].(string)
|
|
||||||
isImage := entry["isImage"].(bool)
|
|
||||||
|
|
||||||
typeStr := "text"
|
|
||||||
if isImage {
|
|
||||||
typeStr = "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp)
|
|
||||||
fmt.Printf(" %s\n", preview)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipGet(cmd *cobra.Command, args []string) {
|
|
||||||
id, err := strconv.ParseUint(args[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Invalid ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipGetCopy {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.copyEntry",
|
|
||||||
Params: map[string]any{
|
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Copied entry %d to clipboard\n", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.getEntry",
|
|
||||||
Params: map[string]any{
|
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("Entry not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, ok := (*resp.Result).(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case clipJSONOutput:
|
|
||||||
output, _ := json.MarshalIndent(entry, "", " ")
|
|
||||||
fmt.Println(string(output))
|
|
||||||
default:
|
|
||||||
if data, ok := entry["data"].(string); ok {
|
|
||||||
fmt.Print(data)
|
|
||||||
} else {
|
|
||||||
output, _ := json.MarshalIndent(entry, "", " ")
|
|
||||||
fmt.Println(string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipDelete(cmd *cobra.Command, args []string) {
|
|
||||||
id, err := strconv.ParseUint(args[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Invalid ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.deleteEntry",
|
|
||||||
Params: map[string]any{
|
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to delete clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Deleted entry %d\n", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipClear(cmd *cobra.Command, args []string) {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.clearHistory",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to clear clipboard history: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Clipboard history cleared")
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipSearch(cmd *cobra.Command, args []string) {
|
|
||||||
params := map[string]any{
|
|
||||||
"limit": clipSearchLimit,
|
|
||||||
"offset": clipSearchOffset,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
params["query"] = args[0]
|
|
||||||
}
|
|
||||||
if clipSearchMimeType != "" {
|
|
||||||
params["mimeType"] = clipSearchMimeType
|
|
||||||
}
|
|
||||||
if clipSearchImages {
|
|
||||||
params["isImage"] = true
|
|
||||||
} else if clipSearchText {
|
|
||||||
params["isImage"] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.search",
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to search clipboard: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("No results")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := (*resp.Result).(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipJSONOutput {
|
|
||||||
out, _ := json.MarshalIndent(result, "", " ")
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, _ := result["entries"].([]any)
|
|
||||||
total := int(result["total"].(float64))
|
|
||||||
hasMore := result["hasMore"].(bool)
|
|
||||||
|
|
||||||
if len(entries) == 0 {
|
|
||||||
fmt.Println("No results found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Results: %d of %d\n\n", len(entries), total)
|
|
||||||
|
|
||||||
for _, item := range entries {
|
|
||||||
entry, ok := item.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uint64(entry["id"].(float64))
|
|
||||||
preview := entry["preview"].(string)
|
|
||||||
timestamp := entry["timestamp"].(string)
|
|
||||||
isImage := entry["isImage"].(bool)
|
|
||||||
|
|
||||||
typeStr := "text"
|
|
||||||
if isImage {
|
|
||||||
typeStr = "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp)
|
|
||||||
fmt.Printf(" %s\n\n", preview)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasMore {
|
|
||||||
fmt.Printf("Use --offset %d to see more results\n", clipSearchOffset+clipSearchLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipConfigGet(cmd *cobra.Command, args []string) {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.getConfig",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("No config returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, ok := (*resp.Result).(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
output, _ := json.MarshalIndent(cfg, "", " ")
|
|
||||||
fmt.Println(string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipConfigSet(cmd *cobra.Command, args []string) {
|
|
||||||
params := map[string]any{}
|
|
||||||
|
|
||||||
if cmd.Flags().Changed("max-history") {
|
|
||||||
params["maxHistory"] = clipConfigMaxHistory
|
|
||||||
}
|
|
||||||
if cmd.Flags().Changed("auto-clear-days") {
|
|
||||||
params["autoClearDays"] = clipConfigAutoClearDays
|
|
||||||
}
|
|
||||||
if clipConfigClearAtStartup {
|
|
||||||
params["clearAtStartup"] = true
|
|
||||||
}
|
|
||||||
if clipConfigNoClearStartup {
|
|
||||||
params["clearAtStartup"] = false
|
|
||||||
}
|
|
||||||
if clipConfigDisabled {
|
|
||||||
params["disabled"] = true
|
|
||||||
}
|
|
||||||
if clipConfigEnabled {
|
|
||||||
params["disabled"] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(params) == 0 {
|
|
||||||
fmt.Println("No config options specified")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.setConfig",
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to set config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 0644); 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, 0644, &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)
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -121,7 +121,13 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyToClipboard(text string) {
|
func copyToClipboard(text string) {
|
||||||
if err := clipboard.CopyText(text); err != nil {
|
var cmd *exec.Cmd
|
||||||
fmt.Fprintln(os.Stderr, "clipboard copy failed:", err)
|
if _, err := exec.LookPath("wl-copy"); err == nil {
|
||||||
|
cmd = exec.Command("wl-copy", text)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = cmd.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ var pluginsUpdateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
|
printASCII()
|
||||||
fmt.Printf("%s\n", formatVersion(Version))
|
fmt.Printf("%s\n", formatVersion(Version))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +220,7 @@ func getBaseVersion() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
return "1.0.2"
|
return "0.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
@@ -512,8 +513,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ func init() {
|
|||||||
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
dank16Cmd.Flags().Bool("neovim", false, "Output in Neovim plugin format")
|
|
||||||
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
||||||
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
||||||
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
@@ -41,7 +40,6 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
isJson, _ := cmd.Flags().GetBool("json")
|
isJson, _ := cmd.Flags().GetBool("json")
|
||||||
isKitty, _ := cmd.Flags().GetBool("kitty")
|
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||||
isFoot, _ := cmd.Flags().GetBool("foot")
|
isFoot, _ := cmd.Flags().GetBool("foot")
|
||||||
isNeovim, _ := cmd.Flags().GetBool("neovim")
|
|
||||||
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
||||||
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
||||||
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
@@ -118,8 +116,6 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
} else if isWezterm {
|
} else if isWezterm {
|
||||||
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
||||||
} else if isNeovim {
|
|
||||||
fmt.Print(dank16.GenerateNeovimTheme(colors))
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,930 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
||||||
"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/utils"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"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+)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
|
||||||
Use: "doctor",
|
|
||||||
Short: "Diagnose DMS installation and dependencies",
|
|
||||||
Long: "Check system health, verify dependencies, and diagnose configuration issues for DMS",
|
|
||||||
Run: runDoctor,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
doctorVerbose bool
|
|
||||||
doctorJSON bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
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
|
|
||||||
|
|
||||||
const (
|
|
||||||
catSystem category = iota
|
|
||||||
catVersions
|
|
||||||
catInstallation
|
|
||||||
catCompositor
|
|
||||||
catQuickshellFeatures
|
|
||||||
catOptionalFeatures
|
|
||||||
catConfigFiles
|
|
||||||
catServices
|
|
||||||
catEnvironment
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c category) String() string {
|
|
||||||
switch c {
|
|
||||||
case catSystem:
|
|
||||||
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 {
|
|
||||||
category category
|
|
||||||
name string
|
|
||||||
status status
|
|
||||||
message 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) {
|
|
||||||
if !doctorJSON {
|
|
||||||
printDoctorHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
qsFeatures, qsMissingFeatures := checkQuickshellFeatures()
|
|
||||||
|
|
||||||
results := slices.Concat(
|
|
||||||
checkSystemInfo(),
|
|
||||||
checkVersions(qsMissingFeatures),
|
|
||||||
checkDMSInstallation(),
|
|
||||||
checkWindowManagers(),
|
|
||||||
qsFeatures,
|
|
||||||
checkOptionalDependencies(),
|
|
||||||
checkConfigurationFiles(),
|
|
||||||
checkSystemdServices(),
|
|
||||||
checkEnvironmentVars(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if doctorJSON {
|
|
||||||
printResultsJSON(results)
|
|
||||||
} else {
|
|
||||||
printResults(results)
|
|
||||||
printSummary(results, qsMissingFeatures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printDoctorHeader() {
|
|
||||||
theme := tui.TerminalTheme()
|
|
||||||
styles := tui.NewStyles(theme)
|
|
||||||
|
|
||||||
fmt.Println(getThemedASCII())
|
|
||||||
fmt.Println(styles.Title.Render("System Health Check"))
|
|
||||||
fmt.Println(styles.Subtle.Render("──────────────────────────────────────"))
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSystemInfo() []checkResult {
|
|
||||||
var results []checkResult
|
|
||||||
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
|
||||||
if err != nil {
|
|
||||||
status, message, details := statusWarn, fmt.Sprintf("Unknown (%v)", err), ""
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "Unsupported distribution") {
|
|
||||||
osRelease := readOSRelease()
|
|
||||||
switch {
|
|
||||||
case osRelease["ID"] == "nixos":
|
|
||||||
status = statusOK
|
|
||||||
message = osRelease["PRETTY_NAME"]
|
|
||||||
if message == "" {
|
|
||||||
message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"])
|
|
||||||
}
|
|
||||||
details = "Supported for runtime (install via NixOS module or Flake)"
|
|
||||||
case 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, checkResult{catSystem, "Operating System", status, message, details, doctorDocsURL + "#operating-system"})
|
|
||||||
} else {
|
|
||||||
status := statusOK
|
|
||||||
message := osInfo.PrettyName
|
|
||||||
if message == "" {
|
|
||||||
message = fmt.Sprintf("%s %s", osInfo.Distribution.ID, osInfo.VersionID)
|
|
||||||
}
|
|
||||||
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
|
|
||||||
status = statusWarn
|
|
||||||
message += " (version may not be fully supported)"
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{
|
|
||||||
catSystem, "Operating System", status, message,
|
|
||||||
fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture),
|
|
||||||
doctorDocsURL + "#operating-system",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
arch := runtime.GOARCH
|
|
||||||
archStatus := statusOK
|
|
||||||
if arch != "amd64" && arch != "arm64" {
|
|
||||||
archStatus = statusError
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, "", doctorDocsURL + "#architecture"})
|
|
||||||
|
|
||||||
waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
|
|
||||||
xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case waylandDisplay != "" || xdgSessionType == "wayland":
|
|
||||||
results = append(results, checkResult{
|
|
||||||
catSystem, "Display Server", statusOK, "Wayland",
|
|
||||||
fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay),
|
|
||||||
doctorDocsURL + "#display-server",
|
|
||||||
})
|
|
||||||
case xdgSessionType == "x11":
|
|
||||||
results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", "", doctorDocsURL + "#display-server"})
|
|
||||||
default:
|
|
||||||
results = append(results, checkResult{
|
|
||||||
catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)",
|
|
||||||
fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType),
|
|
||||||
doctorDocsURL + "#display-server",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
result := make(map[string]string)
|
|
||||||
data, err := os.ReadFile("/etc/os-release")
|
|
||||||
if err != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for line := range strings.SplitSeq(string(data), "\n") {
|
|
||||||
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 {
|
|
||||||
result[parts[0]] = strings.Trim(parts[1], "\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVersions(qsMissingFeatures bool) []checkResult {
|
|
||||||
dmsCliPath, _ := os.Executable()
|
|
||||||
dmsCliDetails := ""
|
|
||||||
if doctorVerbose {
|
|
||||||
dmsCliDetails = dmsCliPath
|
|
||||||
}
|
|
||||||
|
|
||||||
results := []checkResult{
|
|
||||||
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
|
|
||||||
}
|
|
||||||
|
|
||||||
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
|
|
||||||
qsDetails := ""
|
|
||||||
if doctorVerbose && qsPath != "" {
|
|
||||||
qsDetails = qsPath
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
|
|
||||||
|
|
||||||
dmsVersion, dmsPath := getDMSShellVersion()
|
|
||||||
if dmsVersion != "" {
|
|
||||||
results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath, doctorDocsURL + "#dms-shell"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install", doctorDocsURL + "#dms-shell"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDMSShellVersion() (version, path string) {
|
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
|
||||||
versionFile := filepath.Join(configPath, "VERSION")
|
|
||||||
if data, err := os.ReadFile(versionFile); err == nil {
|
|
||||||
return strings.TrimSpace(string(data)), configPath
|
|
||||||
}
|
|
||||||
return "installed", configPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
|
||||||
versionFile := filepath.Join(dmsPath, "VERSION")
|
|
||||||
if data, err := os.ReadFile(versionFile); err == nil {
|
|
||||||
return strings.TrimSpace(string(data)), dmsPath
|
|
||||||
}
|
|
||||||
return "installed", dmsPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
|
|
||||||
if !utils.CommandExists("qs") {
|
|
||||||
return "Not installed", statusError, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
qsPath, _ := exec.LookPath("qs")
|
|
||||||
|
|
||||||
output, err := exec.Command("qs", "--version").Output()
|
|
||||||
if err != nil {
|
|
||||||
return "Installed (version check failed)", statusWarn, qsPath
|
|
||||||
}
|
|
||||||
|
|
||||||
fullVersion := strings.TrimSpace(string(output))
|
|
||||||
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
|
|
||||||
if version.CompareVersions(matches[1], "0.2.0") < 0 {
|
|
||||||
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
|
|
||||||
}
|
|
||||||
if missingFeatures {
|
|
||||||
return fullVersion, statusWarn, qsPath
|
|
||||||
}
|
|
||||||
return fullVersion, statusOK, qsPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullVersion, statusWarn, qsPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkDMSInstallation() []checkResult {
|
|
||||||
var results []checkResult
|
|
||||||
|
|
||||||
dmsPath := ""
|
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
|
||||||
dmsPath = configPath
|
|
||||||
} else if path, err := config.LocateDMSConfig(); err == nil {
|
|
||||||
dmsPath = path
|
|
||||||
}
|
|
||||||
|
|
||||||
if dmsPath == "" {
|
|
||||||
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath, doctorDocsURL + "#dms-configuration"})
|
|
||||||
|
|
||||||
shellQml := filepath.Join(dmsPath, "shell.qml")
|
|
||||||
if _, err := os.Stat(shellQml); err != nil {
|
|
||||||
results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml, doctorDocsURL + "#dms-configuration"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml, doctorDocsURL + "#dms-configuration"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if doctorVerbose {
|
|
||||||
installType := "Unknown"
|
|
||||||
switch {
|
|
||||||
case strings.Contains(dmsPath, "/nix/store"):
|
|
||||||
installType = "Nix store"
|
|
||||||
case strings.Contains(dmsPath, ".local/share") || strings.Contains(dmsPath, "/usr/share"):
|
|
||||||
installType = "System package"
|
|
||||||
case strings.Contains(dmsPath, ".config"):
|
|
||||||
installType = "User config"
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath, doctorDocsURL + "#dms-configuration"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkWindowManagers() []checkResult {
|
|
||||||
compositors := []struct {
|
|
||||||
name, versionCmd, versionArg string
|
|
||||||
versionRegex *regexp.Regexp
|
|
||||||
commands []string
|
|
||||||
}{
|
|
||||||
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
|
|
||||||
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
|
|
||||||
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
|
|
||||||
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
|
|
||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []checkResult
|
|
||||||
foundAny := false
|
|
||||||
|
|
||||||
for _, c := range compositors {
|
|
||||||
if !slices.ContainsFunc(c.commands, utils.CommandExists) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundAny {
|
|
||||||
results = append(results, checkResult{
|
|
||||||
catCompositor, "Compositor", statusError,
|
|
||||||
"No supported Wayland compositor found",
|
|
||||||
"Install Hyprland, niri, Sway, River, or Wayfire",
|
|
||||||
doctorDocsURL + "#compositor",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if wm := detectRunningWM(); wm != "" {
|
|
||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
|
||||||
output, err := exec.Command(cmd, arg).Output()
|
|
||||||
if err != nil {
|
|
||||||
return "installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
outStr := string(output)
|
|
||||||
if matches := regex.FindStringSubmatch(outStr); len(matches) > 1 {
|
|
||||||
ver := matches[1]
|
|
||||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
|
||||||
return ver + " (git)"
|
|
||||||
}
|
|
||||||
return ver
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(outStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectRunningWM() string {
|
|
||||||
switch {
|
|
||||||
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
|
|
||||||
return "Hyprland"
|
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
|
||||||
return "niri"
|
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
|
||||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkQuickshellFeatures() ([]checkResult, bool) {
|
|
||||||
if !utils.CommandExists("qs") {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir := os.TempDir()
|
|
||||||
testScript := filepath.Join(tmpDir, "qs-feature-test.qml")
|
|
||||||
defer os.Remove(testScript)
|
|
||||||
|
|
||||||
qmlContent := `
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
ShellRoot {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool polkitAvailable: false
|
|
||||||
property bool idleMonitorAvailable: false
|
|
||||||
property bool idleInhibitorAvailable: false
|
|
||||||
property bool shortcutInhibitorAvailable: false
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 50
|
|
||||||
running: true
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
try {
|
|
||||||
var polkitTest = Qt.createQmlObject(
|
|
||||||
'import Quickshell.Services.Polkit; import QtQuick; Item {}',
|
|
||||||
root
|
|
||||||
)
|
|
||||||
root.polkitAvailable = true
|
|
||||||
polkitTest.destroy()
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var testItem = Qt.createQmlObject(
|
|
||||||
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
|
||||||
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
|
||||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
|
||||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
|
||||||
'}',
|
|
||||||
root
|
|
||||||
)
|
|
||||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
|
||||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
|
||||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
|
||||||
testItem.destroy()
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
console.warn(root.polkitAvailable ? "FEATURE:Polkit:OK" : "FEATURE:Polkit:UNAVAILABLE")
|
|
||||||
console.warn(root.idleMonitorAvailable ? "FEATURE:IdleMonitor:OK" : "FEATURE:IdleMonitor:UNAVAILABLE")
|
|
||||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
|
||||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
|
||||||
|
|
||||||
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
if err := os.WriteFile(testScript, []byte(qmlContent), 0644); err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("qs", "-p", testScript)
|
|
||||||
cmd.Env = append(os.Environ(), "NO_COLOR=1")
|
|
||||||
output, _ := cmd.CombinedOutput()
|
|
||||||
outputStr := string(output)
|
|
||||||
|
|
||||||
features := []struct{ name, desc string }{
|
|
||||||
{"Polkit", "Authentication prompts"},
|
|
||||||
{"IdleMonitor", "Idle detection"},
|
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []checkResult
|
|
||||||
missingFeatures := false
|
|
||||||
|
|
||||||
for _, f := range features {
|
|
||||||
available := strings.Contains(outputStr, fmt.Sprintf("FEATURE:%s:OK", f.name))
|
|
||||||
status, message := statusOK, "Available"
|
|
||||||
if !available {
|
|
||||||
status, message = statusInfo, "Not available"
|
|
||||||
missingFeatures = true
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc, doctorDocsURL + "#quickshell-features"})
|
|
||||||
}
|
|
||||||
|
|
||||||
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() string {
|
|
||||||
result, err := network.DetectNetworkStack()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result.Backend {
|
|
||||||
case network.BackendNetworkManager:
|
|
||||||
return "NetworkManager"
|
|
||||||
case network.BackendIwd:
|
|
||||||
return "iwd"
|
|
||||||
case network.BackendNetworkd:
|
|
||||||
if result.HasIwd {
|
|
||||||
return "iwd + systemd-networkd"
|
|
||||||
}
|
|
||||||
return "systemd-networkd"
|
|
||||||
case network.BackendConnMan:
|
|
||||||
return "ConnMan"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkOptionalDependencies() []checkResult {
|
|
||||||
var results []checkResult
|
|
||||||
|
|
||||||
if utils.IsServiceActive("accounts-daemon", false) {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.IsServiceActive("power-profiles-daemon", false) {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"})
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, checkI2CAvailability())
|
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"})
|
|
||||||
} else {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"})
|
|
||||||
}
|
|
||||||
|
|
||||||
deps := []struct {
|
|
||||||
name, cmd, altCmd, desc string
|
|
||||||
important bool
|
|
||||||
}{
|
|
||||||
{"matugen", "matugen", "", "Dynamic theming", true},
|
|
||||||
{"dgop", "dgop", "", "System monitoring", true},
|
|
||||||
{"cava", "cava", "", "Audio visualizer", true},
|
|
||||||
{"khal", "khal", "", "Calendar events", false},
|
|
||||||
{"Network", "nmcli", "iwctl", "Network management", false},
|
|
||||||
{"danksearch", "dsearch", "", "File search", false},
|
|
||||||
{"loginctl", "loginctl", "", "Session management", false},
|
|
||||||
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range deps {
|
|
||||||
found, foundCmd := utils.CommandExists(d.cmd), d.cmd
|
|
||||||
if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
|
|
||||||
found, foundCmd = true, d.altCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case found:
|
|
||||||
message := "Installed"
|
|
||||||
details := d.desc
|
|
||||||
if d.name == "Network" {
|
|
||||||
result, err := network.DetectNetworkStack()
|
|
||||||
if err == nil && result.Backend != network.BackendNone {
|
|
||||||
message = detectNetworkBackend() + " (active)"
|
|
||||||
if doctorVerbose {
|
|
||||||
details = result.ChosenReason
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch foundCmd {
|
|
||||||
case "nmcli":
|
|
||||||
message = "NetworkManager (installed)"
|
|
||||||
case "iwctl":
|
|
||||||
message = "iwd (installed)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
|
|
||||||
case d.important:
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"})
|
|
||||||
default:
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkConfigurationFiles() []checkResult {
|
|
||||||
configDir, _ := os.UserConfigDir()
|
|
||||||
cacheDir, _ := os.UserCacheDir()
|
|
||||||
dmsDir := "DankMaterialShell"
|
|
||||||
|
|
||||||
configFiles := []struct{ name, path string }{
|
|
||||||
{"settings.json", filepath.Join(configDir, dmsDir, "settings.json")},
|
|
||||||
{"clsettings.json", filepath.Join(configDir, dmsDir, "clsettings.json")},
|
|
||||||
{"plugin_settings.json", filepath.Join(configDir, dmsDir, "plugin_settings.json")},
|
|
||||||
{"session.json", filepath.Join(utils.XDGStateHome(), dmsDir, "session.json")},
|
|
||||||
{"dms-colors.json", filepath.Join(cacheDir, dmsDir, "dms-colors.json")},
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []checkResult
|
|
||||||
for _, cf := range configFiles {
|
|
||||||
info, err := os.Stat(cf.path)
|
|
||||||
if err != nil {
|
|
||||||
results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path, doctorDocsURL + "#config-files"})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
status := statusOK
|
|
||||||
message := "Present"
|
|
||||||
if info.Mode().Perm()&0200 == 0 {
|
|
||||||
status = statusWarn
|
|
||||||
message += " (read-only)"
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSystemdServices() []checkResult {
|
|
||||||
if !utils.CommandExists("systemctl") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []checkResult
|
|
||||||
|
|
||||||
dmsState := getServiceState("dms", true)
|
|
||||||
if !dmsState.exists {
|
|
||||||
results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"})
|
|
||||||
} else {
|
|
||||||
status, message := statusOK, dmsState.enabled
|
|
||||||
if dmsState.active != "" {
|
|
||||||
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case dmsState.enabled == "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"})
|
|
||||||
}
|
|
||||||
|
|
||||||
greetdState := getServiceState("greetd", false)
|
|
||||||
switch {
|
|
||||||
case greetdState.exists:
|
|
||||||
status := statusOK
|
|
||||||
if greetdState.enabled == "disabled" {
|
|
||||||
status = statusInfo
|
|
||||||
}
|
|
||||||
results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, "", doctorDocsURL + "#services"})
|
|
||||||
case doctorVerbose:
|
|
||||||
results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service", doctorDocsURL + "#services"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
type serviceState struct {
|
|
||||||
exists bool
|
|
||||||
enabled string
|
|
||||||
active string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServiceState(name string, userService bool) serviceState {
|
|
||||||
args := []string{"is-enabled", name}
|
|
||||||
if userService {
|
|
||||||
args = []string{"--user", "is-enabled", name}
|
|
||||||
}
|
|
||||||
|
|
||||||
output, _ := exec.Command("systemctl", args...).Output()
|
|
||||||
enabled := strings.TrimSpace(string(output))
|
|
||||||
|
|
||||||
if enabled == "" || enabled == "not-found" {
|
|
||||||
return serviceState{}
|
|
||||||
}
|
|
||||||
|
|
||||||
state := serviceState{exists: true, enabled: enabled}
|
|
||||||
|
|
||||||
if userService {
|
|
||||||
output, _ = exec.Command("systemctl", "--user", "is-active", name).Output()
|
|
||||||
if active := strings.TrimSpace(string(output)); active != "" && active != "unknown" {
|
|
||||||
state.active = active
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
func printResults(results []checkResult) {
|
|
||||||
theme := tui.TerminalTheme()
|
|
||||||
styles := tui.NewStyles(theme)
|
|
||||||
|
|
||||||
currentCategory := category(-1)
|
|
||||||
for _, r := range results {
|
|
||||||
if r.category != currentCategory {
|
|
||||||
if currentCategory != -1 {
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
fmt.Printf(" %s\n", styles.Bold.Render(r.category.String()))
|
|
||||||
currentCategory = r.category
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
icon, style := r.status.IconStyle(styles)
|
|
||||||
|
|
||||||
name := r.name
|
|
||||||
nameLen := len(name)
|
|
||||||
|
|
||||||
if nameLen > checkNameMaxLength {
|
|
||||||
name = name[:checkNameMaxLength-1] + "…"
|
|
||||||
nameLen = checkNameMaxLength
|
|
||||||
}
|
|
||||||
dots := strings.Repeat("·", checkNameMaxLength-nameLen)
|
|
||||||
|
|
||||||
fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message)
|
|
||||||
|
|
||||||
if doctorVerbose && r.details != "" {
|
|
||||||
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printSummary(results []checkResult, qsMissingFeatures bool) {
|
|
||||||
theme := tui.TerminalTheme()
|
|
||||||
styles := tui.NewStyles(theme)
|
|
||||||
|
|
||||||
var ds DoctorStatus
|
|
||||||
for _, r := range results {
|
|
||||||
ds.Add(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf(" %s\n", styles.Subtle.Render("──────────────────────────────────────"))
|
|
||||||
|
|
||||||
if !ds.HasIssues() {
|
|
||||||
fmt.Printf(" %s\n", styles.Success.Render("✓ All checks passed!"))
|
|
||||||
} else {
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
if ds.ErrorCount() > 0 {
|
|
||||||
parts = append(parts, styles.Error.Render(fmt.Sprintf("%d error(s)", ds.ErrorCount())))
|
|
||||||
}
|
|
||||||
if ds.WarningCount() > 0 {
|
|
||||||
parts = append(parts, styles.Warning.Render(fmt.Sprintf("%d warning(s)", ds.WarningCount())))
|
|
||||||
}
|
|
||||||
parts = append(parts, styles.Success.Render(fmt.Sprintf("%d ok", ds.OKCount())))
|
|
||||||
fmt.Printf(" %s\n", strings.Join(parts, ", "))
|
|
||||||
|
|
||||||
if qsMissingFeatures {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf(" %s\n", styles.Subtle.Render("→ Consider using quickshell-git for full feature support"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,16 +32,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")
|
||||||
@@ -54,7 +49,6 @@ func init() {
|
|||||||
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
@@ -74,7 +68,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
||||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -82,14 +75,13 @@ 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,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
TerminalsAlwaysDark: terminalsAlwaysDark,
|
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||||
SkipTemplates: skipTemplates,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,10 +97,33 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
wait, _ := cmd.Flags().GetBool("wait")
|
wait, _ := cmd.Flags().GetBool("wait")
|
||||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||||
|
|
||||||
request := models.Request{
|
socketPath := os.Getenv("DMS_SOCKET")
|
||||||
ID: 1,
|
if socketPath == "" {
|
||||||
Method: "matugen.queue",
|
var err error
|
||||||
Params: map[string]any{
|
socketPath, err = server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Info("No socket available, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Socket connection failed, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
request := map[string]any{
|
||||||
|
"id": 1,
|
||||||
|
"method": "matugen.queue",
|
||||||
|
"params": map[string]any{
|
||||||
"stateDir": opts.StateDir,
|
"stateDir": opts.StateDir,
|
||||||
"shellDir": opts.ShellDir,
|
"shellDir": opts.ShellDir,
|
||||||
"configDir": opts.ConfigDir,
|
"configDir": opts.ConfigDir,
|
||||||
@@ -121,19 +136,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"stockColors": opts.StockColors,
|
"stockColors": opts.StockColors,
|
||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !wait {
|
if !wait {
|
||||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
|
||||||
log.Info("Server unavailable, running synchronously")
|
|
||||||
if err := matugen.Run(opts); err != nil {
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("Theme generation queued")
|
fmt.Println("Theme generation queued")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -143,18 +154,17 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
resultCh := make(chan error, 1)
|
resultCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
resp, ok := tryServerRequest(request)
|
var response struct {
|
||||||
if !ok {
|
ID int `json:"id"`
|
||||||
log.Info("Server unavailable, running synchronously")
|
Result any `json:"result"`
|
||||||
if err := matugen.Run(opts); err != nil {
|
Error string `json:"error"`
|
||||||
resultCh <- err
|
}
|
||||||
return
|
if err := json.NewDecoder(conn).Decode(&response); err != nil {
|
||||||
}
|
resultCh <- fmt.Errorf("failed to read response: %w", err)
|
||||||
resultCh <- nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if response.Error != "" {
|
||||||
resultCh <- fmt.Errorf("server error: %s", resp.Error)
|
resultCh <- fmt.Errorf("server error: %s", response.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultCh <- nil
|
resultCh <- nil
|
||||||
@@ -170,12 +180,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,14 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -90,6 +93,32 @@ func mimeTypeToCategories(mimeType string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runOpen(target string) {
|
func runOpen(target string) {
|
||||||
|
socketPath, err := server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket not found: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket connection failed: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
_, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if buf[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse file:// URIs to extract the actual file path
|
// Parse file:// URIs to extract the actual file path
|
||||||
actualTarget := target
|
actualTarget := target
|
||||||
detectedMimeType := openMimeType
|
detectedMimeType := openMimeType
|
||||||
@@ -131,12 +160,6 @@ func runOpen(target string) {
|
|||||||
detectedRequestType = "url"
|
detectedRequestType = "url"
|
||||||
}
|
}
|
||||||
log.Infof("Detected HTTP(S) URL")
|
log.Infof("Detected HTTP(S) URL")
|
||||||
} else if strings.HasPrefix(target, "dms://") {
|
|
||||||
// Handle DMS internal URLs (theme/plugin install, etc.)
|
|
||||||
if detectedRequestType == "" {
|
|
||||||
detectedRequestType = "url"
|
|
||||||
}
|
|
||||||
log.Infof("Detected DMS internal URL")
|
|
||||||
} else if _, err := os.Stat(target); err == nil {
|
} else if _, err := os.Stat(target); err == nil {
|
||||||
// Handle local file paths directly (not file:// URIs)
|
// Handle local file paths directly (not file:// URIs)
|
||||||
// Convert to absolute path
|
// Convert to absolute path
|
||||||
@@ -183,7 +206,7 @@ func runOpen(target string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
method := "apppicker.open"
|
method := "apppicker.open"
|
||||||
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
|
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
|
||||||
method = "browser.open"
|
method = "browser.open"
|
||||||
params["url"] = target
|
params["url"] = target
|
||||||
}
|
}
|
||||||
@@ -196,9 +219,8 @@ func runOpen(target string) {
|
|||||||
|
|
||||||
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
||||||
|
|
||||||
if err := sendServerRequestFireAndForget(req); err != nil {
|
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||||
fmt.Println("DMS is not running. Please start DMS first.")
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Request sent successfully")
|
log.Infof("Request sent successfully")
|
||||||
|
|||||||
@@ -50,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -257,7 +257,9 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return clipboard.Copy(data.Bytes(), mimeType)
|
cmd := exec.Command("wl-copy", "--type", mimeType)
|
||||||
|
cmd.Stdin = &data
|
||||||
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
|
||||||
socketPath := getServerSocketPath()
|
|
||||||
|
|
||||||
conn, err := net.Dial("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conn)
|
|
||||||
scanner.Scan() // discard initial capabilities message
|
|
||||||
|
|
||||||
reqData, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write(reqData); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write newline: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !scanner.Scan() {
|
|
||||||
return nil, fmt.Errorf("failed to read response")
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp models.Response[any]
|
|
||||||
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendServerRequestFireAndForget sends a request without waiting for a response.
|
|
||||||
// Useful for commands that trigger UI or async operations.
|
|
||||||
func sendServerRequestFireAndForget(req models.Request) error {
|
|
||||||
socketPath := getServerSocketPath()
|
|
||||||
|
|
||||||
conn, err := net.Dial("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conn)
|
|
||||||
scanner.Scan() // discard initial capabilities message
|
|
||||||
|
|
||||||
reqData, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write(reqData); err != nil {
|
|
||||||
return fmt.Errorf("failed to write request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
|
||||||
return fmt.Errorf("failed to write newline: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryServerRequest attempts to send a request but returns false if server unavailable.
|
|
||||||
// Does not log errors - caller can decide what to do on failure.
|
|
||||||
func tryServerRequest(req models.Request) (*models.Response[any], bool) {
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return resp, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServerSocketPath() string {
|
|
||||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
|
||||||
if runtimeDir == "" {
|
|
||||||
runtimeDir = os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(runtimeDir)
|
|
||||||
if err != nil {
|
|
||||||
return filepath.Join(runtimeDir, "danklinux.sock")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
name := entry.Name()
|
|
||||||
if name == "danklinux.sock" {
|
|
||||||
return filepath.Join(runtimeDir, name)
|
|
||||||
}
|
|
||||||
if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" {
|
|
||||||
return filepath.Join(runtimeDir, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return server.GetSocketPath()
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|
||||||
@@ -20,25 +18,6 @@ import (
|
|||||||
|
|
||||||
type ipcTargets map[string]map[string][]string
|
type ipcTargets map[string]map[string][]string
|
||||||
|
|
||||||
// getProcessExitCode returns the exit code from a ProcessState.
|
|
||||||
// For normal exits, returns the exit code directly.
|
|
||||||
// For signal termination, returns 128 + signal number (Unix convention).
|
|
||||||
func getProcessExitCode(state *os.ProcessState) int {
|
|
||||||
if state == nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if code := state.ExitCode(); code != -1 {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
// Process was killed by signal - extract signal number
|
|
||||||
if status, ok := state.Sys().(syscall.WaitStatus); ok {
|
|
||||||
if status.Signaled() {
|
|
||||||
return 128 + int(status.Signal())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSessionManaged bool
|
var isSessionManaged bool
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
func execDetachedRestart(targetPID int) {
|
||||||
@@ -186,10 +165,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() {
|
||||||
@@ -203,16 +180,6 @@ func runShellInteractive(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
@@ -247,28 +214,14 @@ func runShellInteractive(session bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
if sig == syscall.SIGUSR1 {
|
// Handle SIGUSR1 restart for non-session managed processes
|
||||||
if isSessionManaged {
|
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
||||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
execDetachedRestart(os.Getpid())
|
execDetachedRestart(os.Getpid())
|
||||||
|
// Exit immediately to avoid race conditions with detached restart
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
|
||||||
select {
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
@@ -282,7 +235,7 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,7 +328,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 +385,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() {
|
||||||
@@ -443,16 +400,6 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
|
||||||
}
|
|
||||||
|
|
||||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error opening /dev/null: %v", err)
|
log.Fatalf("Error opening /dev/null: %v", err)
|
||||||
@@ -493,28 +440,15 @@ func runShellDaemon(session bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
if sig == syscall.SIGUSR1 {
|
// Handle SIGUSR1 restart for non-session managed processes
|
||||||
if isSessionManaged {
|
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
||||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
execDetachedRestart(os.Getpid())
|
execDetachedRestart(os.Getpid())
|
||||||
|
// Exit immediately to avoid race conditions with detached restart
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
// All other signals: clean shutdown
|
||||||
select {
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
@@ -526,25 +460,17 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +495,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
|
||||||
|
|
||||||
@@ -627,12 +549,7 @@ func runShellIPCCommand(args []string) {
|
|||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
30
core/go.mod
30
core/go.mod
@@ -9,28 +9,26 @@ require (
|
|||||||
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-20251203232544-981d4ecc17c3
|
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
|
||||||
go.etcd.io/bbolt v1.4.3
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
|
||||||
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.6.2 // 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.3.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.2 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-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
|
||||||
@@ -38,21 +36,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.46.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/net v0.48.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.3 // 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-20251231065035-29ae690a9f19
|
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
|
||||||
@@ -66,7 +64,7 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.39.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
39
core/go.sum
39
core/go.sum
@@ -18,8 +18,6 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
||||||
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=
|
||||||
@@ -28,24 +26,18 @@ github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsy
|
|||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||||
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
|
||||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
|
||||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
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=
|
||||||
@@ -66,22 +58,15 @@ github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
|||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
|
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
|
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
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-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
|
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
|
||||||
github.com/godbus/dbus/v5 v5.2.2/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=
|
||||||
@@ -129,16 +114,12 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
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=
|
||||||
@@ -150,36 +131,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
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=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
|
||||||
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
|
||||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
|
||||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
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=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ NC='\033[0m' # No Color
|
|||||||
|
|
||||||
# Check for root privileges
|
# Check for root privileges
|
||||||
if [ "$(id -u)" == "0" ]; then
|
if [ "$(id -u)" == "0" ]; then
|
||||||
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if running on Linux
|
# Check if running on Linux
|
||||||
@@ -22,17 +22,17 @@ fi
|
|||||||
# Detect architecture
|
# Detect architecture
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
x86_64)
|
x86_64)
|
||||||
ARCH="amd64"
|
ARCH="amd64"
|
||||||
;;
|
;;
|
||||||
aarch64)
|
aarch64)
|
||||||
ARCH="arm64"
|
ARCH="arm64"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
||||||
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Get the latest release version
|
# Get the latest release version
|
||||||
@@ -55,7 +55,7 @@ curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LAT
|
|||||||
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
||||||
|
|
||||||
# Get the expected checksum
|
# Get the expected checksum
|
||||||
EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256)
|
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
|
||||||
|
|
||||||
# Calculate actual checksum
|
# Calculate actual checksum
|
||||||
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
||||||
@@ -67,7 +67,7 @@ if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
|
|||||||
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
||||||
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
||||||
printf "The downloaded file may be corrupted or tampered with\n"
|
printf "The downloaded file may be corrupted or tampered with\n"
|
||||||
cd - >/dev/null
|
cd - > /dev/null
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -82,5 +82,5 @@ printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
|||||||
./installer
|
./installer
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
cd - >/dev/null
|
cd - > /dev/null
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
|
||||||
return CopyOpts(data, mimeType, false, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
return copyServe(data, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := stdin.Write(data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyServe(data []byte, mimeType string, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := source.Offer(mimeType); err != nil {
|
|
||||||
return fmt.Errorf("offer mime type: %w", err)
|
|
||||||
}
|
|
||||||
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
|
||||||
if err := source.Offer("text/plain"); err != nil {
|
|
||||||
return fmt.Errorf("offer text/plain: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
|
||||||
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("UTF8_STRING"); err != nil {
|
|
||||||
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("STRING"); err != nil {
|
|
||||||
return fmt.Errorf("offer STRING: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("TEXT"); err != nil {
|
|
||||||
return fmt.Errorf("offer TEXT: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyText(text string) error {
|
|
||||||
return Copy([]byte(text), "text/plain;charset=utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paste() ([]byte, string, error) {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
ctx := display.Context()
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", 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 nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
|
|
||||||
if seat == nil {
|
|
||||||
return nil, "", fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
|
||||||
|
|
||||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerMimeTypes[e.Id] = nil
|
|
||||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
|
||||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var selectionOffer *ext_data_control.ExtDataControlOfferV1
|
|
||||||
gotSelection := false
|
|
||||||
|
|
||||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
|
||||||
selectionOffer = e.Id
|
|
||||||
gotSelection = true
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeTypes := offerMimeTypes[selectionOffer]
|
|
||||||
selectedMime := selectPreferredMimeType(mimeTypes)
|
|
||||||
if selectedMime == "" {
|
|
||||||
return nil, "", fmt.Errorf("no supported mime type")
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("create pipe: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
|
|
||||||
w.Close()
|
|
||||||
return nil, "", fmt.Errorf("receive: %w", err)
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("read: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, selectedMime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PasteText() (string, error) {
|
|
||||||
data, _, err := Paste()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectPreferredMimeType(mimes []string) string {
|
|
||||||
preferred := []string{
|
|
||||||
"text/plain;charset=utf-8",
|
|
||||||
"text/plain",
|
|
||||||
"UTF8_STRING",
|
|
||||||
"STRING",
|
|
||||||
"TEXT",
|
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pref := range preferred {
|
|
||||||
for _, mime := range mimes {
|
|
||||||
if mime == pref {
|
|
||||||
return mime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mimes) > 0 {
|
|
||||||
return mimes[0]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsImageMimeType(mime string) bool {
|
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
|
||||||
}
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "golang.org/x/image/bmp"
|
|
||||||
_ "golang.org/x/image/tiff"
|
|
||||||
"hash/fnv"
|
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StoreConfig struct {
|
|
||||||
MaxHistory int
|
|
||||||
MaxEntrySize int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultStoreConfig() StoreConfig {
|
|
||||||
return StoreConfig{
|
|
||||||
MaxHistory: 100,
|
|
||||||
MaxEntrySize: 5 * 1024 * 1024,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry struct {
|
|
||||||
ID uint64
|
|
||||||
Data []byte
|
|
||||||
MimeType string
|
|
||||||
Preview string
|
|
||||||
Size int
|
|
||||||
Timestamp time.Time
|
|
||||||
IsImage bool
|
|
||||||
Hash uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func Store(data []byte, mimeType string) error {
|
|
||||||
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if int64(len(data)) > cfg.MaxEntrySize {
|
|
||||||
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath, err := GetDBPath()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get db path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open db: %w", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
entry := Entry{
|
|
||||||
Data: data,
|
|
||||||
MimeType: mimeType,
|
|
||||||
Size: len(data),
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
IsImage: IsImageMimeType(mimeType),
|
|
||||||
Hash: computeHash(data),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case entry.IsImage:
|
|
||||||
entry.Preview = imagePreview(data, mimeType)
|
|
||||||
default:
|
|
||||||
entry.Preview = textPreview(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Update(func(tx *bolt.Tx) error {
|
|
||||||
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := b.NextSequence()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
entry.ID = id
|
|
||||||
|
|
||||||
encoded, err := encodeEntry(entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Put(itob(id), encoded); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimLengthInTx(b, cfg.MaxHistory)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDBPath() (string, error) {
|
|
||||||
cacheDir, err := os.UserCacheDir()
|
|
||||||
if err != nil {
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
cacheDir = filepath.Join(homeDir, ".cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
|
|
||||||
newPath := filepath.Join(newDir, "db")
|
|
||||||
|
|
||||||
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, 0700); 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, 0700); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return newPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|
||||||
c := b.Cursor()
|
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
|
||||||
if extractHash(v) != hash {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := b.Delete(k); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
|
|
||||||
c := b.Cursor()
|
|
||||||
var count int
|
|
||||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
|
||||||
if count < maxHistory {
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := b.Delete(k); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeEntry(e Entry) ([]byte, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
binary.Write(buf, binary.BigEndian, e.ID)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
|
|
||||||
buf.Write(e.Data)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
|
|
||||||
buf.WriteString(e.MimeType)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
|
|
||||||
buf.WriteString(e.Preview)
|
|
||||||
binary.Write(buf, binary.BigEndian, int32(e.Size))
|
|
||||||
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
|
|
||||||
if e.IsImage {
|
|
||||||
buf.WriteByte(1)
|
|
||||||
} else {
|
|
||||||
buf.WriteByte(0)
|
|
||||||
}
|
|
||||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func itob(v uint64) []byte {
|
|
||||||
b := make([]byte, 8)
|
|
||||||
binary.BigEndian.PutUint64(b, v)
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeHash(data []byte) uint64 {
|
|
||||||
h := fnv.New64a()
|
|
||||||
h.Write(data)
|
|
||||||
return h.Sum64()
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractHash(data []byte) uint64 {
|
|
||||||
if len(data) < 8 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func textPreview(data []byte) string {
|
|
||||||
text := string(data)
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
text = strings.Join(strings.Fields(text), " ")
|
|
||||||
|
|
||||||
if len(text) > 100 {
|
|
||||||
return text[:100] + "…"
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
func imagePreview(data []byte, format string) string {
|
|
||||||
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sizeStr(size int) string {
|
|
||||||
units := []string{"B", "KiB", "MiB"}
|
|
||||||
var i int
|
|
||||||
fsize := float64(size)
|
|
||||||
for fsize >= 1024 && i < len(units)-1 {
|
|
||||||
fsize /= 1024
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.0f %s", fsize, units[i])
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClipboardChange struct {
|
|
||||||
Data []byte
|
|
||||||
MimeType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
wlCtx := 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(wlCtx)
|
|
||||||
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(wlCtx)
|
|
||||||
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()
|
|
||||||
|
|
||||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
|
||||||
|
|
||||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerMimeTypes[e.Id] = nil
|
|
||||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
|
||||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mimes := offerMimeTypes[e.Id]
|
|
||||||
selectedMime := selectPreferredMimeType(mimes)
|
|
||||||
if selectedMime == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
|
||||||
w.Close()
|
|
||||||
r.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer r.Close()
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback(data, selectedMime)
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
|
||||||
}
|
|
||||||
if err := wlCtx.Dispatch(); err != nil && err != os.ErrDeadlineExceeded {
|
|
||||||
return fmt.Errorf("dispatch: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
|
||||||
ch := make(chan ClipboardChange, 16)
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(ch)
|
|
||||||
err := Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
select {
|
|
||||||
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil && err != context.Canceled {
|
|
||||||
errCh <- err
|
|
||||||
}
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
return ch, errCh
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -209,17 +209,10 @@ 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 to preserve user modifications
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); 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)
|
||||||
}
|
}
|
||||||
@@ -272,13 +265,7 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
colorResult := DeploymentResult{
|
colorResult := DeploymentResult{
|
||||||
ConfigType: "Ghostty Colors",
|
ConfigType: "Ghostty Colors",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "themes", "dankcolors"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
|
||||||
}
|
|
||||||
|
|
||||||
themesDir := filepath.Dir(colorResult.Path)
|
|
||||||
if err := os.MkdirAll(themesDir, 0755); err != nil {
|
|
||||||
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
||||||
@@ -423,31 +410,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()), 0644); 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 +435,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
|
||||||
@@ -484,12 +465,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
|
||||||
if err := os.MkdirAll(dmsDir, 0755); 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")
|
||||||
@@ -529,7 +504,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 {
|
||||||
@@ -543,44 +518,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
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)
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); 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)
|
||||||
|
|
||||||
@@ -588,20 +532,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()), 0644); 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, "")
|
||||||
|
|
||||||
@@ -685,11 +615,10 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
|
|||||||
|
|
||||||
spawnDms := `spawn-at-startup "dms" "run"`
|
spawnDms := `spawn-at-startup "dms" "run"`
|
||||||
if !strings.Contains(config, spawnDms) {
|
if !strings.Contains(config, spawnDms) {
|
||||||
// Insert spawn-at-startup for dms after the environment block
|
config = strings.Replace(config,
|
||||||
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
|
||||||
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
|
||||||
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
|
1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 = ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -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,13 +459,16 @@ 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) {
|
||||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||||
assert.Contains(t, GhosttyConfig, "theme = dankcolors")
|
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyColorConfigStructure(t *testing.T) {
|
func TestGhosttyColorConfigStructure(t *testing.T) {
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LocateDMSConfig() (string, error) {
|
func LocateDMSConfig() (string, error) {
|
||||||
var primaryPaths []string
|
var primaryPaths []string
|
||||||
|
|
||||||
configHome, err := os.UserConfigDir()
|
configHome := utils.XDGConfigHome()
|
||||||
if err == nil && configHome != "" {
|
if configHome != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +23,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 +35,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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ keybind = shift+enter=text:\n
|
|||||||
gtk-single-instance = true
|
gtk-single-instance = true
|
||||||
|
|
||||||
# Dank color generation
|
# Dank color generation
|
||||||
theme = dankcolors
|
config-file = ./config-dankcolors
|
||||||
|
|||||||
@@ -1,156 +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
|
|
||||||
|
|
||||||
# === 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
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ monitor = , preferred,auto,auto
|
|||||||
# ==================
|
# ==================
|
||||||
exec-once = dbus-update-activation-environment --systemd --all
|
exec-once = dbus-update-activation-environment --systemd --all
|
||||||
exec-once = systemctl --user start hyprland-session.target
|
exec-once = systemctl --user start hyprland-session.target
|
||||||
|
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# INPUT CONFIG
|
# INPUT CONFIG
|
||||||
@@ -27,7 +28,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 +43,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
|
||||||
@@ -87,32 +91,190 @@ misc {
|
|||||||
# ==================
|
# ==================
|
||||||
# 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,8 +1,3 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
highlight {
|
highlight {
|
||||||
corner-radius 12
|
corner-radius 12
|
||||||
|
|||||||
@@ -15,8 +15,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 +46,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 {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
// ! Auto-generated file. Do not edit directly.
|
|
||||||
// Remove `include "dms/colors.kdl"` from your config to override.
|
|
||||||
|
|
||||||
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 +18,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
gaps 4
|
gaps 4
|
||||||
|
|
||||||
|
|||||||
@@ -18,64 +18,15 @@ gestures {
|
|||||||
input {
|
input {
|
||||||
keyboard {
|
keyboard {
|
||||||
xkb {
|
xkb {
|
||||||
// You can set rules, model, layout, variant and options.
|
|
||||||
// For more information, see xkeyboard-config(7).
|
|
||||||
|
|
||||||
// For example:
|
|
||||||
// layout "us,ru"
|
|
||||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
|
||||||
|
|
||||||
// If this section is empty, niri will fetch xkb settings
|
|
||||||
// from org.freedesktop.locale1. You can control these using
|
|
||||||
// localectl set-x11-keymap.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable numlock on startup, omitting this setting disables it.
|
|
||||||
numlock
|
numlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next sections include libinput settings.
|
|
||||||
// Omitting settings disables them, or leaves them at their default values.
|
|
||||||
// All commented-out settings here are examples, not defaults.
|
|
||||||
touchpad {
|
touchpad {
|
||||||
// off
|
|
||||||
tap
|
|
||||||
// dwt
|
|
||||||
// dwtp
|
|
||||||
// drag false
|
|
||||||
// drag-lock
|
|
||||||
natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "two-finger"
|
|
||||||
// disabled-on-external-mouse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse {
|
mouse {
|
||||||
// off
|
|
||||||
// natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "no-scroll"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackpoint {
|
trackpoint {
|
||||||
// off
|
|
||||||
// natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "on-button-down"
|
|
||||||
// scroll-button 273
|
|
||||||
// scroll-button-lock
|
|
||||||
// middle-emulation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
|
||||||
// warp-mouse-to-focus
|
|
||||||
|
|
||||||
// Focus windows and outputs automatically when moving the mouse into them.
|
|
||||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
|
||||||
// focus-follows-mouse max-scroll-amount="0%"
|
|
||||||
}
|
}
|
||||||
// You can configure outputs by their name, which you can find
|
// You can configure outputs by their name, which you can find
|
||||||
// by running `niri msg outputs` while inside a niri instance.
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
@@ -158,6 +109,7 @@ overview {
|
|||||||
// which may be more convenient to use.
|
// which may be more convenient to use.
|
||||||
// See the binds section below for more spawn examples.
|
// See the binds section below for more spawn examples.
|
||||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
|
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||||
environment {
|
environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
}
|
}
|
||||||
@@ -240,6 +192,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 +225,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
|
|
||||||
|
|||||||
@@ -345,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 {
|
||||||
|
|||||||
@@ -658,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++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,24 +112,3 @@ func GenerateWeztermTheme(p Palette) string {
|
|||||||
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateNeovimTheme(p Palette) string {
|
|
||||||
var result strings.Builder
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_0 = \"%s\"\n", p.Color0.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_1 = \"%s\"\n", p.Color1.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_2 = \"%s\"\n", p.Color2.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_3 = \"%s\"\n", p.Color3.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_4 = \"%s\"\n", p.Color4.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_5 = \"%s\"\n", p.Color5.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_6 = \"%s\"\n", p.Color6.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_7 = \"%s\"\n", p.Color7.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_8 = \"%s\"\n", p.Color8.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_9 = \"%s\"\n", p.Color9.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_10 = \"%s\"\n", p.Color10.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_11 = \"%s\"\n", p.Color11.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_12 = \"%s\"\n", p.Color12.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_13 = \"%s\"\n", p.Color13.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_14 = \"%s\"\n", p.Color14.Hex)
|
|
||||||
fmt.Fprintf(&result, "vim.g.terminal_color_15 = \"%s\"\n", p.Color15.Hex)
|
|
||||||
return result.String()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -104,8 +103,10 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
|
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -138,6 +139,8 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"ghostty": {Name: "ghostty", 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},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
}
|
}
|
||||||
@@ -515,9 +518,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)
|
||||||
@@ -543,7 +549,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() {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -185,6 +186,37 @@ func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
cliphist := deps.StatusMissing
|
||||||
|
if b.commandExists("cliphist") {
|
||||||
|
cliphist = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
wlClipboard := deps.StatusMissing
|
||||||
|
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||||
|
wlClipboard = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies,
|
||||||
|
deps.Dependency{
|
||||||
|
Name: "cliphist",
|
||||||
|
Status: cliphist,
|
||||||
|
Description: "Wayland clipboard manager",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
deps.Dependency{
|
||||||
|
Name: "wl-clipboard",
|
||||||
|
Status: wlClipboard,
|
||||||
|
Description: "Wayland clipboard utilities",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
var dependencies []deps.Dependency
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
@@ -550,9 +582,20 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
|||||||
terminalCmd = "ghostty"
|
terminalCmd = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
|
// ! This deviates from master branch so it doesnt need a hotfix
|
||||||
|
var content string
|
||||||
|
if utils.CommandExists("plasmashell") || utils.CommandExists("plasma-session") || utils.CommandExists("plasma_session") {
|
||||||
|
content = fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
TERMINAL=%s
|
TERMINAL=%s
|
||||||
`, terminalCmd)
|
`, terminalCmd)
|
||||||
|
} else {
|
||||||
|
content = fmt.Sprintf(`QT_QPA_PLATFORM=wayland
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
|
QT_QPA_PLATFORMTHEME=gtk3
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6=gtk3
|
||||||
|
TERMINAL=%s
|
||||||
|
`, terminalCmd)
|
||||||
|
}
|
||||||
|
|
||||||
envFile := filepath.Join(envDir, "90-dms.conf")
|
envFile := filepath.Join(envDir, "90-dms.conf")
|
||||||
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -70,6 +69,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, d.detectMatugen())
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
dependencies = append(dependencies, d.detectDgop())
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
|
dependencies = append(dependencies, d.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -102,6 +102,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
"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"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,8 +387,6 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
debianVersion := "Debian_13"
|
debianVersion := "Debian_13"
|
||||||
if osInfo.VersionID == "testing" {
|
if osInfo.VersionID == "testing" {
|
||||||
debianVersion = "Debian_Testing"
|
debianVersion = "Debian_Testing"
|
||||||
} else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") {
|
|
||||||
debianVersion = "Debian_Unstable"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pkg := range obsPkgs {
|
for _, pkg := range obsPkgs {
|
||||||
@@ -430,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -549,7 +549,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,10 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
|
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -115,12 +117,14 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
}
|
}
|
||||||
@@ -153,7 +157,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 {
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
|
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -139,6 +140,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
|
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
@@ -149,6 +151,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
|
|
||||||
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
|
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -74,6 +74,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install hyprland: %w", err)
|
return fmt.Errorf("failed to install hyprland: %w", err)
|
||||||
}
|
}
|
||||||
|
case "hyprpicker":
|
||||||
|
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
|
}
|
||||||
case "ghostty":
|
case "ghostty":
|
||||||
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
@@ -82,6 +86,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install matugen: %w", err)
|
return fmt.Errorf("failed to install matugen: %w", err)
|
||||||
}
|
}
|
||||||
|
case "cliphist":
|
||||||
|
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||||
|
}
|
||||||
case "xwayland-satellite":
|
case "xwayland-satellite":
|
||||||
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||||
@@ -103,12 +111,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 +168,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 +245,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 +281,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 +351,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)
|
||||||
@@ -397,6 +405,184 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing hyprpicker from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install hyprutils first
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Building hyprutils dependency...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
|
||||||
|
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create hyprutils directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(hyprutilsDir)
|
||||||
|
|
||||||
|
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
|
||||||
|
if err := cloneUtilsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"--no-warn-unused-cli",
|
||||||
|
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
||||||
|
"-DBUILD_TESTING=off",
|
||||||
|
"-S", ".",
|
||||||
|
"-B", "./build")
|
||||||
|
configureUtilsCmd.Dir = hyprutilsDir
|
||||||
|
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
|
||||||
|
buildUtilsCmd.Dir = hyprutilsDir
|
||||||
|
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
|
installUtilsCmd.Dir = hyprutilsDir
|
||||||
|
if err := installUtilsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install hyprwayland-scanner
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.2,
|
||||||
|
Step: "Building hyprwayland-scanner dependency...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
|
||||||
|
if err := os.MkdirAll(scannerDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create scanner directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(scannerDir)
|
||||||
|
|
||||||
|
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
|
||||||
|
if err := cloneScannerCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureScannerCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX=/usr",
|
||||||
|
"-B", "build")
|
||||||
|
configureScannerCmd.Dir = scannerDir
|
||||||
|
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
|
||||||
|
buildScannerCmd.Dir = scannerDir
|
||||||
|
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
|
installScannerCmd.Dir = scannerDir
|
||||||
|
if err := installScannerCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now build hyprpicker
|
||||||
|
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: "Cloning hyprpicker repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.45,
|
||||||
|
Step: "Configuring hyprpicker build...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
|
||||||
|
}
|
||||||
|
|
||||||
|
configureCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"--no-warn-unused-cli",
|
||||||
|
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
||||||
|
"-S", ".",
|
||||||
|
"-B", "./build")
|
||||||
|
configureCmd.Dir = tmpDir
|
||||||
|
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
|
||||||
|
output, err := configureCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||||
|
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.55,
|
||||||
|
Step: "Building hyprpicker...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake --build build --target hyprpicker",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing hyprpicker...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cmake --install build",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("hyprpicker installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing Ghostty from source...")
|
m.log("Installing Ghostty from source...")
|
||||||
|
|
||||||
@@ -406,12 +592,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 +714,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +803,52 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing cliphist from source...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Installing cliphist via go install...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
||||||
|
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
||||||
|
targetPath := "/usr/local/bin/cliphist"
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.7,
|
||||||
|
Step: "Installing cliphist binary to system...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("cliphist installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing xwayland-satellite from source...")
|
m.log("Installing xwayland-satellite from source...")
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ func init() {
|
|||||||
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewOpenSUSEDistribution(config, logChan)
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
Register("opensuse-leap", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
|
||||||
return NewOpenSUSEDistribution(config, logChan)
|
|
||||||
})
|
|
||||||
Register("opensuse-slowroll", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
|
||||||
return NewOpenSUSEDistribution(config, logChan)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenSUSEDistribution struct {
|
type OpenSUSEDistribution struct {
|
||||||
@@ -84,8 +78,10 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
|
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -111,8 +107,10 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"ghostty": {Name: "ghostty", 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},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
@@ -440,19 +438,6 @@ func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []
|
|||||||
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
osInfo, err := GetOSInfo()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get OS info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
obsDistroVersion := "openSUSE_Tumbleweed"
|
|
||||||
switch osInfo.Distribution.ID {
|
|
||||||
case "opensuse-leap":
|
|
||||||
obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID)
|
|
||||||
case "opensuse-slowroll":
|
|
||||||
obsDistroVersion = "openSUSE_Slowroll"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkg := range obsPkgs {
|
for _, pkg := range obsPkgs {
|
||||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
@@ -460,8 +445,8 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
// RepoURL format: "home:AvengeMedia:danklinux"
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s/%s.repo",
|
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
|
||||||
repoPath, obsDistroVersion, pkg.RepoURL)
|
repoPath, pkg.RepoURL)
|
||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
||||||
if checkCmd.Run() == nil {
|
if checkCmd.Run() == nil {
|
||||||
|
|||||||
@@ -19,12 +19,11 @@ type DistroInfo struct {
|
|||||||
|
|
||||||
// OSInfo contains complete OS information
|
// OSInfo contains complete OS information
|
||||||
type OSInfo struct {
|
type OSInfo struct {
|
||||||
Distribution DistroInfo
|
Distribution DistroInfo
|
||||||
Version string
|
Version string
|
||||||
VersionID string
|
VersionID string
|
||||||
VersionCodename string
|
PrettyName string
|
||||||
PrettyName string
|
Architecture string
|
||||||
Architecture string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOSInfo detects the current OS and returns information about it
|
// GetOSInfo detects the current OS and returns information about it
|
||||||
@@ -73,8 +72,6 @@ func GetOSInfo() (*OSInfo, error) {
|
|||||||
info.VersionID = value
|
info.VersionID = value
|
||||||
case "VERSION":
|
case "VERSION":
|
||||||
info.Version = value
|
info.Version = value
|
||||||
case "VERSION_CODENAME":
|
|
||||||
info.VersionCodename = value
|
|
||||||
case "PRETTY_NAME":
|
case "PRETTY_NAME":
|
||||||
info.PrettyName = value
|
info.PrettyName = value
|
||||||
}
|
}
|
||||||
@@ -103,10 +100,6 @@ func IsUnsupportedDistro(distroID, versionID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if distroID == "debian" {
|
if distroID == "debian" {
|
||||||
// unstable/sid support
|
|
||||||
if versionID == "sid" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if versionID == "" {
|
if versionID == "" {
|
||||||
// debian testing/sid have no version ID
|
// debian testing/sid have no version ID
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -76,8 +76,10 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
|
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -110,6 +112,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
@@ -118,6 +121,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +539,8 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
buildDeps["libpam0g-dev"] = true
|
buildDeps["libpam0g-dev"] = true
|
||||||
case "matugen":
|
case "matugen":
|
||||||
buildDeps["curl"] = true
|
buildDeps["curl"] = true
|
||||||
|
case "cliphist":
|
||||||
|
// Go will be installed separately with PPA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +550,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
|||||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||||
case "niri":
|
case "niri":
|
||||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||||
case "kitty", "alacritty", "ghostty":
|
case "kitty", "alacritty", "ghostty", "hyprpicker":
|
||||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||||
default:
|
default:
|
||||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ type DiscoveryConfig struct {
|
|||||||
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
||||||
var searchPaths []string
|
var searchPaths []string
|
||||||
|
|
||||||
configDir, err := os.UserConfigDir()
|
configHome := utils.XDGConfigHome()
|
||||||
if err == nil && configDir != "" {
|
if configHome != "" {
|
||||||
searchPaths = append(searchPaths, filepath.Join(configDir, "DankMaterialShell", "cheatsheets"))
|
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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), 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
existingBinds, err := h.loadOverrideBinds()
|
|
||||||
if err != nil {
|
|
||||||
existingBinds = make(map[string]*hyprlandOverrideBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract flags from options
|
|
||||||
var flags string
|
|
||||||
if options != nil {
|
|
||||||
if f, ok := options["flags"].(string); ok {
|
|
||||||
flags = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
|
||||||
existingBinds[normalizedKey] = &hyprlandOverrideBind{
|
|
||||||
Key: key,
|
|
||||||
Action: action,
|
|
||||||
Description: description,
|
|
||||||
Flags: flags,
|
|
||||||
Options: options,
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.writeOverrideBinds(existingBinds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) RemoveBind(key string) error {
|
|
||||||
existingBinds, err := h.loadOverrideBinds()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
|
||||||
delete(existingBinds, normalizedKey)
|
|
||||||
return h.writeOverrideBinds(existingBinds)
|
|
||||||
}
|
|
||||||
|
|
||||||
type hyprlandOverrideBind struct {
|
|
||||||
Key string
|
|
||||||
Action string
|
|
||||||
Description string
|
|
||||||
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
|
||||||
Options map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
|
||||||
overridePath := h.GetOverridePath()
|
|
||||||
binds := make(map[string]*hyprlandOverrideBind)
|
|
||||||
|
|
||||||
data, err := os.ReadFile(overridePath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(line, "bind") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract flags from bind type
|
|
||||||
bindType := strings.TrimSpace(parts[0])
|
|
||||||
flags := extractBindFlags(bindType)
|
|
||||||
hasDescFlag := strings.Contains(flags, "d")
|
|
||||||
|
|
||||||
content := strings.TrimSpace(parts[1])
|
|
||||||
commentParts := strings.SplitN(content, "#", 2)
|
|
||||||
bindContent := strings.TrimSpace(commentParts[0])
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(commentParts) > 1 {
|
|
||||||
comment = strings.TrimSpace(commentParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// For bindd, format is: mods, key, description, dispatcher, params
|
|
||||||
var minFields, descIndex, dispatcherIndex int
|
|
||||||
if hasDescFlag {
|
|
||||||
minFields = 4
|
|
||||||
descIndex = 2
|
|
||||||
dispatcherIndex = 3
|
|
||||||
} else {
|
|
||||||
minFields = 3
|
|
||||||
dispatcherIndex = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.SplitN(bindContent, ",", minFields+2)
|
|
||||||
if len(fields) < minFields {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(fields[0])
|
|
||||||
keyName := strings.TrimSpace(fields[1])
|
|
||||||
|
|
||||||
var dispatcher, params string
|
|
||||||
if hasDescFlag {
|
|
||||||
if comment == "" {
|
|
||||||
comment = strings.TrimSpace(fields[descIndex])
|
|
||||||
}
|
|
||||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
|
||||||
if len(fields) > dispatcherIndex+1 {
|
|
||||||
paramParts := fields[dispatcherIndex+1:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
|
||||||
if len(fields) > dispatcherIndex+1 {
|
|
||||||
paramParts := fields[dispatcherIndex+1:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStr := h.buildKeyString(mods, keyName)
|
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
|
||||||
action := dispatcher
|
|
||||||
if params != "" {
|
|
||||||
action = dispatcher + " " + params
|
|
||||||
}
|
|
||||||
|
|
||||||
binds[normalizedKey] = &hyprlandOverrideBind{
|
|
||||||
Key: keyStr,
|
|
||||||
Action: action,
|
|
||||||
Description: comment,
|
|
||||||
Flags: flags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
|
|
||||||
if mods == "" {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
|
||||||
return r == '+' || r == ' '
|
|
||||||
})
|
|
||||||
|
|
||||||
parts := append(modList, key)
|
|
||||||
return strings.Join(parts, "+")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"):
|
|
||||||
return 0
|
|
||||||
case strings.Contains(action, "workspace"):
|
|
||||||
return 1
|
|
||||||
case strings.Contains(action, "window") || strings.Contains(action, "focus") ||
|
|
||||||
strings.Contains(action, "move") || strings.Contains(action, "swap") ||
|
|
||||||
strings.Contains(action, "resize"):
|
|
||||||
return 2
|
|
||||||
case strings.Contains(action, "monitor"):
|
|
||||||
return 3
|
|
||||||
case strings.HasPrefix(action, "exec"):
|
|
||||||
return 4
|
|
||||||
case action == "exit" || strings.Contains(action, "dpms"):
|
|
||||||
return 5
|
|
||||||
default:
|
|
||||||
return 6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
|
||||||
overridePath := h.GetOverridePath()
|
|
||||||
content := h.generateBindsContent(binds)
|
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
|
|
||||||
if len(binds) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
bindList := make([]*hyprlandOverrideBind, 0, len(binds))
|
|
||||||
for _, bind := range binds {
|
|
||||||
bindList = append(bindList, bind)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(bindList, func(i, j int) bool {
|
|
||||||
pi, pj := h.getBindSortPriority(bindList[i].Action), h.getBindSortPriority(bindList[j].Action)
|
|
||||||
if pi != pj {
|
|
||||||
return pi < pj
|
|
||||||
}
|
|
||||||
return bindList[i].Key < bindList[j].Key
|
|
||||||
})
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
for _, bind := range bindList {
|
|
||||||
h.writeBindLine(&sb, bind)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
|
||||||
mods, key := h.parseKeyString(bind.Key)
|
|
||||||
dispatcher, params := h.parseAction(bind.Action)
|
|
||||||
|
|
||||||
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
|
||||||
sb.WriteString("bind")
|
|
||||||
if bind.Flags != "" {
|
|
||||||
sb.WriteString(bind.Flags)
|
|
||||||
}
|
|
||||||
sb.WriteString(" = ")
|
|
||||||
sb.WriteString(mods)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(key)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
|
|
||||||
// For bindd (description flag), include description before dispatcher
|
|
||||||
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString(dispatcher)
|
|
||||||
|
|
||||||
if params != "" {
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only add comment if not using bindd (which has inline description)
|
|
||||||
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
|
|
||||||
sb.WriteString(" # ")
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
|
|
||||||
parts := strings.Split(keyStr, "+")
|
|
||||||
switch len(parts) {
|
|
||||||
case 0:
|
|
||||||
return "", keyStr
|
|
||||||
case 1:
|
|
||||||
return "", parts[0]
|
|
||||||
default:
|
|
||||||
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
|
|
||||||
parts := strings.SplitN(action, " ", 2)
|
|
||||||
switch len(parts) {
|
|
||||||
case 0:
|
|
||||||
return action, ""
|
|
||||||
case 1:
|
|
||||||
dispatcher = parts[0]
|
|
||||||
default:
|
|
||||||
dispatcher = parts[0]
|
|
||||||
params = parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert internal spawn format to Hyprland's exec
|
|
||||||
if dispatcher == "spawn" {
|
|
||||||
dispatcher = "exec"
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatcher, params
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,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)
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewHyprlandParser("")
|
parser := NewHyprlandParser()
|
||||||
if err := parser.ReadContent(tmpDir); err != nil {
|
if err := parser.ReadContent(tmpDir); err != nil {
|
||||||
t.Fatalf("ReadContent failed: %v", err)
|
t.Fatalf("ReadContent failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -343,7 +343,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
t.Skip("Cannot create relative path")
|
t.Skip("Cannot create relative path")
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewHyprlandParser("")
|
parser := NewHyprlandParser()
|
||||||
tildePathMatch := "~/" + relPath
|
tildePathMatch := "~/" + relPath
|
||||||
err = parser.ReadContent(tildePathMatch)
|
err = parser.ReadContent(tildePathMatch)
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
|
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
|
||||||
parser := NewHyprlandParser("")
|
parser := NewHyprlandParser()
|
||||||
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
|
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
|
||||||
|
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
@@ -394,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) {
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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), 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
existingBinds, err := m.loadOverrideBinds()
|
|
||||||
if err != nil {
|
|
||||||
existingBinds = make(map[string]*mangowcOverrideBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
|
||||||
existingBinds[normalizedKey] = &mangowcOverrideBind{
|
|
||||||
Key: key,
|
|
||||||
Action: action,
|
|
||||||
Description: description,
|
|
||||||
Options: options,
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.writeOverrideBinds(existingBinds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) RemoveBind(key string) error {
|
|
||||||
existingBinds, err := m.loadOverrideBinds()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
|
||||||
delete(existingBinds, normalizedKey)
|
|
||||||
return m.writeOverrideBinds(existingBinds)
|
|
||||||
}
|
|
||||||
|
|
||||||
type mangowcOverrideBind struct {
|
|
||||||
Key string
|
|
||||||
Action string
|
|
||||||
Description string
|
|
||||||
Options map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
|
|
||||||
overridePath := m.GetOverridePath()
|
|
||||||
binds := make(map[string]*mangowcOverrideBind)
|
|
||||||
|
|
||||||
data, err := os.ReadFile(overridePath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(line, "bind") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
content := strings.TrimSpace(parts[1])
|
|
||||||
commentParts := strings.SplitN(content, "#", 2)
|
|
||||||
bindContent := strings.TrimSpace(commentParts[0])
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(commentParts) > 1 {
|
|
||||||
comment = strings.TrimSpace(commentParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.SplitN(bindContent, ",", 4)
|
|
||||||
if len(fields) < 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(fields[0])
|
|
||||||
keyName := strings.TrimSpace(fields[1])
|
|
||||||
command := strings.TrimSpace(fields[2])
|
|
||||||
|
|
||||||
var params string
|
|
||||||
if len(fields) > 3 {
|
|
||||||
params = strings.TrimSpace(fields[3])
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStr := m.buildKeyString(mods, keyName)
|
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
|
||||||
action := command
|
|
||||||
if params != "" {
|
|
||||||
action = command + " " + params
|
|
||||||
}
|
|
||||||
|
|
||||||
binds[normalizedKey] = &mangowcOverrideBind{
|
|
||||||
Key: keyStr,
|
|
||||||
Action: action,
|
|
||||||
Description: comment,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
|
|
||||||
if mods == "" || strings.EqualFold(mods, "none") {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
|
||||||
return r == '+' || r == ' '
|
|
||||||
})
|
|
||||||
|
|
||||||
parts := append(modList, key)
|
|
||||||
return strings.Join(parts, "+")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
|
|
||||||
return 0
|
|
||||||
case strings.Contains(action, "view") || strings.Contains(action, "tag"):
|
|
||||||
return 1
|
|
||||||
case strings.Contains(action, "focus") || strings.Contains(action, "exchange") ||
|
|
||||||
strings.Contains(action, "resize") || strings.Contains(action, "move"):
|
|
||||||
return 2
|
|
||||||
case strings.Contains(action, "mon"):
|
|
||||||
return 3
|
|
||||||
case strings.HasPrefix(action, "spawn"):
|
|
||||||
return 4
|
|
||||||
case action == "quit" || action == "reload_config":
|
|
||||||
return 5
|
|
||||||
default:
|
|
||||||
return 6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
|
||||||
overridePath := m.GetOverridePath()
|
|
||||||
content := m.generateBindsContent(binds)
|
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
|
||||||
if len(binds) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
bindList := make([]*mangowcOverrideBind, 0, len(binds))
|
|
||||||
for _, bind := range binds {
|
|
||||||
bindList = append(bindList, bind)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(bindList, func(i, j int) bool {
|
|
||||||
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
|
|
||||||
if pi != pj {
|
|
||||||
return pi < pj
|
|
||||||
}
|
|
||||||
return bindList[i].Key < bindList[j].Key
|
|
||||||
})
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
for _, bind := range bindList {
|
|
||||||
m.writeBindLine(&sb, bind)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
|
|
||||||
mods, key := m.parseKeyString(bind.Key)
|
|
||||||
command, params := m.parseAction(bind.Action)
|
|
||||||
|
|
||||||
sb.WriteString("bind=")
|
|
||||||
if mods == "" {
|
|
||||||
sb.WriteString("none")
|
|
||||||
} else {
|
|
||||||
sb.WriteString(mods)
|
|
||||||
}
|
|
||||||
sb.WriteString(",")
|
|
||||||
sb.WriteString(key)
|
|
||||||
sb.WriteString(",")
|
|
||||||
sb.WriteString(command)
|
|
||||||
|
|
||||||
if params != "" {
|
|
||||||
sb.WriteString(",")
|
|
||||||
sb.WriteString(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
if bind.Description != "" {
|
|
||||||
sb.WriteString(" # ")
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
|
|
||||||
parts := strings.Split(keyStr, "+")
|
|
||||||
switch len(parts) {
|
|
||||||
case 0:
|
|
||||||
return "", keyStr
|
|
||||||
case 1:
|
|
||||||
return "", parts[0]
|
|
||||||
default:
|
|
||||||
return strings.Join(parts[:len(parts)-1], "+"), parts[len(parts)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MangoWCProvider) parseAction(action string) (command, params string) {
|
|
||||||
parts := strings.SplitN(action, " ", 2)
|
|
||||||
switch len(parts) {
|
|
||||||
case 0:
|
|
||||||
return action, ""
|
|
||||||
case 1:
|
|
||||||
return parts[0], ""
|
|
||||||
default:
|
|
||||||
return parts[0], parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,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,320 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := sourcePath
|
|
||||||
if !filepath.IsAbs(sourcePath) {
|
|
||||||
fullPath = filepath.Join(baseDir, sourcePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
expanded, err := utils.ExpandPath(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
includedBinds, err := p.parseFileWithSource(expanded)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*keybinds = append(*keybinds, includedBinds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
|
||||||
data, err := os.ReadFile(dmsBindsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
prevSource := p.currentSource
|
|
||||||
p.currentSource = dmsBindsPath
|
|
||||||
|
|
||||||
var keybinds []MangoWCKeyBinding
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
|
|
||||||
for lineNum, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if !strings.HasPrefix(trimmed, "bind") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
kb := p.getKeybindAtLineContent(line, lineNum)
|
|
||||||
if kb == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kb.Source = dmsBindsPath
|
|
||||||
p.addBind(kb)
|
|
||||||
keybinds = append(keybinds, *kb)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.currentSource = prevSource
|
|
||||||
p.dmsProcessed = true
|
|
||||||
return keybinds
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyBinding {
|
|
||||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
|
||||||
matches := bindMatch.FindStringSubmatch(line)
|
|
||||||
if len(matches) < 3 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
content := matches[2]
|
|
||||||
parts := strings.SplitN(content, "#", 2)
|
|
||||||
keys := parts[0]
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(parts) > 1 {
|
|
||||||
comment = strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(comment, MangoWCHideComment) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
keyFields := strings.SplitN(keys, ",", 4)
|
|
||||||
if len(keyFields) < 3 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(keyFields[0])
|
|
||||||
key := strings.TrimSpace(keyFields[1])
|
|
||||||
command := strings.TrimSpace(keyFields[2])
|
|
||||||
|
|
||||||
var params string
|
|
||||||
if len(keyFields) > 3 {
|
|
||||||
params = strings.TrimSpace(keyFields[3])
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment == "" {
|
|
||||||
comment = mangowcAutogenerateComment(command, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
var modList []string
|
|
||||||
if mods != "" && !strings.EqualFold(mods, "none") {
|
|
||||||
modstring := mods + string(MangoWCModSeparators[0])
|
|
||||||
idx := 0
|
|
||||||
for index, char := range modstring {
|
|
||||||
isModSep := false
|
|
||||||
for _, sep := range MangoWCModSeparators {
|
|
||||||
if char == sep {
|
|
||||||
isModSep = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isModSep {
|
|
||||||
if index-idx > 1 {
|
|
||||||
modList = append(modList, modstring[idx:index])
|
|
||||||
}
|
|
||||||
idx = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MangoWCKeyBinding{
|
|
||||||
Mods: modList,
|
|
||||||
Key: key,
|
|
||||||
Command: command,
|
|
||||||
Params: params,
|
|
||||||
Comment: comment,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseMangoWCKeysWithDMS(path string) (*MangoWCParseResult, error) {
|
|
||||||
parser := NewMangoWCParser(path)
|
|
||||||
keybinds, err := parser.ParseWithDMS()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MangoWCParseResult{
|
|
||||||
Keybinds: keybinds,
|
|
||||||
DMSBindsIncluded: parser.dmsBindsIncluded,
|
|
||||||
DMSStatus: parser.buildDMSStatus(),
|
|
||||||
ConflictingConfigs: parser.conflictingConfigs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parser := NewMangoWCParser("")
|
parser := NewMangoWCParser()
|
||||||
parser.contentLines = []string{tt.line}
|
parser.contentLines = []string{tt.line}
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewMangoWCParser("")
|
parser := NewMangoWCParser()
|
||||||
if err := parser.ReadContent(tmpDir); err != nil {
|
if err := parser.ReadContent(tmpDir); err != nil {
|
||||||
t.Fatalf("ReadContent failed: %v", err)
|
t.Fatalf("ReadContent failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -304,7 +304,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write config: %v", err)
|
t.Fatalf("Failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewMangoWCParser("")
|
parser := NewMangoWCParser()
|
||||||
if err := parser.ReadContent(configFile); err != nil {
|
if err := parser.ReadContent(configFile); err != nil {
|
||||||
t.Fatalf("ReadContent failed: %v", err)
|
t.Fatalf("ReadContent failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
t.Skip("Cannot create relative path")
|
t.Skip("Cannot create relative path")
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := NewMangoWCParser("")
|
parser := NewMangoWCParser()
|
||||||
tildePathMatch := "~/" + relPath
|
tildePathMatch := "~/" + relPath
|
||||||
err = parser.ReadContent(tildePathMatch)
|
err = parser.ReadContent(tildePathMatch)
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ func TestMangoWCInvalidBindLines(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parser := NewMangoWCParser("")
|
parser := NewMangoWCParser()
|
||||||
parser.contentLines = []string{tt.line}
|
parser.contentLines = []string{tt.line}
|
||||||
result := parser.getKeybindAtLine(0)
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
|
|||||||
@@ -15,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
@@ -30,11 +31,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultNiriConfigDir() string {
|
func defaultNiriConfigDir() string {
|
||||||
configDir, err := os.UserConfigDir()
|
return filepath.Join(utils.XDGConfigHome(), "niri")
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return filepath.Join(configDir, "niri")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) Name() string {
|
func (n *NiriProvider) Name() string {
|
||||||
@@ -187,15 +184,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 {
|
||||||
@@ -301,15 +290,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 +302,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)
|
||||||
|
|||||||
@@ -121,8 +121,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"},
|
||||||
@@ -326,58 +324,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, 0755); err != nil {
|
|
||||||
t.Fatalf("Failed to create dms directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
|
||||||
if err := os.WriteFile(bindsFile, []byte(content), 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to write binds file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testProvider := NewNiriProvider(tmpDir)
|
|
||||||
loadedBinds, err := testProvider.loadOverrideBinds()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load binds: %v\nContent was:\n%s", err, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, expected := range binds {
|
|
||||||
loaded, ok := loadedBinds[key]
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("Missing bind for key %s", key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if loaded.Action != expected.Action {
|
|
||||||
t.Errorf("Action mismatch for %s:\n got: %q\n want: %q", key, loaded.Action, expected.Action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|||||||
@@ -8,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,63 +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},
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -84,15 +27,13 @@ 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
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
SkipTemplates string
|
|
||||||
AppChecker utils.AppChecker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColorsOutput struct {
|
type ColorsOutput struct {
|
||||||
@@ -106,18 +47,6 @@ func (o *Options) ColorsOutput() string {
|
|||||||
return filepath.Join(o.StateDir, "dms-colors.json")
|
return filepath.Join(o.StateDir, "dms-colors.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) ShouldSkipTemplate(name string) bool {
|
|
||||||
if o.SkipTemplates == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, skip := range strings.Split(o.SkipTemplates, ",") {
|
|
||||||
if strings.TrimSpace(skip) == name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(opts Options) error {
|
func Run(opts Options) error {
|
||||||
if opts.StateDir == "" {
|
if opts.StateDir == "" {
|
||||||
return fmt.Errorf("state-dir is required")
|
return fmt.Errorf("state-dir is required")
|
||||||
@@ -135,7 +64,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"
|
||||||
@@ -143,9 +72,6 @@ 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, 0755); 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)
|
||||||
@@ -206,7 +132,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
|
||||||
@@ -242,7 +168,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
|
||||||
@@ -281,7 +207,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")
|
||||||
}
|
}
|
||||||
@@ -292,34 +218,35 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
switch opts.Mode {
|
||||||
for _, tmpl := range templateRegistry {
|
case "light":
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
||||||
continue
|
default:
|
||||||
}
|
appendConfig(opts, cfgFile, "skip", "gtk3-dark.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:
|
|
||||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
||||||
|
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
||||||
|
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
||||||
|
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
||||||
|
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
||||||
|
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
||||||
|
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
||||||
|
|
||||||
|
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]", "")
|
||||||
@@ -346,34 +273,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)
|
||||||
@@ -384,7 +305,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
|
||||||
}
|
}
|
||||||
@@ -422,32 +343,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'
|
||||||
@@ -467,12 +370,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 {
|
||||||
@@ -604,19 +503,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)
|
||||||
@@ -642,7 +541,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() {
|
||||||
@@ -672,9 +571,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"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,52 +581,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, 0755); 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), 0644); 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, 0755); 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), 0644); 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, 0755); 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), 0644); 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, 0755); 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), 0644); 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, 0755); 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), 0644); 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, 0755); 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), 0644); 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, 0755); 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), 0644); 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, 0755); 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,229 +0,0 @@
|
|||||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
|
||||||
|
|
||||||
package mocks_wlclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockWaylandDisplay is an autogenerated mock type for the WaylandDisplay type
|
|
||||||
type MockWaylandDisplay struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockWaylandDisplay_Expecter struct {
|
|
||||||
mock *mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_m *MockWaylandDisplay) EXPECT() *MockWaylandDisplay_Expecter {
|
|
||||||
return &MockWaylandDisplay_Expecter{mock: &_m.Mock}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) Context() *client.Context {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Context")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 *client.Context
|
|
||||||
if rf, ok := ret.Get(0).(func() *client.Context); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*client.Context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context'
|
|
||||||
type MockWaylandDisplay_Context_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) Context() *MockWaylandDisplay_Context_Call {
|
|
||||||
return &MockWaylandDisplay_Context_Call{Call: _e.mock.On("Context")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Context_Call) Run(run func()) *MockWaylandDisplay_Context_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Context_Call) Return(_a0 *client.Context) *MockWaylandDisplay_Context_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Context_Call) RunAndReturn(run func() *client.Context) *MockWaylandDisplay_Context_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) Destroy() error {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Destroy")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func() error); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_Destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Destroy'
|
|
||||||
type MockWaylandDisplay_Destroy_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) Destroy() *MockWaylandDisplay_Destroy_Call {
|
|
||||||
return &MockWaylandDisplay_Destroy_Call{Call: _e.mock.On("Destroy")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Destroy_Call) Run(run func()) *MockWaylandDisplay_Destroy_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Destroy_Call) Return(_a0 error) *MockWaylandDisplay_Destroy_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Destroy_Call) RunAndReturn(run func() error) *MockWaylandDisplay_Destroy_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistry provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) GetRegistry() (*client.Registry, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for GetRegistry")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 *client.Registry
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(0).(func() (*client.Registry, error)); ok {
|
|
||||||
return rf()
|
|
||||||
}
|
|
||||||
if rf, ok := ret.Get(0).(func() *client.Registry); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*client.Registry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_GetRegistry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRegistry'
|
|
||||||
type MockWaylandDisplay_GetRegistry_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistry is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) GetRegistry() *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
return &MockWaylandDisplay_GetRegistry_Call{Call: _e.mock.On("GetRegistry")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_GetRegistry_Call) Run(run func()) *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_GetRegistry_Call) Return(_a0 *client.Registry, _a1 error) *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
_c.Call.Return(_a0, _a1)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_GetRegistry_Call) RunAndReturn(run func() (*client.Registry, error)) *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roundtrip provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) Roundtrip() error {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Roundtrip")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func() error); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_Roundtrip_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Roundtrip'
|
|
||||||
type MockWaylandDisplay_Roundtrip_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roundtrip is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) Roundtrip() *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
return &MockWaylandDisplay_Roundtrip_Call{Call: _e.mock.On("Roundtrip")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Roundtrip_Call) Run(run func()) *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Roundtrip_Call) Return(_a0 error) *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Roundtrip_Call) RunAndReturn(run func() error) *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockWaylandDisplay creates a new instance of MockWaylandDisplay. 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 NewMockWaylandDisplay(t interface {
|
|
||||||
mock.TestingT
|
|
||||||
Cleanup(func())
|
|
||||||
}) *MockWaylandDisplay {
|
|
||||||
mock := &MockWaylandDisplay{}
|
|
||||||
mock.Mock.Test(t)
|
|
||||||
|
|
||||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user