1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-12 23:32:50 -04:00

Compare commits

...

79 Commits

Author SHA1 Message Date
bbedward 7fb358bada v1.0.2 2025-12-12 10:18:54 -05:00
bbedward 73cf3130e1 ci: disable pkg builds from main release wf 2025-12-12 10:18:31 -05:00
bbedward 119b5df6df gamma: fix initial night mode enablement 2025-12-12 10:15:38 -05:00
bbedward 8ede810d32 settings: make default height screen-aware 2025-12-12 10:14:33 -05:00
bbedward 830dd93af5 chore: bump version to v1.0.1 2025-12-12 10:04:47 -05:00
bbedward 75f28c5ea7 ci: switch to dispatch-based release flow 2025-12-12 10:04:20 -05:00
bbedward 6c9b8c590e dankinstall: call add-wants for niri/hyprland with dms service 2025-12-12 10:04:20 -05:00
bbedward 24d9b77307 niri: fix keybind handling of cooldown-ms parameter 2025-12-12 10:04:20 -05:00
bbedward d4be68912c workspaces: make icons scale with bar size, fixi valign of numbers fixes #990 2025-12-12 10:04:20 -05:00
bbedward a443721000 core: fix socket reported CLI version 2025-12-12 10:03:32 -05:00
bbedward 786b097187 plugins: hide uninstall and update buttons for system plugins 2025-12-12 10:03:18 -05:00
bbedward 8ca60c7d2a dwl: fix layout popout not opening fixes #980 2025-12-12 10:03:04 -05:00
bbedward 406dc64aba wf: disable update-versions job 2025-12-10 10:50:47 -05:00
dms-ci[bot] af5d6a2015 chore: bump version to v1.0.0 2025-12-10 15:43:36 +00:00
bbedward 61c6f509ae i18n: update translations 2025-12-10 09:32:57 -05:00
Marcus Ramberg 98769ecd88 nix: switch to standard nixpkgs rfc formatting (#962) 2025-12-10 04:55:45 -03:00
bbedward 8615950bd6 cc: allow 75 width sliders 2025-12-10 00:48:27 -05:00
bbedward 1bec8dfc48 vpn: make import modal floating variant 2025-12-10 00:30:45 -05:00
bbedward 460486fe25 media: fix media player updates 2025-12-09 23:59:04 -05:00
bbedward 318c50bc6c media: block scrolling media volume in widget when no player vol avail 2025-12-09 23:45:01 -05:00
purian23 3e08bac7f3 distros: Prep dms-git build versioning 2025-12-09 23:25:34 -05:00
bbedward c3d64ab185 scrollwm: fix keybind provider registration 2025-12-09 20:14:07 -05:00
bbedward 2b73077b50 cc: add small disk usage variant
fixes #958
2025-12-09 16:09:13 -05:00
bbedward f953bd5488 i18n: update translations 2025-12-09 16:01:05 -05:00
Varshit f94011cf05 feat: add scroll compositor support (#959)
* added scroll support

* import QuickShell.i3

* update scroll provider registration logic

* improve scroll support for workspace switcher

* update title for scroll keybinds

* add scroll to dms-greeter

* fix: formatting & sway keybind provider

* readme update

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-09 15:57:46 -05:00
bbedward aeacf109eb core: add slices, paths, exec utils 2025-12-09 15:34:13 -05:00
purian23 e307de83e2 packages: Update manual changelogs 2025-12-09 14:17:53 -05:00
bbedward 85968ec417 core/server: refactory to use shared params/request structs 2025-12-09 14:13:20 -05:00
bbedward 993f14a31f widgets: make dank icon picker a popup 2025-12-09 13:41:12 -05:00
purian23 566d617508 Re-adjust systemd debian/ubuntu 2025-12-09 13:40:59 -05:00
purian23 542a279fcb Add systemd debian/ubuntu packages 2025-12-09 12:39:56 -05:00
purian23 e784bb89e1 Version lock dms fedora/opensuse packages 2025-12-09 12:39:21 -05:00
bbedward f680ace258 keybinds: fix dms args for some commands, some XF86 mappings 2025-12-09 12:21:20 -05:00
bbedward 7aa5976e07 media: fix padding issues with long titles 2025-12-09 11:46:50 -05:00
bbedward f88f1ea951 gamma: display automation state in UI 2025-12-09 11:26:28 -05:00
bbedward da4561cb35 keybinds: support more keys, allow Super+Alt 2025-12-09 10:41:39 -05:00
bbedward 1f89ae9813 popout: fix sizing on older QT 2025-12-09 09:57:31 -05:00
bbedward 5647323449 gamma: switch to wlsunset-style transitions 2025-12-09 09:44:16 -05:00
Karsten Zeides bc27253cbf fix(README): fixes documentation link to include trailing slash (#920)
fixes same issue as described in AvengeMedia/DankLinux-Docs#25
2025-12-09 08:13:33 +01:00
Lucas 0672b711f3 nix: fix greeter custom theme (#954) 2025-12-09 07:14:13 +01:00
bbedward ed9ee6e347 gamma: fix transition on enable 2025-12-09 00:46:49 -05:00
bbedward 7ad23ad4a2 gamma: fix night mode toggling 2025-12-09 00:35:52 -05:00
bbedward 8a83f03cc1 keybinds: fix provider loading via IPC 2025-12-09 00:30:14 -05:00
bbedward 0be9ac4097 keybinds: fix cheatsheet on non niri
- separate read only logic from writeread
2025-12-09 00:03:39 -05:00
bbedward ba5be6b516 wallpaper: cleanup transitions 2025-12-08 23:53:50 -05:00
bbedward c4aea6d326 themes: dont handle custom themes in onCompleted
- Defer entirley to FileView
2025-12-08 23:44:04 -05:00
bbedward 858c6407a9 dankinstall: ;remove keyring file on debian 2025-12-08 23:37:13 -05:00
bbedward c4313395b5 dankinstall: use gpg batch for deb 2025-12-08 23:36:14 -05:00
bbedward a32aec3d59 dankinstall: fix other debian sudo cmd 2025-12-08 23:31:08 -05:00
bbedward 696bcfe8fa dankinstall: fix deb sudo command 2025-12-08 23:30:03 -05:00
bbedward 2f3a253c6a wallpaper: fix per-monitor wallpaper in dash 2025-12-08 23:25:02 -05:00
bbedward e41fbe0188 misc: change transmission icon override 2025-12-08 23:11:17 -05:00
bbedward ef9d28597b dankinstall: don't fail suse if addrepo fails 2025-12-08 23:03:46 -05:00
bbedward 6f3c4c89ab keybinds: show fallback as action 2025-12-08 22:18:40 -05:00
bbedward 60c577a61e core: hyprland session on all distros, dms setup systemd prompt 2025-12-08 22:04:04 -05:00
bbedward f3276c3039 notification: fix closing popout from escape
fixes #953
2025-12-08 20:46:22 -05:00
bbedward 37a843323d dankisntall: add hyprland session target, disable hyprland-git variant
universally
2025-12-08 20:40:13 -05:00
bbedward 95c780ca8c Revert "dankinstall: remove systemd path for Hyprland"
This reverts commit 0435a805c7.
2025-12-08 20:24:58 -05:00
bbedward d60d5b154a dankinstall: switch to yalter/niri copr 2025-12-08 20:04:48 -05:00
bbedward 0435a805c7 dankinstall: remove systemd path for Hyprland 2025-12-08 19:48:07 -05:00
bbedward f406a977e0 Revert "dankinstall: update hyprland syntax"
This reverts commit 54b253099d.
2025-12-08 19:35:05 -05:00
bbedward 18db1e1ecb dankinstall: update postinstall message 2025-12-08 19:13:32 -05:00
bbedward 6bd1beb719 dankinstall: pin arch to quickshell-git 2025-12-08 19:05:29 -05:00
bbedward 1293aecbca dankinstall: nuke polkit 2025-12-08 19:03:11 -05:00
Marcus Ramberg 8a10c2e112 nixos: fix fprintd unlock (#952)
* nixos: fix fprintd unlock

* ci: this workflow doesn't need a token
2025-12-08 19:14:51 -03:00
bbedward c21d777269 screenshot: flip bits for RGB888 2025-12-08 15:38:49 -05:00
bbedward d864094f48 screenshot/colorpicker: handle 24-bit frames from compositor 2025-12-08 14:56:01 -05:00
bbedward deaac3fdf0 list: approve mouse detection 2025-12-08 14:11:44 -05:00
bbedward b7062fe40c windows: dont close on esc
fixes #911
2025-12-08 14:02:58 -05:00
bbedward 64d5e99b9d dock: ensure creation after bars
fixes #919
2025-12-08 13:54:44 -05:00
bbedward f9d8a7d22b greeter: fix weather setting
fixes #921
2025-12-08 13:45:26 -05:00
bbedward 52fcd3ad98 lock: make VPN icon white to be consistent with others
fixes #926
2025-12-08 13:24:53 -05:00
bbedward 9d1e0ee29b fix color picker color space 2025-12-08 12:59:24 -05:00
bbedward de62f48f50 screenshot: handle transformed displays 2025-12-08 12:45:05 -05:00
bbedward f47b19274c media: fix position/bar awareness
- shift media control column so it doesnt go off screen
fixes #942
2025-12-08 11:51:40 -05:00
bbedward bb7f7083b9 meta: transparency fixes
- fixes #949 - transparency not working > 95%
- fixes #947 - dont apply opacity to windows, defer to window-rules
2025-12-08 11:43:29 -05:00
Yuxiang Qiu cd580090dc evdev: improve capslock detection for no led device (#923)
* evdev: improve capslock detection for no led device

* style: fmt
2025-12-08 11:16:43 -05:00
Marcus Ramberg ddb74b598d ci: add flake check (#951) 2025-12-08 11:15:35 -05:00
bbedward 29571fc3aa screenshot: use wlr-output-management on DWL for x/y offsets 2025-12-08 10:53:08 -05:00
197 changed files with 12646 additions and 10473 deletions
+23
View File
@@ -0,0 +1,23 @@
name: Check nix flake
on:
pull_request:
branches: [master, main]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Check the flake
run: nix flake check
+294 -303
View File
@@ -1,16 +1,19 @@
name: Release name: Release
on: on:
push: workflow_dispatch:
tags: inputs:
- 'v*' tag:
description: "Tag to release (e.g., v1.0.1)"
required: true
type: string
permissions: permissions:
contents: write contents: write
actions: write actions: write
concurrency: concurrency:
group: release-${{ github.ref_name }} group: release-${{ inputs.tag }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -24,10 +27,14 @@ jobs:
run: run:
working-directory: core working-directory: core
env:
TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@@ -54,7 +61,7 @@ jobs:
run: | run: |
set -eux set -eux
cd cmd/dankinstall cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \ go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }} -o ../../dankinstall-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }} gzip -9 -k dankinstall-${{ matrix.arch }}
@@ -68,7 +75,7 @@ jobs:
run: | run: |
set -eux set -eux
cd cmd/dms cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \ go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }} -o ../../dms-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dms-${{ matrix.arch }} gzip -9 -k dms-${{ matrix.arch }}
@@ -91,7 +98,7 @@ jobs:
run: | run: |
set -eux set -eux
cd cmd/dms cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \ go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }} -o ../../dms-distropkg-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }} gzip -9 -k dms-distropkg-${{ matrix.arch }}
@@ -128,60 +135,61 @@ jobs:
core/completion.zsh core/completion.zsh
if-no-files-found: error if-no-files-found: error
update-versions: # update-versions:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
needs: build-core # needs: build-core
steps: # steps:
- name: Create GitHub App token # - name: Create GitHub App token
id: app_token # id: app_token
uses: actions/create-github-app-token@v1 # uses: actions/create-github-app-token@v1
with: # with:
app-id: ${{ secrets.APP_ID }} # app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} # private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout # - name: Checkout
uses: actions/checkout@v4 # uses: actions/checkout@v4
with: # with:
token: ${{ steps.app_token.outputs.token }} # token: ${{ steps.app_token.outputs.token }}
fetch-depth: 0 # fetch-depth: 0
- name: Update VERSION # - name: Update VERSION
env: # env:
GH_TOKEN: ${{ steps.app_token.outputs.token }} # GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: | # run: |
set -euo pipefail # set -euo pipefail
git config user.name "dms-ci[bot]" # git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" # git config user.email "dms-ci[bot]@users.noreply.github.com"
version="${GITHUB_REF#refs/tags/}" # version="${GITHUB_REF#refs/tags/}"
echo "Updating to version: $version" # echo "Updating to version: $version"
echo "${version}" > quickshell/VERSION # echo "${version}" > quickshell/VERSION
git add quickshell/VERSION # git add quickshell/VERSION
if ! git diff --cached --quiet; then # if ! git diff --cached --quiet; then
git commit -m "chore: bump version to $version" # git commit -m "chore: bump version to $version"
git pull --rebase origin master # git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master # git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
fi # fi
git tag -f "${version}" # git tag -f "${version}"
git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}" # git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
release: release:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: [build-core, update-versions] needs: [build-core] #, update-versions]
env: env:
TAG: ${{ github.ref_name }} TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Fetch updated tag after version bump - name: Fetch updated tag after version bump
run: | run: |
git fetch origin --force tag ${{ github.ref_name }} git fetch origin --force tag ${TAG}
git checkout ${{ github.ref_name }} git checkout ${TAG}
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -388,313 +396,296 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trigger-obs-update: # trigger-obs-update:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
needs: release # needs: release
steps: # env:
- name: Checkout # TAG: ${{ inputs.tag }}
uses: actions/checkout@v4 # steps:
# - name: Checkout
- name: Install OSC # uses: actions/checkout@v4
run: | # with:
sudo apt-get update # ref: ${{ inputs.tag }}
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: |
VERSION="${{ github.ref_name }}"
cd distro
bash scripts/obs-upload.sh dms "Update to $VERSION"
trigger-ppa-update: # - name: Install OSC
runs-on: ubuntu-latest # run: |
needs: release # sudo apt-get update
steps: # sudo apt-get install -y osc
- name: Checkout
uses: actions/checkout@v4
- 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: |
VERSION="${{ github.ref_name }}"
cd distro/ubuntu/ppa
bash create-and-upload.sh ../dms dms questing
copr-build: # mkdir -p ~/.config/osc
runs-on: ubuntu-latest # cat > ~/.config/osc/oscrc << EOF
needs: release # [general]
env: # apiurl = https://api.opensuse.org
TAG: ${{ github.ref_name }}
steps: # [https://api.opensuse.org]
- name: Checkout repository # user = ${{ secrets.OBS_USERNAME }}
uses: actions/checkout@v4 # pass = ${{ secrets.OBS_PASSWORD }}
# EOF
# chmod 600 ~/.config/osc/oscrc
- name: Determine version # - name: Update OBS packages
id: version # run: |
run: | # cd distro
VERSION="${TAG#v}" # bash scripts/obs-upload.sh dms "Update to ${TAG}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building DMS stable version: $VERSION"
- name: Setup build environment # trigger-ppa-update:
run: | # runs-on: ubuntu-latest
sudo apt-get update # needs: release
sudo apt-get install -y rpm wget curl jq gzip # env:
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} # TAG: ${{ inputs.tag }}
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# with:
# ref: ${{ inputs.tag }}
- name: Download release assets # - name: Install build dependencies
run: | # run: |
VERSION="${{ steps.version.outputs.version }}" # sudo apt-get update
cd ~/rpmbuild/SOURCES # sudo apt-get install -y \
# debhelper \
# devscripts \
# dput \
# lftp \
# build-essential \
# fakeroot \
# dpkg-dev
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || { # - name: Configure GPG
echo "Failed to download dms-qml.tar.gz for v${VERSION}" # env:
exit 1 # 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: Generate stable spec file # - name: Upload to PPA
run: | # run: |
VERSION="${{ steps.version.outputs.version }}" # cd distro/ubuntu/ppa
CHANGELOG_DATE="$(date '+%a %b %d %Y')" # bash create-and-upload.sh ../dms dms questing
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF' # copr-build:
# Spec for DMS stable releases - Generated by GitHub Actions # runs-on: ubuntu-latest
# needs: release
# env:
# TAG: ${{ inputs.tag }}
%global debug_package %{nil} # steps:
%global version VERSION_PLACEHOLDER # - name: Checkout repository
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors # uses: actions/checkout@v4
# with:
# ref: ${{ inputs.tag }}
Name: dms # - name: Determine version
Version: %{version} # id: version
Release: 1%{?dist} # run: |
Summary: %{pkg_summary} # VERSION="${TAG#v}"
# echo "version=$VERSION" >> $GITHUB_OUTPUT
# echo "Building DMS stable version: $VERSION"
License: MIT # - name: Setup build environment
URL: https://github.com/AvengeMedia/DankMaterialShell # run: |
# sudo apt-get update
# sudo apt-get install -y rpm wget curl jq gzip
# mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
Source0: dms-qml.tar.gz # - name: Download release assets
# run: |
# VERSION="${{ steps.version.outputs.version }}"
# cd ~/rpmbuild/SOURCES
BuildRequires: gzip # wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
BuildRequires: wget # echo "Failed to download dms-qml.tar.gz for v${VERSION}"
BuildRequires: systemd-rpm-macros # exit 1
# }
Requires: (quickshell or quickshell-git) # - name: Generate stable spec file
Requires: accountsservice # run: |
Requires: dms-cli # VERSION="${{ steps.version.outputs.version }}"
Requires: dgop # CHANGELOG_DATE="$(date '+%a %b %d %Y')"
Recommends: cava # cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
Recommends: cliphist # # Spec for DMS stable releases - Generated by GitHub Actions
Recommends: danksearch
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description # %global debug_package %{nil}
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell # %global version VERSION_PLACEHOLDER
and optimized for the niri and hyprland compositors. Features notifications, # %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets, # Name: dms
process monitoring, notification center, clipboard history, dock, control center, # Version: %{version}
lock screen, and comprehensive plugin system. # Release: 1%{?dist}
# Summary: %{pkg_summary}
%package -n dms-cli # License: MIT
Summary: DankMaterialShell CLI tool # URL: https://github.com/AvengeMedia/DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli # Source0: dms-qml.tar.gz
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop # BuildRequires: gzip
Summary: Stateless CPU/GPU monitor for DankMaterialShell # BuildRequires: wget
License: MIT # BuildRequires: systemd-rpm-macros
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop # Requires: (quickshell or quickshell-git)
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and # Requires: accountsservice
network statistics. Designed for integration with DankMaterialShell but can be # Requires: dms-cli = %{version}-%{release}
used standalone. This package always includes the latest stable dgop release. # Requires: dgop
%prep # Recommends: cava
%setup -q -c -n dms-qml # Recommends: cliphist
# Recommends: danksearch
# Recommends: matugen
# Recommends: wl-clipboard
# Recommends: NetworkManager
# Recommends: qt6-qtmultimedia
# Suggests: qt6ct
# Download architecture-specific binaries during build # %description
case "%{_arch}" in # DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
x86_64) # and optimized for the niri and hyprland compositors. Features notifications,
ARCH_SUFFIX="amd64" # app launcher, wallpaper customization, and fully customizable with plugins.
;;
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" || { # Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
echo "Failed to download dms-cli for architecture %{_arch}" # process monitoring, notification center, clipboard history, dock, control center,
exit 1 # lock screen, and comprehensive plugin system.
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || { # %package -n dms-cli
echo "Failed to download dgop for architecture %{_arch}" # Summary: DankMaterialShell CLI tool
exit 1 # License: MIT
} # URL: https://github.com/AvengeMedia/DankMaterialShell
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build # %description -n dms-cli
# Command-line interface for DankMaterialShell configuration and management.
# Provides native DBus bindings, NetworkManager integration, and system utilities.
%install # %prep
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms # %setup -q -c -n dms-qml
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
install -d %{buildroot}%{_datadir}/bash-completion/completions # # Download architecture-specific binaries during build
install -d %{buildroot}%{_datadir}/zsh/site-functions # case "%{_arch}" in
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d # x86_64)
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || : # ARCH_SUFFIX="amd64"
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || : # ;;
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : # aarch64)
# ARCH_SUFFIX="arm64"
# ;;
# *)
# echo "Unsupported architecture: %{_arch}"
# exit 1
# ;;
# esac
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service # 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
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop # %build
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms # %install
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/ # install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git* # install -d %{buildroot}%{_datadir}/bash-completion/completions
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore # install -d %{buildroot}%{_datadir}/zsh/site-functions
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github # install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro # %{_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 || :
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION # install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
%posttrans # install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then # install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
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
if [ "$1" -ge 2 ]; then # install -dm755 %{buildroot}%{_datadir}/quickshell/dms
pkill -USR1 -x dms >/dev/null 2>&1 || true # cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
fi
%files # rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
%license LICENSE # rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
%doc README.md CONTRIBUTING.md # rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
%{_datadir}/quickshell/dms/ # rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli # echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop # %posttrans
%{_bindir}/dgop # 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 || :
%changelog # %files
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1 # %license LICENSE
- Stable release VERSION_PLACEHOLDER # %doc README.md CONTRIBUTING.md
- Built from GitHub release # %{_datadir}/quickshell/dms/
- Includes latest dms-cli and dgop binaries # %{_userunitdir}/dms.service
SPECEOF # %{_datadir}/applications/dms-open.desktop
# %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec # %files -n dms-cli
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec # %{_bindir}/dms
# %{_datadir}/bash-completion/completions/dms
# %{_datadir}/zsh/site-functions/_dms
# %{_datadir}/fish/vendor_completions.d/dms.fish
- name: Build SRPM # %changelog
id: build # * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
run: | # - Stable release VERSION_PLACEHOLDER
cd ~/rpmbuild/SPECS # - Built from GitHub release
rpmbuild -bs dms.spec # SPECEOF
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1) # sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
SRPM_NAME=$(basename "$SRPM") # sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT # - name: Build SRPM
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT # id: build
echo "SRPM built: $SRPM_NAME" # run: |
# cd ~/rpmbuild/SPECS
# rpmbuild -bs dms.spec
- name: Upload SRPM artifact # SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
uses: actions/upload-artifact@v4 # SRPM_NAME=$(basename "$SRPM")
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI # echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
run: | # echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
sudo apt-get install -y python3-pip # echo "SRPM built: $SRPM_NAME"
pip3 install copr-cli
mkdir -p ~/.config # - name: Upload SRPM artifact
cat > ~/.config/copr << EOF # uses: actions/upload-artifact@v4
[copr-cli] # with:
login = ${{ secrets.COPR_LOGIN }} # name: dms-stable-srpm-${{ steps.version.outputs.version }}
username = avengemedia # path: ${{ steps.build.outputs.srpm_path }}
token = ${{ secrets.COPR_TOKEN }} # retention-days: 90
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
- name: Upload to Copr # - name: Install Copr CLI
run: | # run: |
SRPM="${{ steps.build.outputs.srpm_path }}" # sudo apt-get install -y python3-pip
VERSION="${{ steps.version.outputs.version }}" # pip3 install copr-cli
echo "Uploading SRPM to avengemedia/dms..." # mkdir -p ~/.config
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1) # cat > ~/.config/copr << EOF
echo "$BUILD_OUTPUT" # [copr-cli]
# login = ${{ secrets.COPR_LOGIN }}
# username = avengemedia
# token = ${{ secrets.COPR_TOKEN }}
# copr_url = https://copr.fedorainfracloud.org
# EOF
# chmod 600 ~/.config/copr
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown") # - name: Upload to Copr
# run: |
# SRPM="${{ steps.build.outputs.srpm_path }}"
# VERSION="${{ steps.version.outputs.version }}"
if [ "$BUILD_ID" != "unknown" ]; then # echo "Uploading SRPM to avengemedia/dms..."
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/" # BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
fi # 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
+4 -31
View File
@@ -62,7 +62,7 @@ jobs:
} }
echo "✅ Source downloaded" echo "✅ Source downloaded"
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture" echo "Note: dms-cli binary will be downloaded during build based on target architecture"
ls -lh ls -lh
- name: Generate stable spec file - name: Generate stable spec file
@@ -94,7 +94,7 @@ jobs:
Requires: (quickshell or quickshell-git) Requires: (quickshell or quickshell-git)
Requires: accountsservice Requires: accountsservice
Requires: dms-cli Requires: dms-cli = %{version}-%{release}
Requires: dgop Requires: dgop
Recommends: cava Recommends: cava
@@ -125,17 +125,6 @@ jobs:
Command-line interface for DankMaterialShell configuration and management. Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities. Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep %prep
%setup -q -c -n dms-qml %setup -q -c -n dms-qml
@@ -162,19 +151,10 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli chmod +x %{_builddir}/dms-cli
# Download dgop for target architecture
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build %build
%install %install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Shell completions # Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions install -d %{buildroot}%{_datadir}/bash-completion/completions
@@ -202,11 +182,8 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi fi
# Signal running DMS instances to reload (harmless if none running)
# Restart DMS for active users after upgrade pkill -USR1 -x dms >/dev/null 2>&1 || :
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
@@ -220,14 +197,10 @@ jobs:
%{_datadir}/zsh/site-functions/_dms %{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish %{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER - Stable release VERSION_PLACEHOLDER
- Built from GitHub release - Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
+9 -6
View File
@@ -5,21 +5,21 @@
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200"> <img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
</a> </a>
### A modern desktop shell for Wayland ### A modern desktop shell for Wayland
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/) Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs) [![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs)
[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers) [![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE) [![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases) [![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin) [![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git) [![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux) [![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux)
</div> </div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure ## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features. Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
## Documentation ## Documentation
- **Website:** [danklinux.com](https://danklinux.com) - **Website:** [danklinux.com](https://danklinux.com)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs) - **Docs:** [danklinux.com/docs](https://danklinux.com/docs/)
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes) - **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview) - **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc) - **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
@@ -143,6 +143,7 @@ See component-specific documentation:
### Building from Source ### Building from Source
**Core + Dankinstall:** **Core + Dankinstall:**
```bash ```bash
cd core cd core
make # Build dms CLI make # Build dms CLI
@@ -150,11 +151,13 @@ make dankinstall # Build installer
``` ```
**Shell:** **Shell:**
```bash ```bash
quickshell -p quickshell/ quickshell -p quickshell/
``` ```
**NixOS:** **NixOS:**
```nix ```nix
{ {
inputs.dms.url = "github:AvengeMedia/DankMaterialShell"; inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
-1
View File
@@ -1 +0,0 @@
indentation = "FourSpaces"
+2
View File
@@ -22,6 +22,8 @@ linters:
- (*os.Process).Signal - (*os.Process).Signal
- (*os.Process).Kill - (*os.Process).Kill
- syscall.Kill - syscall.Kill
# Seek on memfd (reset position before passing fd)
- syscall.Seek
# DBus cleanup # DBus cleanup
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal - (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal - (*github.com/godbus/dbus/v5.Conn).RemoveSignal
+5
View File
@@ -12,6 +12,11 @@ import (
var Version = "dev" var Version = "dev"
func main() { func main() {
if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1)
}
fileLogger, err := log.NewFileLogger() fileLogger, err := log.NewFileLogger()
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err) fmt.Printf("Warning: Failed to create log file: %v\n", err)
+33 -34
View File
@@ -211,45 +211,13 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
exponential, _ := cmd.Flags().GetBool("exponential") exponential, _ := cmd.Flags().GetBool("exponential")
exponent, _ := cmd.Flags().GetFloat64("exponent") exponent, _ := cmd.Flags().GetFloat64("exponent")
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
parts := strings.SplitN(deviceID, ":", 2) parts := strings.SplitN(deviceID, ":", 2)
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") { if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
subsystem := parts[0] if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok {
name := parts[1] return
// Initialize backends needed for logind approach
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
} else {
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
} else {
defer logind.Close()
// Get device info to convert percent to value
dev, err := sysfs.GetDevice(deviceID)
if err == nil {
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
// Call logind with hardware value
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return
} else {
log.Debugf("logind.SetBrightness failed: %v", err)
}
} else {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
}
}
} }
} }
// Fallback to direct sysfs (requires write permissions)
sysfs, err := brightness.NewSysfsBackend() sysfs, err := brightness.NewSysfsBackend()
if err == nil { if err == nil {
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil { if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
@@ -280,6 +248,37 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to set brightness for device: %s", deviceID) log.Fatalf("Failed to set brightness for device: %s", deviceID)
} }
func tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool {
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
return false
}
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
return false
}
defer logind.Close()
dev, err := sysfs.GetDevice(deviceID)
if err != nil {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
return false
}
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil {
log.Debugf("logind.SetBrightness failed: %v", err)
return false
}
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return true
}
func getBrightnessDevices(includeDDC bool) []string { func getBrightnessDevices(includeDDC bool) []string {
allDevices := getAllBrightnessDevices(includeDDC) allDevices := getAllBrightnessDevices(includeDDC)
+68 -29
View File
@@ -152,6 +152,24 @@ var pluginsUninstallCmd = &cobra.Command{
}, },
} }
var pluginsUpdateCmd = &cobra.Command{
Use: "update <plugin-id>",
Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) { func runVersion(cmd *cobra.Command, args []string) {
printASCII() printASCII()
fmt.Printf("%s\n", formatVersion(Version)) fmt.Printf("%s\n", formatVersion(Version))
@@ -408,49 +426,70 @@ func uninstallPluginCLI(idOrName string) error {
return fmt.Errorf("failed to create registry: %w", err) return fmt.Errorf("failed to create registry: %w", err)
} }
pluginList, err := registry.List() pluginList, _ := registry.List()
if err != nil { plugin := plugins.FindByIDOrName(idOrName, pluginList)
return fmt.Errorf("failed to list plugins: %w", err)
}
// First, try to find by ID (preferred method) if plugin != nil {
var plugin *plugins.Plugin installed, err := manager.IsInstalled(*plugin)
for _, p := range pluginList { if err != nil {
if p.ID == idOrName { return fmt.Errorf("failed to check install status: %w", err)
plugin = &p
break
} }
} if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
// Fallback to name for backward compatibility
if plugin == nil {
for _, p := range pluginList {
if p.Name == idOrName {
plugin = &p
break
}
} }
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err)
}
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
return nil
} }
if plugin == nil { fmt.Printf("Uninstalling plugin: %s\n", idOrName)
return fmt.Errorf("plugin not found: %s", idOrName) if err := manager.UninstallByIDOrName(idOrName); err != nil {
return err
} }
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
return nil
}
installed, err := manager.IsInstalled(*plugin) func updatePluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to check install status: %w", err) return fmt.Errorf("failed to create manager: %w", err)
} }
if !installed { registry, err := plugins.NewRegistry()
return fmt.Errorf("plugin not installed: %s", plugin.Name) if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
} }
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID) pluginList, _ := registry.List()
if err := manager.Uninstall(*plugin); err != nil { plugin := plugins.FindByIDOrName(idOrName, pluginList)
return fmt.Errorf("failed to uninstall plugin: %w", err)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
return fmt.Errorf("failed to update plugin: %w", err)
}
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
return nil
} }
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name) fmt.Printf("Updating plugin: %s\n", idOrName)
if err := manager.UpdateByIDOrName(idOrName); err != nil {
return err
}
fmt.Printf("Plugin updated successfully: %s\n", idOrName)
return nil return nil
} }
+3 -2
View File
@@ -15,6 +15,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -121,10 +122,10 @@ func updateArchLinux() error {
var helper string var helper string
var updateCmd *exec.Cmd var updateCmd *exec.Cmd
if commandExists("yay") { if utils.CommandExists("yay") {
helper = "yay" helper = "yay"
updateCmd = exec.Command("yay", "-S", packageName) updateCmd = exec.Command("yay", "-S", packageName)
} else if commandExists("paru") { } else if utils.CommandExists("paru") {
helper = "paru" helper = "paru"
updateCmd = exec.Command("paru", "-S", packageName) updateCmd = exec.Command("paru", "-S", packageName)
} else { } else {
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
@@ -448,7 +449,7 @@ func enableGreeter() error {
fmt.Println("Detecting installed compositors...") fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors() compositors := greeter.DetectCompositors()
if commandExists("sway") { if utils.CommandExists("sway") {
compositors = append(compositors, "sway") compositors = append(compositors, "sway")
} }
+7
View File
@@ -89,6 +89,11 @@ func initializeProviders() {
log.Warnf("Failed to register MangoWC provider: %v", err) log.Warnf("Failed to register MangoWC provider: %v", err)
} }
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
swayProvider := providers.NewSwayProvider("$HOME/.config/sway") swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(swayProvider); err != nil { if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err) log.Warnf("Failed to register Sway provider: %v", err)
@@ -125,6 +130,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewMangoWCProvider(path) return providers.NewMangoWCProvider(path)
case "sway": case "sway":
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "scroll":
return providers.NewSwayProvider(path)
case "niri": case "niri":
return providers.NewNiriProvider(path) return providers.NewNiriProvider(path)
default: default:
+51 -13
View File
@@ -295,7 +295,14 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data() data := buf.Data()
rgb := make([]byte, dstW*dstH*3) rgb := make([]byte, dstW*dstH*3)
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < dstH; y++ { for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale) srcY := int(float64(y) / scale)
@@ -309,16 +316,17 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
} }
si := srcY*buf.Stride + srcX*4 si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3 di := (y*dstW + x) * 3
if si+2 < len(data) { if si+3 >= len(data) {
if swapRB { continue
rgb[di+0] = data[si+2] }
rgb[di+1] = data[si+1] if swapRB {
rgb[di+2] = data[si+0] rgb[di+0] = data[si+2]
} else { rgb[di+1] = data[si+1]
rgb[di+0] = data[si+0] rgb[di+2] = data[si+0]
rgb[di+1] = data[si+1] } else {
rgb[di+2] = data[si+2] rgb[di+0] = data[si+0]
} rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
} }
} }
} }
@@ -370,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
} }
for _, o := range outputs { for _, o := range outputs {
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n", scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale) if o.FractionalScale == float64(int(o.FractionalScale)) {
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
}
transformStr := transformName(o.Transform)
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
}
}
func transformName(t int32) string {
switch t {
case 0:
return "normal"
case 1:
return "90"
case 2:
return "180"
case 3:
return "270"
case 4:
return "flipped"
case 5:
return "flipped-90"
case 6:
return "flipped-180"
case 7:
return "flipped-270"
default:
return fmt.Sprintf("%d", t)
} }
} }
+17 -3
View File
@@ -29,6 +29,7 @@ func runSetup() error {
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected { if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.") fmt.Println("No configurations selected. Exiting.")
@@ -67,14 +68,14 @@ func runSetup() error {
var err error var err error
if wmSelected && terminalSelected { if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal) results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
} else if wmSelected { } else if wmSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty) results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
if len(results) > 1 { if len(results) > 1 {
results = results[:1] results = results[:1]
} }
} else if terminalSelected { } else if terminalSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal) results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
if len(results) > 0 && results[0].ConfigType == "Niri" { if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:] results = results[1:]
} }
@@ -144,6 +145,19 @@ func promptTerminal() (deps.Terminal, bool) {
} }
} }
func promptSystemd() bool {
fmt.Println("\nUse systemd for session management?")
fmt.Println("1) Yes (recommended for most distros)")
fmt.Println("2) No (standalone, no systemd integration)")
var response string
fmt.Print("\nChoice (1-2): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
return response != "2"
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool { func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME") homeDir := os.Getenv("HOME")
willBackup := false willBackup := false
+1 -1
View File
@@ -23,7 +23,7 @@ func init() {
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root // Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
+1 -1
View File
@@ -21,7 +21,7 @@ func init() {
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root // Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
+1 -11
View File
@@ -104,7 +104,6 @@ func getAllDMSPIDs() []int {
continue continue
} }
// Check if the child process is still alive
proc, err := os.FindProcess(childPID) proc, err := os.FindProcess(childPID)
if err != nil { if err != nil {
os.Remove(pidFile) os.Remove(pidFile)
@@ -112,18 +111,15 @@ func getAllDMSPIDs() []int {
} }
if err := proc.Signal(syscall.Signal(0)); err != nil { if err := proc.Signal(syscall.Signal(0)); err != nil {
// Process is dead, remove stale PID file
os.Remove(pidFile) os.Remove(pidFile)
continue continue
} }
pids = append(pids, childPID) pids = append(pids, childPID)
// Also get the parent PID from the filename
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-") parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid") parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil { if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
// Check if parent is still alive
if parentProc, err := os.FindProcess(parentPID); err == nil { if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil { if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID) pids = append(pids, parentPID)
@@ -159,6 +155,7 @@ func runShellInteractive(session bool) {
errChan <- fmt.Errorf("server panic: %v", r) errChan <- fmt.Errorf("server panic: %v", r)
} }
}() }()
server.CLIVersion = Version
if err := server.Start(false); err != nil { if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err) errChan <- fmt.Errorf("server error: %w", err)
} }
@@ -225,7 +222,6 @@ func runShellInteractive(session bool) {
return return
} }
// All other signals: clean shutdown
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 +278,6 @@ func restartShell() {
} }
func killShell() { func killShell() {
// Get all tracked DMS PIDs from PID files
pids := getAllDMSPIDs() pids := getAllDMSPIDs()
if len(pids) == 0 { if len(pids) == 0 {
@@ -293,14 +288,12 @@ func killShell() {
currentPid := os.Getpid() currentPid := os.Getpid()
uniquePids := make(map[int]bool) uniquePids := make(map[int]bool)
// Deduplicate and filter out current process
for _, pid := range pids { for _, pid := range pids {
if pid != currentPid { if pid != currentPid {
uniquePids[pid] = true uniquePids[pid] = true
} }
} }
// Kill all tracked processes
for pid := range uniquePids { for pid := range uniquePids {
proc, err := os.FindProcess(pid) proc, err := os.FindProcess(pid)
if err != nil { if err != nil {
@@ -308,7 +301,6 @@ func killShell() {
continue continue
} }
// Check if process is still alive before killing
if err := proc.Signal(syscall.Signal(0)); err != nil { if err := proc.Signal(syscall.Signal(0)); err != nil {
continue continue
} }
@@ -320,7 +312,6 @@ func killShell() {
} }
} }
// Clean up any remaining PID files
dir := getRuntimeDir() dir := getRuntimeDir()
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
@@ -337,7 +328,6 @@ func killShell() {
func runShellDaemon(session bool) { func runShellDaemon(session bool) {
isSessionManaged = session isSessionManaged = session
// Check if this is the daemon child process by looking for the hidden flag
isDaemonChild := false isDaemonChild := false
for _, arg := range os.Args { for _, arg := range os.Args {
if arg == "--daemon-child" { if arg == "--daemon-child" {
-6
View File
@@ -6,12 +6,6 @@ import (
"strings" "strings"
) )
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// findCommandPath returns the absolute path to a command in PATH
func findCommandPath(cmd string) (string, error) { func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd) path, err := exec.LookPath(cmd)
if err != nil { if err != nil {
+14 -1
View File
@@ -30,6 +30,7 @@ type Output struct {
height int32 height int32
scale int32 scale int32
fractionalScale float64 fractionalScale float64
transform int32
} }
type LayerSurface struct { type LayerSurface struct {
@@ -276,6 +277,7 @@ func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
if o, ok := p.outputs[name]; ok { if o, ok := p.outputs[name]; ok {
o.x = e.X o.x = e.X
o.y = e.Y o.y = e.Y
o.transform = int32(e.Transform)
} }
p.outputsMu.Unlock() p.outputsMu.Unlock()
}) })
@@ -485,8 +487,19 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady() ls.state.OnScreencopyReady()
logicalW, _ := ls.state.LogicalSize()
screenBuf := ls.state.ScreenBuffer() screenBuf := ls.state.ScreenBuffer()
if screenBuf != nil && ls.output.transform != TransformNormal {
invTransform := InverseTransform(ls.output.transform)
transformed, err := screenBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != screenBuf {
ls.state.ReplaceScreenBuffer(transformed)
}
}
logicalW, _ := ls.state.LogicalSize()
screenBuf = ls.state.ScreenBuffer()
if logicalW > 0 && screenBuf != nil { if logicalW > 0 && screenBuf != nil {
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW) ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
} }
+15
View File
@@ -4,10 +4,25 @@ import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
)
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color { func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888) return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
} }
+78 -21
View File
@@ -1,6 +1,7 @@
package colorpicker package colorpicker
import ( import (
"fmt"
"math" "math"
"strings" "strings"
"sync" "sync"
@@ -15,6 +16,8 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888 FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888 FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
) )
type SurfaceState struct { type SurfaceState struct {
@@ -79,6 +82,11 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
bpp := format.BytesPerPixel()
if stride < width*bpp {
return fmt.Errorf("invalid stride %d for width %d (bpp=%d)", stride, width, bpp)
}
if s.screenBuf != nil { if s.screenBuf != nil {
s.screenBuf.Close() s.screenBuf.Close()
s.screenBuf = nil s.screenBuf = nil
@@ -90,6 +98,7 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
} }
s.screenBuf = buf s.screenBuf = buf
s.screenBuf.Format = format
s.screenFormat = format s.screenFormat = format
return nil return nil
} }
@@ -106,6 +115,20 @@ func (s *SurfaceState) ScreenFormat() PixelFormat {
return s.screenFormat return s.screenFormat
} }
func (s *SurfaceState) ReplaceScreenBuffer(newBuf *ShmBuffer) {
s.mu.Lock()
defer s.mu.Unlock()
if s.screenBuf != nil {
s.screenBuf.Close()
}
s.screenBuf = newBuf
s.screenFormat = newBuf.Format
s.recomputeScale()
s.ensureRenderBuffers()
}
func (s *SurfaceState) OnScreencopyFlags(flags uint32) { func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
s.mu.Lock() s.mu.Lock()
s.yInverted = (flags & 1) != 0 s.yInverted = (flags & 1) != 0
@@ -120,6 +143,15 @@ func (s *SurfaceState) OnScreencopyReady() {
return return
} }
if s.screenFormat.Is24Bit() {
converted, newFormat, err := s.screenBuf.ConvertTo32Bit(s.screenFormat)
if err == nil && converted != s.screenBuf {
s.screenBuf.Close()
s.screenBuf = converted
s.screenFormat = newFormat
}
}
s.recomputeScale() s.recomputeScale()
s.ensureRenderBuffers() s.ensureRenderBuffers()
s.readyForDisplay = true s.readyForDisplay = true
@@ -279,10 +311,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawMagnifierWithInversion( drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.Height, dst.Data(), dst.Stride, dst.Width, dst.Height,
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height, s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
px, py, picked, s.yInverted, px, py, picked, s.yInverted, s.screenFormat,
) )
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase) drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
return dst return dst
} }
@@ -390,6 +422,7 @@ func drawMagnifierWithInversion(
cx, cy int, cx, cy int,
borderColor Color, borderColor Color,
yInverted bool, yInverted bool,
format PixelFormat,
) { ) {
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 { if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
return return
@@ -407,6 +440,14 @@ func drawMagnifierWithInversion(
innerRadius := float64(outerRadius - borderThickness) innerRadius := float64(outerRadius - borderThickness)
outerRadiusF := float64(outerRadius) outerRadiusF := float64(outerRadius)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ { for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
y := cy + dy y := cy + dy
if y < 0 || y >= dstH { if y < 0 || y >= dstH {
@@ -431,9 +472,9 @@ func drawMagnifierWithInversion(
} }
bgColor := Color{ bgColor := Color{
B: dst[dstOff+0], R: dst[dstOff+rOff],
G: dst[dstOff+1], G: dst[dstOff+1],
R: dst[dstOff+2], B: dst[dstOff+bOff],
A: dst[dstOff+3], A: dst[dstOff+3],
} }
@@ -462,7 +503,7 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255} magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
finalColor = blendColors(magColor, borderColor, alpha) finalColor = blendColors(magColor, borderColor, alpha)
} else { } else {
finalColor = borderColor finalColor = borderColor
@@ -483,24 +524,25 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255} finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
} else { } else {
continue continue
} }
} }
dst[dstOff+0] = finalColor.B dst[dstOff+rOff] = finalColor.R
dst[dstOff+1] = finalColor.G dst[dstOff+1] = finalColor.G
dst[dstOff+2] = finalColor.R dst[dstOff+bOff] = finalColor.B
dst[dstOff+3] = 255 dst[dstOff+3] = 255
} }
} }
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius) drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
} }
func drawMagnifierCrosshair( func drawMagnifierCrosshair(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int, data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) { ) {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return return
@@ -998,7 +1040,7 @@ var fontGlyphs = map[rune][fontH]uint8{
}, },
} }
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) { func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
text := formatColorForPreview(c, format, lowercase) text := formatColorForPreview(c, format, lowercase)
if len(text) == 0 { if len(text) == 0 {
return return
@@ -1033,9 +1075,8 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
y = height - boxH y = height - boxH
} }
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c) drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
// Use contrasting text color based on luminance
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B) lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
var fg Color var fg Color
if lum > 128 { if lum > 128 {
@@ -1043,7 +1084,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else { } else {
fg = Color{R: 255, G: 255, B: 255, A: 255} fg = Color{R: 255, G: 255, B: 255, A: 255}
} }
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg) drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
} }
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string { func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
@@ -1064,7 +1105,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
} }
} }
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) { func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) {
if w <= 0 || h <= 0 { if w <= 0 || h <= 0 {
return return
} }
@@ -1073,6 +1114,14 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
x = clamp(x, 0, width) x = clamp(x, 0, width)
y = clamp(y, 0, height) y = clamp(y, 0, height)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for yy := y; yy < yEnd; yy++ { for yy := y; yy < yEnd; yy++ {
rowOff := yy * stride rowOff := yy * stride
for xx := x; xx < xEnd; xx++ { for xx := x; xx < xEnd; xx++ {
@@ -1080,26 +1129,34 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
if off+4 > len(data) { if off+4 > len(data) {
continue continue
} }
data[off+0] = col.B data[off+rOff] = col.R
data[off+1] = col.G data[off+1] = col.G
data[off+2] = col.R data[off+bOff] = col.B
data[off+3] = 255 data[off+3] = 255
} }
} }
} }
func drawText(data []byte, stride, width, height, x, y int, text string, col Color) { func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) {
for i, r := range text { for i, r := range text {
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col) drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format)
} }
} }
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) { func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) {
g, ok := fontGlyphs[r] g, ok := fontGlyphs[r]
if !ok { if !ok {
return return
} }
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for row := 0; row < fontH; row++ { for row := 0; row < fontH; row++ {
yy := y + row yy := y + row
if yy < 0 || yy >= height { if yy < 0 || yy >= height {
@@ -1123,9 +1180,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
continue continue
} }
data[off+0] = col.B data[off+rOff] = col.R
data[off+1] = col.G data[off+1] = col.G
data[off+2] = col.R data[off+bOff] = col.B
data[off+3] = 255 data[off+3] = 255
} }
} }
+91 -69
View File
@@ -46,11 +46,20 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil) return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
} }
// DeployConfigurationsWithSystemd deploys configurations with systemd option
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
}
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil) return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
shouldReplaceConfig := func(configType string) bool { shouldReplaceConfig := func(configType string) bool {
@@ -64,7 +73,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") { if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal) result, err := cd.deployNiriConfig(terminal, useSystemd)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err) return results, fmt.Errorf("failed to deploy Niri config: %w", err)
@@ -72,7 +81,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
} }
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") { if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal) result, err := cd.deployHyprlandConfig(terminal, useSystemd)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err) return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
@@ -110,7 +119,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return results, nil return results, nil
} }
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) { func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Niri", ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"), Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
@@ -148,12 +157,6 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
}
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -166,8 +169,11 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
terminalCommand = "ghostty" terminalCommand = "ghostty"
} }
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
@@ -404,41 +410,6 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil return results, nil
} }
// detectPolkitAgent tries to find the polkit authentication agent on the system
// Prioritizes mate-polkit paths since that's what we install
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
// Prioritize mate-polkit paths first
matePaths := []string{
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
}
for _, path := range matePaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
return path, nil
}
}
// Fallback to other polkit agents if mate-polkit is not found
fallbackPaths := []string{
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"/usr/libexec/polkit-gnome-authentication-agent-1",
}
for _, path := range fallbackPaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
return path, nil
}
}
return "", fmt.Errorf("no polkit agent found in common locations")
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config // mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones) // Regular expression to match output sections (including commented ones)
@@ -482,7 +453,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
} }
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging // deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) { func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Hyprland", ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"), Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
@@ -514,14 +485,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -531,13 +494,15 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
case deps.TerminalAlacritty: case deps.TerminalAlacritty:
terminalCommand = "alacritty" terminalCommand = "alacritty"
default: default:
terminalCommand = "ghostty" // fallback to ghostty terminalCommand = "ghostty"
} }
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the monitor sections
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
if err != nil { if err != nil {
@@ -560,24 +525,16 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config // mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match monitor lines (including commented ones)
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
// Also matches commented versions: # monitor = ...
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`) monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
// Find all monitor lines in the existing config
existingMonitors := monitorRegex.FindAllString(existingConfig, -1) existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 { if len(existingMonitors) == 0 {
// No monitor sections to merge
return newConfig, nil return newConfig, nil
} }
// Remove the example monitor line from the new config
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`) exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "") mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`) monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig) headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
@@ -585,8 +542,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return "", fmt.Errorf("could not find MONITOR CONFIG section") return "", fmt.Errorf("could not find MONITOR CONFIG section")
} }
// Insert after the header insertPos := headerMatch[1] + 1
insertPos := headerMatch[1] + 1 // +1 for the newline
var builder strings.Builder var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos]) builder.WriteString(mergedConfig[:insertPos])
@@ -601,3 +557,69 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return builder.String(), nil return builder.String(), nil
} }
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
lines := strings.Split(config, "\n")
var result []string
startupSectionFound := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
continue
}
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
continue
}
result = append(result, line)
}
if !startupSectionFound {
for i, line := range result {
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
}
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
break
}
}
}
return strings.Join(result, "\n")
}
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "%s"
}`, terminalCommand)
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
config = strings.Replace(config,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
1)
}
return config
}
+6 -38
View File
@@ -3,7 +3,6 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -11,23 +10,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDetectPolkitAgent(t *testing.T) {
cd := &ConfigDeployer{}
// This test depends on the system having a polkit agent installed
// We'll just test that the function doesn't crash and returns some path or error
path, err := cd.detectPolkitAgent()
if err != nil {
// If no polkit agent is found, that's okay for testing
assert.Contains(t, err.Error(), "no polkit agent found")
} else {
// If found, it should be a valid path
assert.NotEmpty(t, path)
assert.True(t, strings.Contains(path, "polkit"))
}
}
func TestMergeNiriOutputSections(t *testing.T) { func TestMergeNiriOutputSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -272,17 +254,6 @@ func getGhosttyPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config") return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
} }
func TestPolkitPathInjection(t *testing.T) {
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
other content`
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
}
func TestMergeHyprlandMonitorSections(t *testing.T) { func TestMergeHyprlandMonitorSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -424,7 +395,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
cd := NewConfigDeployer(logChan) cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) { t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty) result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -435,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), "bind = $mod, T, exec, $TERMINAL") assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
assert.Contains(t, string(content), "exec-once = ") assert.Contains(t, string(content), "exec-once = ")
}) })
@@ -454,7 +425,7 @@ general {
err = os.WriteFile(hyprPath, []byte(existingContent), 0644) err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err) require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty) result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -471,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), "bind = $mod, T, exec, $TERMINAL") 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")
}) })
} }
@@ -479,7 +450,6 @@ general {
func TestNiriConfigStructure(t *testing.T) { func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {") assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {") assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriBindsConfig, "binds {") assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`) assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
@@ -490,11 +460,9 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# STARTUP APPS") assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG") assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS") assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}") assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle") assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrule {") assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
} }
func TestGhosttyConfigStructure(t *testing.T) { func TestGhosttyConfigStructure(t *testing.T) {
+3 -8
View File
@@ -5,19 +5,14 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
func LocateDMSConfig() (string, error) { func LocateDMSConfig() (string, error) {
var primaryPaths []string var primaryPaths []string
configHome := os.Getenv("XDG_CONFIG_HOME") configHome := utils.XDGConfigHome()
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" { if configHome != "" {
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms")) primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
} }
+26 -121
View File
@@ -10,8 +10,9 @@ monitor = , preferred,auto,auto
# ================== # ==================
# STARTUP APPS # STARTUP APPS
# ================== # ==================
exec-once = dbus-update-activation-environment --systemd --all
exec-once = systemctl --user start hyprland-session.target
exec-once = bash -c "wl-paste --watch cliphist store &" exec-once = bash -c "wl-paste --watch cliphist store &"
exec-once = {{POLKIT_AGENT_PATH}}
# ================== # ==================
# INPUT CONFIG # INPUT CONFIG
@@ -90,132 +91,36 @@ misc {
# ================== # ==================
# WINDOW RULES # WINDOW RULES
# ================== # ==================
windowrule { windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
name = windowrule-1
tile = on
match:class = ^(org\.wezfurlong\.wezterm)$
border_size = 0
}
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrule { windowrulev2 = tile, class:^(gnome-control-center)$
name = windowrule-2 windowrulev2 = tile, class:^(pavucontrol)$
rounding = 12 windowrulev2 = tile, class:^(nm-connection-editor)$
match:class = ^(org\.gnome\.)
border_size = 0
}
windowrulev2 = float, class:^(gnome-calculator)$
windowrulev2 = float, class:^(galculator)$
windowrulev2 = float, class:^(blueman-manager)$
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
windowrulev2 = float, class:^(steam)$
windowrulev2 = float, class:^(xdg-desktop-portal)$
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = noborder, class:^(Alacritty)$
windowrulev2 = noborder, class:^(zen)$
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
windowrulev2 = noborder, class:^(kitty)$
windowrule { windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
name = windowrule-3 windowrulev2 = float, class:^(zoom)$
tile = on
match:class = ^(gnome-control-center)$
}
windowrule { # DMS windows floating by default
name = windowrule-4 windowrulev2 = float, class:^(org.quickshell)$
tile = on windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
match:class = ^(pavucontrol)$
}
windowrule { layerrule = noanim, ^(quickshell)$
name = windowrule-5
tile = on
match:class = ^(nm-connection-editor)$
}
windowrule {
name = windowrule-6
float = on
match:class = ^(gnome-calculator)$
}
windowrule {
name = windowrule-7
float = on
match:class = ^(galculator)$
}
windowrule {
name = windowrule-8
float = on
match:class = ^(blueman-manager)$
}
windowrule {
name = windowrule-9
float = on
match:class = ^(org\.gnome\.Nautilus)$
}
windowrule {
name = windowrule-10
float = on
match:class = ^(steam)$
}
windowrule {
name = windowrule-11
float = on
match:class = ^(xdg-desktop-portal)$
}
windowrule {
name = windowrule-12
border_size = 0
match:class = ^(Alacritty)$
}
windowrule {
name = windowrule-13
border_size = 0
match:class = ^(zen)$
}
windowrule {
name = windowrule-14
border_size = 0
match:class = ^(com\.mitchellh\.ghostty)$
}
windowrule {
name = windowrule-15
border_size = 0
match:class = ^(kitty)$
}
windowrule {
name = windowrule-16
float = on
match:class = ^(firefox)$
match:title = ^(Picture-in-Picture)$
}
windowrule {
name = windowrule-17
float = on
match:class = ^(zoom)$
}
windowrule {
name = windowrule-18
opacity = 0.9 0.9
match:float = 0
match:focus = 0
}
layerrule {
name = layerrule-1
no_anim = on
match:namespace = ^(quickshell)$
}
# ================== # ==================
# KEYBINDINGS # KEYBINDINGS
@@ -223,7 +128,7 @@ layerrule {
$mod = SUPER $mod = SUPER
# === Application Launchers === # === Application Launchers ===
bind = $mod, T, exec, $TERMINAL bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle bind = $mod, M, exec, dms ipc call processlist focusOrToggle
-7
View File
@@ -44,7 +44,6 @@ input {
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout // https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout { layout {
// Set gaps around windows in logical pixels. // Set gaps around windows in logical pixels.
gaps 5
background-color "transparent" background-color "transparent"
// When to center a column when changing focus, options are: // When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left // - "never", default behavior, focusing an off-screen column will keep at the left
@@ -87,11 +86,6 @@ layout {
inactive-color "#d0d0d0" // Light gray inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red urgent-color "#cc4444" // Softer red
} }
focus-ring {
width 2
active-color "#808080" // Medium gray
inactive-color "#505050" // Dark gray
}
shadow { shadow {
softness 30 softness 30
spread 5 spread 5
@@ -116,7 +110,6 @@ overview {
// 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 &" spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment { environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
} }
+5 -4
View File
@@ -113,13 +113,14 @@ func RGBToHSV(rgb RGB) HSV {
delta := max - min delta := max - min
var h float64 var h float64
if delta == 0 { switch {
case delta == 0:
h = 0 h = 0
} else if max == rgb.R { case max == rgb.R:
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0 h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
} else if max == rgb.G { case max == rgb.G:
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0 h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
} else { default:
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0 h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
} }
+10 -28
View File
@@ -112,31 +112,11 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
} }
func (a *ArchDistribution) detectXDGPortal() deps.Dependency { func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
if a.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (a *ArchDistribution) detectAccountsService() deps.Dependency { func (a *ArchDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
if a.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {
@@ -182,13 +162,11 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
if forceQuickshellGit || variant == deps.VariantGit { if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem} // ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
} }
@@ -362,7 +340,11 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := a.EnableDMSService(ctx); err != nil { if err := a.WriteWindowManagerConfig(wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := a.EnableDMSService(ctx, wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+71 -29
View File
@@ -76,47 +76,42 @@ func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *
return exec.CommandContext(ctx, "bash", "-c", cmdStr) return exec.CommandContext(ctx, "bash", "-c", cmdStr)
} }
// Common dependency detection methods func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
func (b *BaseDistribution) detectGit() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if b.commandExists("git") { if b.commandExists(name) {
status = deps.StatusInstalled status = deps.StatusInstalled
} }
return deps.Dependency{ return deps.Dependency{
Name: "git", Name: name,
Status: status, Status: status,
Description: "Version control system", Description: description,
Required: true, Required: true,
} }
} }
func (b *BaseDistribution) detectPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: true,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system")
}
func (b *BaseDistribution) detectMatugen() deps.Dependency { func (b *BaseDistribution) detectMatugen() deps.Dependency {
status := deps.StatusMissing return b.detectCommand("matugen", "Material Design color generation tool")
if b.commandExists("matugen") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "matugen",
Status: status,
Description: "Material Design color generation tool",
Required: true,
}
} }
func (b *BaseDistribution) detectDgop() deps.Dependency { func (b *BaseDistribution) detectDgop() deps.Dependency {
status := deps.StatusMissing return b.detectCommand("dgop", "Desktop portal management tool")
if b.commandExists("dgop") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dgop",
Status: status,
Description: "Desktop portal management tool",
Required: true,
}
} }
func (b *BaseDistribution) detectDMS() deps.Dependency { func (b *BaseDistribution) detectDMS() deps.Dependency {
@@ -602,12 +597,59 @@ TERMINAL=%s
return nil return nil
} }
func (b *BaseDistribution) EnableDMSService(ctx context.Context) error { func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms") cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to enable dms service: %w", err) return fmt.Errorf("failed to enable dms service: %w", err)
} }
b.log("Enabled dms systemd user service") b.log("Enabled dms systemd user service")
switch wm {
case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service")
}
case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target")
}
}
return nil
}
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
if wm == deps.WindowManagerHyprland {
if err := b.WriteHyprlandSessionTarget(); err != nil {
return fmt.Errorf("failed to write hyprland session target: %w", err)
}
}
return nil
}
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
targetPath := filepath.Join(targetDir, "hyprland-session.target")
content := `[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
return nil return nil
} }
+3 -7
View File
@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) { func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
@@ -36,7 +37,7 @@ func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
} }
func TestBaseDistribution_detectDMS_Installed(t *testing.T) { func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
if !commandExists("git") { if !utils.CommandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -80,7 +81,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
} }
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) { func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
if !commandExists("git") { if !utils.CommandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -164,11 +165,6 @@ func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
} }
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func TestBaseDistribution_versionCompare(t *testing.T) { func TestBaseDistribution_versionCompare(t *testing.T) {
logChan := make(chan string, 10) logChan := make(chan string, 10)
defer close(logChan) defer close(logChan)
+15 -41
View File
@@ -75,45 +75,15 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (d *DebianDistribution) detectXDGPortal() deps.Dependency { func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk"))
if d.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency { func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing return d.detectCommand("xwayland-satellite", "Xwayland support")
if d.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (d *DebianDistribution) detectAccountsService() deps.Dependency { func (d *DebianDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
if d.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
@@ -208,7 +178,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil { if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential") cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err) return fmt.Errorf("failed to install build-essential: %w", err)
} }
@@ -225,7 +195,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -338,7 +308,11 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := d.EnableDMSService(ctx); err != nil { if err := d.WriteWindowManagerConfig(wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := d.EnableDMSService(ctx, wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
@@ -449,7 +423,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL), CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
} }
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath) keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd) cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil { if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
@@ -467,7 +441,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
addRepoCmd := ExecSudoCommand(ctx, sudoPassword, addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile)) fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
} }
@@ -502,7 +476,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"apt-get", "install", "-y"} args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -612,7 +586,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup", CommandInfo: "sudo apt-get install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup") rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
@@ -651,7 +625,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go", CommandInfo: "sudo apt-get install golang-go",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go") installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
} }
+8 -17
View File
@@ -97,17 +97,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency { func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk"))
if f.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (f *FedoraDistribution) packageInstalled(pkg string) bool { func (f *FedoraDistribution) packageInstalled(pkg string) bool {
@@ -166,10 +156,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"} return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
} }
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"} return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
} }
@@ -177,7 +164,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
if variant == deps.VariantGit { if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
} }
return PackageMapping{Name: "niri", Repository: RepoTypeSystem} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
} }
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -362,7 +349,11 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := f.EnableDMSService(ctx); err != nil { if err := f.WriteWindowManagerConfig(wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := f.EnableDMSService(ctx, wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+10 -56
View File
@@ -95,7 +95,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectWindowManager(wm)) dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell()) dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal()) dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectPolkitAgent())
dependencies = append(dependencies, g.detectAccountsService()) dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland { if wm == deps.WindowManagerHyprland {
@@ -114,59 +113,15 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (g *GentooDistribution) detectXDGPortal() deps.Dependency { func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk"))
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("mate-extra/mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite"))
if g.packageInstalled("gui-apps/xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (g *GentooDistribution) detectAccountsService() deps.Dependency { func (g *GentooDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice"))
if g.packageInstalled("sys-apps/accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (g *GentooDistribution) packageInstalled(pkg string) bool { func (g *GentooDistribution) packageInstalled(pkg string) bool {
@@ -187,7 +142,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"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}, "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"},
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"}, "qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
@@ -223,12 +177,8 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"} return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
} }
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
archKeyword := g.getArchKeyword() return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
if variant == deps.VariantGit {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
}
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
} }
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
@@ -456,7 +406,11 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := g.EnableDMSService(ctx); err != nil { if err := g.WriteWindowManagerConfig(wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := g.EnableDMSService(ctx, wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+7 -13
View File
@@ -87,17 +87,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
} }
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency { func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk"))
if o.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
@@ -377,7 +367,11 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := o.EnableDMSService(ctx); err != nil { if err := o.WriteWindowManagerConfig(wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := o.EnableDMSService(ctx, wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
@@ -472,7 +466,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
cmd := ExecSudoCommand(ctx, sudoPassword, cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL)) fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err) o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
} }
enabledRepos[pkg.RepoURL] = true enabledRepos[pkg.RepoURL] = true
+8 -34
View File
@@ -85,45 +85,15 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency { func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return u.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", u.packageInstalled("xdg-desktop-portal-gtk"))
if u.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency { func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing return u.detectCommand("xwayland-satellite", "Xwayland support")
if u.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency { func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
if u.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
@@ -357,7 +327,11 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := u.EnableDMSService(ctx); err != nil { if err := u.WriteWindowManagerConfig(wm); err != nil {
u.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := u.EnableDMSService(ctx, wm); err != nil {
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+7
View File
@@ -286,6 +286,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, loadInstalledPlugins return m, loadInstalledPlugins
} }
return m, nil return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg: case pluginInstalledMsg:
if msg.err != nil { if msg.err != nil {
m.pluginsError = msg.err.Error() m.pluginsError = msg.err.Error()
+13 -7
View File
@@ -75,14 +75,13 @@ type MenuItem struct {
func NewModel(version string) Model { func NewModel(version string) Model {
detector, _ := NewDetector() detector, _ := NewDetector()
dependencies := detector.GetInstalledComponents()
// Use the proper detection method for both window managers var dependencies []DependencyInfo
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus() var hyprlandInstalled, niriInstalled bool
if err != nil {
// Fallback to false if detection fails if detector != nil {
hyprlandInstalled = false dependencies = detector.GetInstalledComponents()
niriInstalled = false hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
} }
m := Model{ m := Model{
@@ -201,6 +200,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, loadInstalledPlugins return m, loadInstalledPlugins
} }
return m, nil return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg: case pluginInstalledMsg:
if msg.err != nil { if msg.err != nil {
m.pluginsError = msg.err.Error() m.pluginsError = msg.err.Error()
+38
View File
@@ -227,6 +227,11 @@ func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd)
plugin := m.installedPluginsList[m.selectedInstalledIndex] plugin := m.installedPluginsList[m.selectedInstalledIndex]
return m, uninstallPlugin(plugin) return m, uninstallPlugin(plugin)
} }
case "p":
if m.selectedInstalledIndex < len(m.installedPluginsList) {
plugin := m.installedPluginsList[m.selectedInstalledIndex]
return m, updatePlugin(plugin)
}
} }
return m, nil return m, nil
} }
@@ -246,6 +251,11 @@ type pluginInstalledMsg struct {
err error err error
} }
type pluginUpdatedMsg struct {
pluginName string
err error
}
func loadInstalledPlugins() tea.Msg { func loadInstalledPlugins() tea.Msg {
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
@@ -337,3 +347,31 @@ func uninstallPlugin(plugin pluginInfo) tea.Cmd {
return pluginUninstalledMsg{pluginName: plugin.Name} return pluginUninstalledMsg{pluginName: plugin.Name}
} }
} }
func updatePlugin(plugin pluginInfo) tea.Cmd {
return func() tea.Msg {
manager, err := plugins.NewManager()
if err != nil {
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
}
p := plugins.Plugin{
ID: plugin.ID,
Name: plugin.Name,
Category: plugin.Category,
Author: plugin.Author,
Description: plugin.Description,
Repo: plugin.Repo,
Path: plugin.Path,
Capabilities: plugin.Capabilities,
Compositors: plugin.Compositors,
Dependencies: plugin.Dependencies,
}
if err := manager.Update(p); err != nil {
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
}
return pluginUpdatedMsg{pluginName: plugin.Name}
}
}
+7 -11
View File
@@ -11,6 +11,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// DetectDMSPath checks for DMS installation following XDG Base Directory specification // DetectDMSPath checks for DMS installation following XDG Base Directory specification
@@ -22,10 +23,10 @@ func DetectDMSPath() (string, error) {
func DetectCompositors() []string { func DetectCompositors() []string {
var compositors []string var compositors []string
if commandExists("niri") { if utils.CommandExists("niri") {
compositors = append(compositors, "niri") compositors = append(compositors, "niri")
} }
if commandExists("Hyprland") { if utils.CommandExists("Hyprland") {
compositors = append(compositors, "Hyprland") compositors = append(compositors, "Hyprland")
} }
@@ -62,7 +63,7 @@ func PromptCompositorChoice(compositors []string) (string, error) {
// EnsureGreetdInstalled checks if greetd is installed and installs it if not // EnsureGreetdInstalled checks if greetd is installed and installs it if not
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error { func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
if commandExists("greetd") { if utils.CommandExists("greetd") {
logFunc("✓ greetd is already installed") logFunc("✓ greetd is already installed")
return nil return nil
} }
@@ -144,7 +145,7 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
// Check if dms-greeter is already in PATH // Check if dms-greeter is already in PATH
if commandExists("dms-greeter") { if utils.CommandExists("dms-greeter") {
logFunc("✓ dms-greeter wrapper already installed") logFunc("✓ dms-greeter wrapper already installed")
} else { } else {
// Install the wrapper script // Install the wrapper script
@@ -204,7 +205,7 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal // SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
if !commandExists("setfacl") { if !utils.CommandExists("setfacl") {
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.") logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
logFunc(" If theme sync doesn't work, you may need to install acl package:") logFunc(" If theme sync doesn't work, you may need to install acl package:")
logFunc(" - Fedora/RHEL: sudo dnf install acl") logFunc(" - Fedora/RHEL: sudo dnf install acl")
@@ -419,7 +420,7 @@ user = "greeter"
// Determine wrapper command path // Determine wrapper command path
wrapperCmd := "dms-greeter" wrapperCmd := "dms-greeter"
if !commandExists("dms-greeter") { if !utils.CommandExists("dms-greeter") {
wrapperCmd = "/usr/local/bin/dms-greeter" wrapperCmd = "/usr/local/bin/dms-greeter"
} }
@@ -486,8 +487,3 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
+4 -22
View File
@@ -5,6 +5,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type DiscoveryConfig struct { type DiscoveryConfig struct {
@@ -14,13 +16,7 @@ type DiscoveryConfig struct {
func DefaultDiscoveryConfig() *DiscoveryConfig { func DefaultDiscoveryConfig() *DiscoveryConfig {
var searchPaths []string var searchPaths []string
configHome := os.Getenv("XDG_CONFIG_HOME") configHome := utils.XDGConfigHome()
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" { if configHome != "" {
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets")) searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
} }
@@ -43,7 +39,7 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
var files []string var files []string
for _, searchPath := range d.SearchPaths { for _, searchPath := range d.SearchPaths {
expandedPath, err := expandPath(searchPath) expandedPath, err := utils.ExpandPath(searchPath)
if err != nil { if err != nil {
continue continue
} }
@@ -74,20 +70,6 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
return files, nil return files, nil
} }
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}
type JSONProviderFactory func(filePath string) (Provider, error) type JSONProviderFactory func(filePath string) (Provider, error)
var jsonProviderFactory JSONProviderFactory var jsonProviderFactory JSONProviderFactory
+4 -2
View File
@@ -4,6 +4,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestDefaultDiscoveryConfig(t *testing.T) { func TestDefaultDiscoveryConfig(t *testing.T) {
@@ -272,13 +274,13 @@ func TestExpandPathInDiscovery(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) {
result, err := expandPath(tt.input) result, err := utils.ExpandPath(tt.input)
if err != nil { if err != nil {
t.Fatalf("expandPath failed: %v", err) t.Fatalf("expandPath failed: %v", err)
} }
if result != tt.expected { if result != tt.expected {
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected) t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
} }
}) })
} }
@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -42,14 +44,9 @@ func NewHyprlandParser() *HyprlandParser {
} }
func (p *HyprlandParser) ReadContent(directory string) error { func (p *HyprlandParser) ReadContent(directory string) error {
expandedDir := os.ExpandEnv(directory) expandedDir, err := utils.ExpandPath(directory)
expandedDir = filepath.Clean(expandedDir) if err != nil {
if strings.HasPrefix(expandedDir, "~") { return err
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedDir = filepath.Join(home, expandedDir[1:])
} }
info, err := os.Stat(expandedDir) info, err := os.Stat(expandedDir)
+2 -16
View File
@@ -5,9 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type JSONFileProvider struct { type JSONFileProvider struct {
@@ -20,7 +20,7 @@ func NewJSONFileProvider(filePath string) (*JSONFileProvider, error) {
return nil, fmt.Errorf("file path cannot be empty") return nil, fmt.Errorf("file path cannot be empty")
} }
expandedPath, err := expandPath(filePath) expandedPath, err := utils.ExpandPath(filePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to expand path: %w", err) return nil, fmt.Errorf("failed to expand path: %w", err)
} }
@@ -117,17 +117,3 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil }, nil
} }
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}
@@ -4,6 +4,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestNewJSONFileProvider(t *testing.T) { func TestNewJSONFileProvider(t *testing.T) {
@@ -266,13 +268,13 @@ func TestExpandPath(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) {
result, err := expandPath(tt.input) result, err := utils.ExpandPath(tt.input)
if err != nil { if err != nil {
t.Fatalf("expandPath failed: %v", err) t.Fatalf("expandPath failed: %v", err)
} }
if result != tt.expected { if result != tt.expected {
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected) t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
} }
}) })
} }
@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -34,14 +36,9 @@ func NewMangoWCParser() *MangoWCParser {
} }
func (p *MangoWCParser) ReadContent(path string) error { func (p *MangoWCParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath, err := utils.ExpandPath(path)
expandedPath = filepath.Clean(expandedPath) if err != nil {
if strings.HasPrefix(expandedPath, "~") { return err
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedPath = filepath.Join(home, expandedPath[1:])
} }
info, err := os.Stat(expandedPath) info, err := os.Stat(expandedPath)
+21 -16
View File
@@ -6,9 +6,11 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"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"
) )
@@ -29,15 +31,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
} }
func defaultNiriConfigDir() string { func defaultNiriConfigDir() string {
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" { return filepath.Join(utils.XDGConfigHome(), "niri")
return filepath.Join(configHome, "niri")
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "niri")
} }
func (n *NiriProvider) Name() string { func (n *NiriProvider) Name() string {
@@ -154,11 +148,13 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
} }
bind := keybinds.Keybind{ bind := keybinds.Keybind{
Key: keyStr, Key: keyStr,
Description: kb.Description, Description: kb.Description,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source, Source: source,
HideOnOverlay: kb.HideOnOverlay,
CooldownMs: kb.CooldownMs,
} }
if source == "dms" && conflicts != nil { if source == "dms" && conflicts != nil {
@@ -316,7 +312,9 @@ func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
opts["repeat"] = val.String() == "true" opts["repeat"] = val.String() == "true"
} }
if val, ok := node.Properties.Get("cooldown-ms"); ok { if val, ok := node.Properties.Get("cooldown-ms"); ok {
opts["cooldown-ms"] = val.String() if ms, err := strconv.Atoi(val.String()); err == nil {
opts["cooldown-ms"] = ms
}
} }
if val, ok := node.Properties.Get("allow-when-locked"); ok { if val, ok := node.Properties.Get("allow-when-locked"); ok {
opts["allow-when-locked"] = val.String() == "true" opts["allow-when-locked"] = val.String() == "true"
@@ -342,7 +340,14 @@ func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
node.AddProperty("repeat", false, "") node.AddProperty("repeat", false, "")
} }
if v, ok := bind.Options["cooldown-ms"]; ok { if v, ok := bind.Options["cooldown-ms"]; ok {
node.AddProperty("cooldown-ms", v, "") switch val := v.(type) {
case int:
node.AddProperty("cooldown-ms", val, "")
case string:
if ms, err := strconv.Atoi(val); err == nil {
node.AddProperty("cooldown-ms", ms, "")
}
}
} }
if v, ok := bind.Options["allow-when-locked"]; ok && v == true { if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
node.AddProperty("allow-when-locked", true, "") node.AddProperty("allow-when-locked", true, "")
+28 -13
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/sblinch/kdl-go" "github.com/sblinch/kdl-go"
@@ -11,12 +12,14 @@ import (
) )
type NiriKeyBinding struct { type NiriKeyBinding struct {
Mods []string Mods []string
Key string Key string
Action string Action string
Args []string Args []string
Description string Description string
Source string HideOnOverlay bool
CooldownMs int
Source string
} }
type NiriSection struct { type NiriSection struct {
@@ -273,19 +276,31 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
} }
var description string var description string
var hideOnOverlay bool
var cooldownMs int
if node.Properties != nil { if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok { if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
description = val.ValueString() switch val.ValueString() {
case "null", "":
hideOnOverlay = true
default:
description = val.ValueString()
}
}
if val, ok := node.Properties.Get("cooldown-ms"); ok {
cooldownMs, _ = strconv.Atoi(val.String())
} }
} }
return &NiriKeyBinding{ return &NiriKeyBinding{
Mods: mods, Mods: mods,
Key: key, Key: key,
Action: action, Action: action,
Args: args, Args: args,
Description: description, Description: description,
Source: p.currentSource, HideOnOverlay: hideOnOverlay,
CooldownMs: cooldownMs,
Source: p.currentSource,
} }
} }
+32 -2
View File
@@ -2,6 +2,7 @@ package providers
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -9,18 +10,42 @@ import (
type SwayProvider struct { type SwayProvider struct {
configPath string configPath string
isScroll bool
} }
func NewSwayProvider(configPath string) *SwayProvider { func NewSwayProvider(configPath string) *SwayProvider {
isScroll := false
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
if configPath == "" { if configPath == "" {
configPath = "$HOME/.config/sway" if scrollEnvSet {
configPath = "$HOME/.config/scroll"
isScroll = true
} else {
configPath = "$HOME/.config/sway"
}
} else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll")
} }
return &SwayProvider{ return &SwayProvider{
configPath: configPath, configPath: configPath,
isScroll: isScroll,
} }
} }
func (s *SwayProvider) Name() string { func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK")
if ok {
return "scroll"
}
}
return "sway" return "sway"
} }
@@ -33,8 +58,13 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
categorizedBinds := make(map[string][]keybinds.Keybind) categorizedBinds := make(map[string][]keybinds.Keybind)
s.convertSection(section, "", categorizedBinds) s.convertSection(section, "", categorizedBinds)
cheatSheetTitle := "Sway Keybinds"
if s != nil && s.isScroll {
cheatSheetTitle = "Scroll Keybinds"
}
return &keybinds.CheatSheet{ return &keybinds.CheatSheet{
Title: "Sway Keybinds", Title: cheatSheetTitle,
Provider: s.Name(), Provider: s.Name(),
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil }, nil
@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -42,14 +44,9 @@ func NewSwayParser() *SwayParser {
} }
func (p *SwayParser) ReadContent(path string) error { func (p *SwayParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath, err := utils.ExpandPath(path)
expandedPath = filepath.Clean(expandedPath) if err != nil {
if strings.HasPrefix(expandedPath, "~") { return err
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedPath = filepath.Join(home, expandedPath[1:])
} }
info, err := os.Stat(expandedPath) info, err := os.Stat(expandedPath)
+8 -6
View File
@@ -1,12 +1,14 @@
package keybinds package keybinds
type Keybind struct { type Keybind struct {
Key string `json:"key"` Key string `json:"key"`
Description string `json:"desc"` Description string `json:"desc"`
Action string `json:"action,omitempty"` Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"` Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"` HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
CooldownMs int `json:"cooldownMs,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
} }
type DMSBindsStatus struct { type DMSBindsStatus struct {
+3 -7
View File
@@ -13,6 +13,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
var ( var (
@@ -277,7 +278,7 @@ func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return return
} }
if checkCmd != "skip" && !commandExists(checkCmd) { if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
return return
} }
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
@@ -293,7 +294,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fil
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return return
} }
if checkCmd != "skip" && !commandExists(checkCmd) { if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
return return
} }
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
@@ -390,11 +391,6 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx] return content[startIdx : startIdx+endIdx]
} }
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func checkMatugenVersion() { func checkMatugenVersion() {
matugenVersionOnce.Do(func() { matugenVersionOnce.Do(func() {
cmd := exec.Command("matugen", "--version") cmd := exec.Command("matugen", "--version")
+216 -63
View File
@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@@ -32,33 +33,70 @@ func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
} }
func getPluginsDir() string { func getPluginsDir() string {
configHome := os.Getenv("XDG_CONFIG_HOME") return filepath.Join(utils.XDGConfigHome(), "DankMaterialShell", "plugins")
if configHome == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return filepath.Join(os.TempDir(), "DankMaterialShell", "plugins")
}
configHome = filepath.Join(homeDir, ".config")
}
return filepath.Join(configHome, "DankMaterialShell", "plugins")
} }
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) { func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
pluginPath := filepath.Join(m.pluginsDir, plugin.ID) path, err := m.findInstalledPath(plugin.ID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { return path != "", nil
return true, nil }
func (m *Manager) findInstalledPath(pluginID string) (string, error) {
// Check user plugins directory
path, err := m.findInDir(m.pluginsDir, pluginID)
if err != nil {
return "", err
}
if path != "" {
return path, nil
} }
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID) // Check system plugins directory
systemExists, err := afero.DirExists(m.fs, systemPluginPath) systemDir := "/etc/xdg/quickshell/dms-plugins"
if err != nil { return m.findInDir(systemDir, pluginID)
return false, err }
func (m *Manager) findInDir(dir, pluginID string) (string, error) {
// First, check if folder with exact ID name exists
exactPath := filepath.Join(dir, pluginID)
if exists, _ := afero.DirExists(m.fs, exactPath); exists {
return exactPath, nil
} }
return systemExists, nil
// Scan all folders and check plugin.json for matching ID
exists, err := afero.DirExists(m.fs, dir)
if err != nil || !exists {
return "", nil
}
entries, err := afero.ReadDir(m.fs, dir)
if err != nil {
return "", nil
}
for _, entry := range entries {
name := entry.Name()
if name == ".repos" || strings.HasSuffix(name, ".meta") {
continue
}
fullPath := filepath.Join(dir, name)
isPlugin := entry.IsDir() || entry.Mode()&os.ModeSymlink != 0
if !isPlugin {
if info, err := m.fs.Stat(fullPath); err == nil && info.IsDir() {
isPlugin = true
}
}
if isPlugin && m.getPluginID(fullPath) == pluginID {
return fullPath, nil
}
}
return "", nil
} }
func (m *Manager) Install(plugin Plugin) error { func (m *Manager) Install(plugin Plugin) error {
@@ -151,25 +189,19 @@ func (m *Manager) createSymlink(source, dest string) error {
} }
func (m *Manager) Update(plugin Plugin) error { func (m *Manager) Update(plugin Plugin) error {
pluginPath := filepath.Join(m.pluginsDir, plugin.ID) pluginPath, err := m.findInstalledPath(plugin.ID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err) return fmt.Errorf("failed to find plugin: %w", err)
} }
if !exists { if pluginPath == "" {
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err)
}
if systemExists {
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
}
return fmt.Errorf("plugin not installed: %s", plugin.Name) return fmt.Errorf("plugin not installed: %s", plugin.Name)
} }
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
}
metaPath := pluginPath + ".meta" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
@@ -209,25 +241,19 @@ func (m *Manager) Update(plugin Plugin) error {
} }
func (m *Manager) Uninstall(plugin Plugin) error { func (m *Manager) Uninstall(plugin Plugin) error {
pluginPath := filepath.Join(m.pluginsDir, plugin.ID) pluginPath, err := m.findInstalledPath(plugin.ID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err) return fmt.Errorf("failed to find plugin: %w", err)
} }
if !exists { if pluginPath == "" {
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err)
}
if systemExists {
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
}
return fmt.Errorf("plugin not installed: %s", plugin.Name) return fmt.Errorf("plugin not installed: %s", plugin.Name)
} }
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
}
metaPath := pluginPath + ".meta" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
@@ -369,47 +395,174 @@ func (m *Manager) ListInstalled() ([]string, error) {
// getPluginID reads the plugin.json file and returns the plugin ID // getPluginID reads the plugin.json file and returns the plugin ID
func (m *Manager) getPluginID(pluginPath string) string { func (m *Manager) getPluginID(pluginPath string) string {
manifest := m.getPluginManifest(pluginPath)
if manifest == nil {
return ""
}
return manifest.ID
}
func (m *Manager) getPluginManifest(pluginPath string) *pluginManifest {
manifestPath := filepath.Join(pluginPath, "plugin.json") manifestPath := filepath.Join(pluginPath, "plugin.json")
data, err := afero.ReadFile(m.fs, manifestPath) data, err := afero.ReadFile(m.fs, manifestPath)
if err != nil { if err != nil {
return "" return nil
} }
var manifest struct { var manifest pluginManifest
ID string `json:"id"`
}
if err := json.Unmarshal(data, &manifest); err != nil { if err := json.Unmarshal(data, &manifest); err != nil {
return "" return nil
} }
return manifest.ID return &manifest
}
type pluginManifest struct {
ID string `json:"id"`
Name string `json:"name"`
} }
func (m *Manager) GetPluginsDir() string { func (m *Manager) GetPluginsDir() string {
return m.pluginsDir return m.pluginsDir
} }
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) { func (m *Manager) UninstallByIDOrName(idOrName string) error {
pluginPath := filepath.Join(m.pluginsDir, pluginID) pluginPath, err := m.findInstalledPathByIDOrName(idOrName)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to check if plugin exists: %w", err) return err
}
if pluginPath == "" {
return fmt.Errorf("plugin not found: %s", idOrName)
} }
if !exists { if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", pluginID) return fmt.Errorf("cannot uninstall system plugin: %s", idOrName)
systemExists, err := afero.DirExists(m.fs, systemPluginPath) }
if err != nil {
return false, fmt.Errorf("failed to check system plugin: %w", err) metaPath := pluginPath + ".meta"
metaExists, _ := afero.Exists(m.fs, metaPath)
if metaExists {
if err := m.fs.Remove(pluginPath); err != nil {
return fmt.Errorf("failed to remove symlink: %w", err)
} }
if systemExists { if err := m.fs.Remove(metaPath); err != nil {
return false, nil return fmt.Errorf("failed to remove metadata: %w", err)
} }
} else {
if err := m.fs.RemoveAll(pluginPath); err != nil {
return fmt.Errorf("failed to remove plugin: %w", err)
}
}
return nil
}
func (m *Manager) UpdateByIDOrName(idOrName string) error {
pluginPath, err := m.findInstalledPathByIDOrName(idOrName)
if err != nil {
return err
}
if pluginPath == "" {
return fmt.Errorf("plugin not found: %s", idOrName)
}
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return fmt.Errorf("cannot update system plugin: %s", idOrName)
}
metaPath := pluginPath + ".meta"
metaExists, _ := afero.Exists(m.fs, metaPath)
if metaExists {
// Plugin is from monorepo, but we don't know the repo URL without registry
// Just try to pull from existing .git in the symlink target
return fmt.Errorf("cannot update monorepo plugin without registry info: %s", idOrName)
}
// Standalone plugin - just pull
if err := m.gitClient.Pull(pluginPath); err != nil {
return fmt.Errorf("failed to update plugin: %w", err)
}
return nil
}
func (m *Manager) findInstalledPathByIDOrName(idOrName string) (string, error) {
path, err := m.findInDirByIDOrName(m.pluginsDir, idOrName)
if err != nil {
return "", err
}
if path != "" {
return path, nil
}
systemDir := "/etc/xdg/quickshell/dms-plugins"
return m.findInDirByIDOrName(systemDir, idOrName)
}
func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
// Check exact folder name match first
exactPath := filepath.Join(dir, idOrName)
if exists, _ := afero.DirExists(m.fs, exactPath); exists {
return exactPath, nil
}
exists, err := afero.DirExists(m.fs, dir)
if err != nil || !exists {
return "", nil
}
entries, err := afero.ReadDir(m.fs, dir)
if err != nil {
return "", nil
}
for _, entry := range entries {
name := entry.Name()
if name == ".repos" || strings.HasSuffix(name, ".meta") {
continue
}
fullPath := filepath.Join(dir, name)
isPlugin := entry.IsDir() || entry.Mode()&os.ModeSymlink != 0
if !isPlugin {
if info, err := m.fs.Stat(fullPath); err == nil && info.IsDir() {
isPlugin = true
}
}
if !isPlugin {
continue
}
manifest := m.getPluginManifest(fullPath)
if manifest == nil {
continue
}
if manifest.ID == idOrName || manifest.Name == idOrName {
return fullPath, nil
}
}
return "", nil
}
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
pluginPath, err := m.findInstalledPath(pluginID)
if err != nil {
return false, fmt.Errorf("failed to find plugin: %w", err)
}
if pluginPath == "" {
return false, fmt.Errorf("plugin not installed: %s", pluginID) return false, fmt.Errorf("plugin not installed: %s", pluginID)
} }
// Check if there's a .meta file (plugin installed from a monorepo) if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return false, nil
}
metaPath := pluginPath + ".meta" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
+31 -48
View File
@@ -3,6 +3,8 @@ package plugins
import ( import (
"sort" "sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func FuzzySearch(query string, plugins []Plugin) []Plugin { func FuzzySearch(query string, plugins []Plugin) []Plugin {
@@ -11,18 +13,12 @@ func FuzzySearch(query string, plugins []Plugin) []Plugin {
} }
queryLower := strings.ToLower(query) queryLower := strings.ToLower(query)
var results []Plugin return utils.Filter(plugins, func(p Plugin) bool {
return fuzzyMatch(queryLower, strings.ToLower(p.Name)) ||
for _, plugin := range plugins { fuzzyMatch(queryLower, strings.ToLower(p.Category)) ||
if fuzzyMatch(queryLower, strings.ToLower(plugin.Name)) || fuzzyMatch(queryLower, strings.ToLower(p.Description)) ||
fuzzyMatch(queryLower, strings.ToLower(plugin.Category)) || fuzzyMatch(queryLower, strings.ToLower(p.Author))
fuzzyMatch(queryLower, strings.ToLower(plugin.Description)) || })
fuzzyMatch(queryLower, strings.ToLower(plugin.Author)) {
results = append(results, plugin)
}
}
return results
} }
func fuzzyMatch(query, text string) bool { func fuzzyMatch(query, text string) bool {
@@ -39,57 +35,34 @@ func FilterByCategory(category string, plugins []Plugin) []Plugin {
if category == "" { if category == "" {
return plugins return plugins
} }
var results []Plugin
categoryLower := strings.ToLower(category) categoryLower := strings.ToLower(category)
return utils.Filter(plugins, func(p Plugin) bool {
for _, plugin := range plugins { return strings.ToLower(p.Category) == categoryLower
if strings.ToLower(plugin.Category) == categoryLower { })
results = append(results, plugin)
}
}
return results
} }
func FilterByCompositor(compositor string, plugins []Plugin) []Plugin { func FilterByCompositor(compositor string, plugins []Plugin) []Plugin {
if compositor == "" { if compositor == "" {
return plugins return plugins
} }
var results []Plugin
compositorLower := strings.ToLower(compositor) compositorLower := strings.ToLower(compositor)
return utils.Filter(plugins, func(p Plugin) bool {
for _, plugin := range plugins { return utils.Any(p.Compositors, func(c string) bool {
for _, comp := range plugin.Compositors { return strings.ToLower(c) == compositorLower
if strings.ToLower(comp) == compositorLower { })
results = append(results, plugin) })
break
}
}
}
return results
} }
func FilterByCapability(capability string, plugins []Plugin) []Plugin { func FilterByCapability(capability string, plugins []Plugin) []Plugin {
if capability == "" { if capability == "" {
return plugins return plugins
} }
var results []Plugin
capabilityLower := strings.ToLower(capability) capabilityLower := strings.ToLower(capability)
return utils.Filter(plugins, func(p Plugin) bool {
for _, plugin := range plugins { return utils.Any(p.Capabilities, func(c string) bool {
for _, cap := range plugin.Capabilities { return strings.ToLower(c) == capabilityLower
if strings.ToLower(cap) == capabilityLower { })
results = append(results, plugin) })
break
}
}
}
return results
} }
func SortByFirstParty(plugins []Plugin) []Plugin { func SortByFirstParty(plugins []Plugin) []Plugin {
@@ -103,3 +76,13 @@ func SortByFirstParty(plugins []Plugin) []Plugin {
}) })
return plugins return plugins
} }
func FindByIDOrName(idOrName string, plugins []Plugin) *Plugin {
if p, found := utils.Find(plugins, func(p Plugin) bool { return p.ID == idOrName }); found {
return &p
}
if p, found := utils.Find(plugins, func(p Plugin) bool { return p.Name == idOrName }); found {
return &p
}
return nil
}
@@ -238,9 +238,17 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy != nil { if proxy == nil {
e.Head = proxy.(*ZwlrOutputHeadV1) head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context())
head.SetID(objectID)
registerServerProxy(i.Context(), head, objectID)
e.Head = head
} else if head, ok := proxy.(*ZwlrOutputHeadV1); ok {
e.Head = head
} else { } else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
head := &ZwlrOutputHeadV1{} head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context()) head.SetContext(i.Context())
head.SetID(objectID) head.SetID(objectID)
@@ -715,9 +723,17 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy != nil { if proxy == nil {
e.Mode = proxy.(*ZwlrOutputModeV1) mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
registerServerProxy(i.Context(), mode, objectID)
e.Mode = mode
} else if mode, ok := proxy.(*ZwlrOutputModeV1); ok {
e.Mode = mode
} else { } else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
mode := &ZwlrOutputModeV1{} mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context()) mode.SetContext(i.Context())
mode.SetID(objectID) mode.SetID(objectID)
@@ -743,7 +759,26 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
} }
var e ZwlrOutputHeadV1CurrentModeEvent var e ZwlrOutputHeadV1CurrentModeEvent
l := 0 l := 0
e.Mode = i.Context().GetProxy(client.Uint32(data[l : l+4])).(*ZwlrOutputModeV1) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
// Mode not yet registered, create it
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
registerServerProxy(i.Context(), mode, objectID)
e.Mode = mode
} else if mode, ok := proxy.(*ZwlrOutputModeV1); ok {
e.Mode = mode
} else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
registerServerProxy(i.Context(), mode, objectID)
e.Mode = mode
}
l += 4 l += 4
i.currentModeHandler(e) i.currentModeHandler(e)
+136 -8
View File
@@ -7,6 +7,7 @@ import (
"os/exec" "os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
@@ -19,6 +20,7 @@ const (
CompositorSway CompositorSway
CompositorNiri CompositorNiri
CompositorDWL CompositorDWL
CompositorScroll
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -31,6 +33,7 @@ func DetectCompositor() Compositor {
hyprlandSig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") hyprlandSig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
niriSocket := os.Getenv("NIRI_SOCKET") niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK") swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK")
switch { switch {
case niriSocket != "": case niriSocket != "":
@@ -38,6 +41,12 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorNiri detectedCompositor = CompositorNiri
return detectedCompositor return detectedCompositor
} }
case scrollSocket != "":
if _, err := os.Stat(scrollSocket); err == nil {
detectedCompositor = CompositorScroll
return detectedCompositor
}
case swaySocket != "": case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil { if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway detectedCompositor = CompositorSway
@@ -89,12 +98,15 @@ func SetCompositorDWL() {
} }
type WindowGeometry struct { type WindowGeometry struct {
X int32 X int32
Y int32 Y int32
Width int32 Width int32
Height int32 Height int32
Output string Output string
Scale float64 Scale float64
OutputX int32
OutputY int32
OutputTransform int32
} }
func GetActiveWindow() (*WindowGeometry, error) { func GetActiveWindow() (*WindowGeometry, error) {
@@ -229,6 +241,25 @@ func getSwayFocusedMonitor() string {
return "" return ""
} }
func getScrollFocusedMonitor() string {
output, err := exec.Command("scrollmsg", "-t", "get_workspaces").Output()
if err != nil {
return ""
}
var workspaces []swayWorkspace
if err := json.Unmarshal(output, &workspaces); err != nil {
return ""
}
for _, ws := range workspaces {
if ws.Focused {
return ws.Output
}
}
return ""
}
type niriWorkspace struct { type niriWorkspace struct {
Output string `json:"output"` Output string `json:"output"`
IsFocused bool `json:"is_focused"` IsFocused bool `json:"is_focused"`
@@ -374,6 +405,8 @@ func GetFocusedMonitor() string {
return getHyprlandFocusedMonitor() return getHyprlandFocusedMonitor()
case CompositorSway: case CompositorSway:
return getSwayFocusedMonitor() return getSwayFocusedMonitor()
case CompositorScroll:
return getScrollFocusedMonitor()
case CompositorNiri: case CompositorNiri:
return getNiriFocusedMonitor() return getNiriFocusedMonitor()
case CompositorDWL: case CompositorDWL:
@@ -382,6 +415,92 @@ func GetFocusedMonitor() string {
return "" return ""
} }
type outputInfo struct {
x, y int32
transform int32
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
display, err := client.Connect("")
if err != nil {
return nil, false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, false
}
var outputManager *wlr_output_management.ZwlrOutputManagerV1
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
outputManager = mgr
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false
}
if outputManager == nil {
return nil, false
}
type headState struct {
name string
x, y int32
transform int32
}
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
done := false
outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
state := &headState{}
heads[e.Head] = state
e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) {
state.name = ne.Name
})
e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
state.x = pe.X
state.y = pe.Y
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform
})
})
outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
done = true
})
for !done {
if err := ctx.Dispatch(); err != nil {
return nil, false
}
}
for _, state := range heads {
if state.name == outputName {
return &outputInfo{
x: state.x,
y: state.y,
transform: state.transform,
}, true
}
}
return nil, false
}
func getDWLActiveWindow() (*WindowGeometry, error) { func getDWLActiveWindow() (*WindowGeometry, error) {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
@@ -509,14 +628,23 @@ func getDWLActiveWindow() (*WindowGeometry, error) {
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
return &WindowGeometry{
geom := &WindowGeometry{
X: state.x, X: state.x,
Y: state.y, Y: state.y,
Width: state.w, Width: state.w,
Height: state.h, Height: state.h,
Output: state.name, Output: state.name,
Scale: scale, Scale: scale,
}, nil }
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
} }
return nil, fmt.Errorf("no active output found") return nil, fmt.Errorf("no active output found")
+20 -51
View File
@@ -9,7 +9,10 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func BufferToImage(buf *ShmBuffer) *image.RGBA { func BufferToImage(buf *ShmBuffer) *image.RGBA {
@@ -20,7 +23,13 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height)) img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
data := buf.Data() data := buf.Data()
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0 var swapRB bool
switch format {
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < buf.Height; y++ { for y := 0; y < buf.Height; y++ {
srcOff := y * buf.Stride srcOff := y * buf.Stride
@@ -110,70 +119,30 @@ func GetOutputDir() string {
} }
func getXDGPicturesDir() string { func getXDGPicturesDir() string {
configDir := os.Getenv("XDG_CONFIG_HOME") userDirsFile := filepath.Join(utils.XDGConfigHome(), "user-dirs.dirs")
if configDir == "" {
home := os.Getenv("HOME")
if home == "" {
return ""
}
configDir = filepath.Join(home, ".config")
}
userDirsFile := filepath.Join(configDir, "user-dirs.dirs")
data, err := os.ReadFile(userDirsFile) data, err := os.ReadFile(userDirsFile)
if err != nil { if err != nil {
return "" return ""
} }
for _, line := range splitLines(string(data)) { for _, line := range strings.Split(string(data), "\n") {
if len(line) == 0 || line[0] == '#' { if len(line) == 0 || line[0] == '#' {
continue continue
} }
const prefix = "XDG_PICTURES_DIR=" const prefix = "XDG_PICTURES_DIR="
if len(line) > len(prefix) && line[:len(prefix)] == prefix { if !strings.HasPrefix(line, prefix) {
path := line[len(prefix):] continue
path = trimQuotes(path)
path = expandHome(path)
return path
} }
path := strings.Trim(line[len(prefix):], "\"")
expanded, err := utils.ExpandPath(path)
if err != nil {
return ""
}
return expanded
} }
return "" return ""
} }
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
lines = append(lines, s[start:i])
start = i + 1
}
}
if start < len(s) {
lines = append(lines, s[start:])
}
return lines
}
func trimQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
func expandHome(path string) string {
if len(path) >= 5 && path[:5] == "$HOME" {
home := os.Getenv("HOME")
return home + path[5:]
}
if len(path) >= 1 && path[0] == '~' {
home := os.Getenv("HOME")
return home + path[1:]
}
return path
}
func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error { func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error {
return WriteToFileWithFormat(buf, path, format, quality, uint32(FormatARGB8888)) return WriteToFileWithFormat(buf, path, format, quality, uint32(FormatARGB8888))
} }
+52 -6
View File
@@ -380,19 +380,24 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
return return
} }
var capturedBuf *ShmBuffer
var capturedFormat PixelFormat
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
capturedFormat = PixelFormat(e.Format)
bpp := capturedFormat.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
return
}
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("create screen buffer failed", "err", err) log.Error("create screen buffer failed", "err", err)
return return
} }
if withCursor { capturedBuf = buf
pc.screenBuf = buf buf.Format = capturedFormat
pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil { if err != nil {
@@ -421,6 +426,47 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
frame.Destroy() frame.Destroy()
if capturedBuf == nil {
onReady()
return
}
if capturedFormat.Is24Bit() {
converted, newFormat, err := capturedBuf.ConvertTo32Bit(capturedFormat)
if err != nil {
log.Error("convert 24-bit to 32-bit failed", "err", err)
} else if converted != capturedBuf {
capturedBuf.Close()
capturedBuf = converted
capturedFormat = newFormat
}
}
pc.format = uint32(capturedFormat)
if pc.yInverted {
capturedBuf.FlipVertical()
pc.yInverted = false
}
if output.transform != TransformNormal {
invTransform := InverseTransform(output.transform)
transformed, err := capturedBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != capturedBuf {
capturedBuf.Close()
capturedBuf = transformed
}
}
if withCursor {
pc.screenBuf = capturedBuf
} else {
pc.screenBufNoCursor = capturedBuf
}
onReady() onReady()
}) })
+162 -38
View File
@@ -150,51 +150,33 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
case CompositorHyprland: case CompositorHyprland:
return s.captureAndCrop(output, region) return s.captureAndCrop(output, region)
case CompositorDWL: case CompositorDWL:
return s.captureDWLWindow(output, region, geom.Scale) return s.captureDWLWindow(output, region, geom)
default: default:
return s.captureRegionOnOutput(output, region) return s.captureRegionOnOutput(output, region)
} }
} }
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, dwlScale float64) (*CaptureResult, error) { func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output) result, err := s.captureWholeOutput(output)
if err != nil { if err != nil {
return nil, err return nil, err
} }
scale := dwlScale scale := geom.Scale
if scale <= 0 { if scale <= 0 || scale == 1.0 {
scale = float64(result.Buffer.Width) / float64(output.width) if output.fractionalScale > 1.0 {
scale = output.fractionalScale
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
localX := int(float64(region.X) * scale) localX := int(float64(region.X-geom.OutputX) * scale)
localY := int(float64(region.Y) * scale) localY := int(float64(region.Y-geom.OutputY) * scale)
if localX >= result.Buffer.Width {
localX = localX % result.Buffer.Width
}
if localY >= result.Buffer.Height {
localY = localY % result.Buffer.Height
}
w := int(float64(region.Width) * scale) w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale) h := int(float64(region.Height) * scale)
if localY+h > result.Buffer.Height && h <= result.Buffer.Height {
localY = result.Buffer.Height - h
if localY < 0 {
localY = 0
}
}
if localX+w > result.Buffer.Width && w <= result.Buffer.Width {
localX = result.Buffer.Width - w
if localX < 0 {
localX = 0
}
}
if localX < 0 { if localX < 0 {
w += localX w += localX
localX = 0 localX = 0
@@ -342,13 +324,18 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
outX, outY := output.x, output.y outX, outY := output.x, output.y
scale := float64(output.scale) scale := float64(output.scale)
if DetectCompositor() == CompositorHyprland { switch DetectCompositor() {
case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy outX, outY = hx, hy
} }
if s := GetHyprlandMonitorScale(output.name); s > 0 { if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s scale = s
} }
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
@@ -476,13 +463,42 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
return nil, fmt.Errorf("capture output: %w", err) return nil, fmt.Errorf("capture output: %w", err)
} }
return s.processFrame(frame, Region{ result, err := s.processFrame(frame, Region{
X: output.x, X: output.x,
Y: output.y, Y: output.y,
Width: output.width, Width: output.width,
Height: output.height, Height: output.height,
Output: output.name, Output: output.name,
}) })
if err != nil {
return nil, err
}
if result.YInverted {
result.Buffer.FlipVertical()
result.YInverted = false
}
if output.transform == TransformNormal {
return result, nil
}
invTransform := InverseTransform(output.transform)
transformed, err := result.Buffer.ApplyTransform(invTransform)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("apply transform: %w", err)
}
if transformed != result.Buffer {
result.Buffer.Close()
result.Buffer = transformed
}
result.Region.Width = int32(transformed.Width)
result.Region.Height = int32(transformed.Height)
return result, nil
} }
func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
@@ -563,6 +579,10 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
} }
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
if output.transform != TransformNormal {
return s.captureRegionOnTransformedOutput(output, region)
}
scale := output.fractionalScale scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland { if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name) scale = GetHyprlandMonitorScale(output.name)
@@ -617,6 +637,76 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return s.processFrame(frame, region) return s.processFrame(frame, region)
} }
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
}
scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-output.x) * scale)
localY := int(float64(region.Y-output.y) * scale)
w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale)
if localX < 0 {
w += localX
localX = 0
}
if localY < 0 {
h += localY
localY = 0
}
if localX+w > result.Buffer.Width {
w = result.Buffer.Width - localX
}
if localY+h > result.Buffer.Height {
h = result.Buffer.Height - localY
}
if w <= 0 || h <= 0 {
result.Buffer.Close()
return nil, fmt.Errorf("region not visible on output")
}
cropped, err := CreateShmBuffer(w, h, w*4)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("create crop buffer: %w", err)
}
srcData := result.Buffer.Data()
dstData := cropped.Data()
for y := 0; y < h; y++ {
srcOff := (localY+y)*result.Buffer.Stride + localX*4
dstOff := y * cropped.Stride
if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) {
copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4])
}
}
result.Buffer.Close()
cropped.Format = PixelFormat(result.Format)
return &CaptureResult{
Buffer: cropped,
Region: region,
YInverted: false,
Format: result.Format,
}, nil
}
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) { func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
var buf *ShmBuffer var buf *ShmBuffer
var pool *client.ShmPool var pool *client.ShmPool
@@ -627,13 +717,18 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
failed := false failed := false
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
format = PixelFormat(e.Format)
bpp := format.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
return
}
var err error var err error
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("failed to create buffer", "err", err) log.Error("failed to create buffer", "err", err)
return return
} }
format = PixelFormat(e.Format)
buf.Format = format buf.Format = format
}) })
@@ -696,6 +791,19 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
return nil, fmt.Errorf("frame capture failed") return nil, fmt.Errorf("frame capture failed")
} }
if format.Is24Bit() {
converted, newFormat, err := buf.ConvertTo32Bit(format)
if err != nil {
buf.Close()
return nil, fmt.Errorf("convert 24-bit to 32-bit: %w", err)
}
if converted != buf {
buf.Close()
buf = converted
}
format = newFormat
}
return &CaptureResult{ return &CaptureResult{
Buffer: buf, Buffer: buf,
Region: region, Region: region,
@@ -924,16 +1032,32 @@ func ListOutputs() ([]Output, error) {
sc.outputsMu.Lock() sc.outputsMu.Lock()
defer sc.outputsMu.Unlock() defer sc.outputsMu.Unlock()
compositor := DetectCompositor()
result := make([]Output, 0, len(sc.outputs)) result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs { for _, o := range sc.outputs {
result = append(result, Output{ out := Output{
Name: o.name, Name: o.name,
X: o.x, X: o.x,
Y: o.y, Y: o.y,
Width: o.width, Width: o.width,
Height: o.height, Height: o.height,
Scale: o.scale, Scale: o.scale,
}) FractionalScale: o.fractionalScale,
Transform: o.transform,
}
switch compositor {
case CompositorHyprland:
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
out.X, out.Y = hx, hy
out.Width, out.Height = hw, hh
}
if s := GetHyprlandMonitorScale(o.name); s > 0 {
out.FractionalScale = s
}
}
result = append(result, out)
} }
return result, nil return result, nil
} }
+17
View File
@@ -9,6 +9,19 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888 FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888 FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
)
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
) )
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
@@ -16,3 +29,7 @@ type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
+3 -9
View File
@@ -6,6 +6,8 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type ThemeColors struct { type ThemeColors struct {
@@ -72,15 +74,7 @@ func loadColorsFile() *ColorScheme {
} }
func getColorsFilePath() string { func getColorsFilePath() string {
cacheDir := os.Getenv("XDG_CACHE_HOME") return filepath.Join(utils.XDGCacheHome(), "DankMaterialShell", "dms-colors.json")
if cacheDir == "" {
home := os.Getenv("HOME")
if home == "" {
return ""
}
cacheDir = filepath.Join(home, ".cache")
}
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
} }
func isLightMode() bool { func isLightMode() bool {
+7 -5
View File
@@ -32,11 +32,13 @@ func (r Region) IsEmpty() bool {
} }
type Output struct { type Output struct {
Name string Name string
X, Y int32 X, Y int32
Width int32 Width int32
Height int32 Height int32
Scale int32 Scale int32
FractionalScale float64
Transform int32
} }
type Config struct { type Config struct {
+2 -8
View File
@@ -7,13 +7,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "apppicker.open", "browser.open": case "apppicker.open", "browser.open":
handleOpen(conn, req, manager) handleOpen(conn, req, manager)
@@ -22,7 +16,7 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleOpen(conn net.Conn, req Request, manager *Manager) { func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params) log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := req.Params["target"].(string) target, ok := req.Params["target"].(string)
+56 -79
View File
@@ -6,25 +6,15 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type BluetoothEvent struct { type BluetoothEvent struct {
Type string `json:"type"` Type string `json:"type"`
Data BluetoothState `json:"data"` Data BluetoothState `json:"data"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method { switch req.Method {
case "bluetooth.getState": case "bluetooth.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -57,31 +47,30 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleStartDiscovery(conn net.Conn, req Request, manager *Manager) { func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.StartDiscovery(); err != nil { if err := manager.StartDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery started"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"})
} }
func handleStopDiscovery(conn net.Conn, req Request, manager *Manager) { func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.StopDiscovery(); err != nil { if err := manager.StopDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery stopped"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"})
} }
func handleSetPowered(conn net.Conn, req Request, manager *Manager) { func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
powered, ok := req.Params["powered"].(bool) powered, err := params.Bool(req.Params, "powered")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'powered' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -90,13 +79,13 @@ func handleSetPowered(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "powered state updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"})
} }
func handlePairDevice(conn net.Conn, req Request, manager *Manager) { func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -105,13 +94,13 @@ func handlePairDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing initiated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"})
} }
func handleConnectDevice(conn net.Conn, req Request, manager *Manager) { func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -120,13 +109,13 @@ func handleConnectDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleDisconnectDevice(conn net.Conn, req Request, manager *Manager) { func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -135,13 +124,13 @@ func handleDisconnectDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
} }
func handleRemoveDevice(conn net.Conn, req Request, manager *Manager) { func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -150,13 +139,13 @@ func handleRemoveDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device removed"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"})
} }
func handleTrustDevice(conn net.Conn, req Request, manager *Manager) { func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -165,13 +154,13 @@ func handleTrustDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device trusted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"})
} }
func handleUntrustDevice(conn net.Conn, req Request, manager *Manager) { func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -180,43 +169,31 @@ func handleUntrustDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device untrusted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"})
} }
func handlePairingSubmit(conn net.Conn, req Request, manager *Manager) { func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
secretsRaw, ok := req.Params["secrets"].(map[string]any) secrets := params.StringMapOpt(req.Params, "secrets")
secrets := make(map[string]string) accept := params.BoolOpt(req.Params, "accept", false)
if ok {
for k, v := range secretsRaw {
if str, ok := v.(string); ok {
secrets[k] = str
}
}
}
accept := false
if acceptParam, ok := req.Params["accept"].(bool); ok {
accept = acceptParam
}
if err := manager.SubmitPairing(token, secrets, accept); err != nil { if err := manager.SubmitPairing(token, secrets, accept); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing response submitted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"})
} }
func handlePairingCancel(conn net.Conn, req Request, manager *Manager) { func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -225,10 +202,10 @@ func handlePairingCancel(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing cancelled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+49 -88
View File
@@ -2,12 +2,14 @@ package brightness
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
func HandleRequest(conn net.Conn, req Request, m *Manager) { func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method { switch req.Method {
case "brightness.getState": case "brightness.getState":
handleGetState(conn, req, m) handleGetState(conn, req, m)
@@ -22,131 +24,90 @@ func HandleRequest(conn net.Conn, req Request, m *Manager) {
case "brightness.subscribe": case "brightness.subscribe":
handleSubscribe(conn, req, m) handleSubscribe(conn, req, m)
default: default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method) models.RespondError(conn, req.ID, "unknown method: "+req.Method)
} }
} }
func handleGetState(conn net.Conn, req Request, m *Manager) { func handleGetState(conn net.Conn, req models.Request, m *Manager) {
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleSetBrightness(conn net.Conn, req Request, m *Manager) { func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
var params SetBrightnessParams device, err := params.String(req.Params, "device")
if err != nil {
device, ok := req.Params["device"].(string) models.RespondError(conn, req.ID, err.Error())
if !ok {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter")
return
}
params.Device = device
percentFloat, ok := req.Params["percent"].(float64)
if !ok {
models.RespondError(conn, req.ID.(int), "missing or invalid percent parameter")
return
}
params.Percent = int(percentFloat)
if exponential, ok := req.Params["exponential"].(bool); ok {
params.Exponential = exponential
}
exponent := 1.2
if exponentFloat, ok := req.Params["exponent"].(float64); ok {
params.Exponent = exponentFloat
exponent = exponentFloat
}
if err := m.SetBrightnessWithExponent(params.Device, params.Percent, params.Exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error())
return return
} }
state := m.GetState() percent, err := params.Int(req.Params, "percent")
models.Respond(conn, req.ID.(int), state) if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
exponential := params.BoolOpt(req.Params, "exponential", false)
exponent := params.FloatOpt(req.Params, "exponent", 1.2)
if err := m.SetBrightnessWithExponent(device, percent, exponential, exponent); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, m.GetState())
} }
func handleIncrement(conn net.Conn, req Request, m *Manager) { func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
device, ok := req.Params["device"].(string) device, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
step := 10 step := params.IntOpt(req.Params, "step", 10)
if stepFloat, ok := req.Params["step"].(float64); ok { exponential := params.BoolOpt(req.Params, "exponential", false)
step = int(stepFloat) exponent := params.FloatOpt(req.Params, "exponent", 1.2)
}
exponential := false
if expBool, ok := req.Params["exponential"].(bool); ok {
exponential = expBool
}
exponent := 1.2
if exponentFloat, ok := req.Params["exponent"].(float64); ok {
exponent = exponentFloat
}
if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil { if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleDecrement(conn net.Conn, req Request, m *Manager) { func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
device, ok := req.Params["device"].(string) device, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
step := 10 step := params.IntOpt(req.Params, "step", 10)
if stepFloat, ok := req.Params["step"].(float64); ok { exponential := params.BoolOpt(req.Params, "exponential", false)
step = int(stepFloat) exponent := params.FloatOpt(req.Params, "exponent", 1.2)
}
exponential := false
if expBool, ok := req.Params["exponential"].(bool); ok {
exponential = expBool
}
exponent := 1.2
if exponentFloat, ok := req.Params["exponent"].(float64); ok {
exponent = exponentFloat
}
if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil { if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleRescan(conn net.Conn, req Request, m *Manager) { func handleRescan(conn net.Conn, req models.Request, m *Manager) {
m.Rescan() m.Rescan()
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleSubscribe(conn net.Conn, req Request, m *Manager) { func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := "brightness-subscriber" clientID := fmt.Sprintf("brightness-%d", req.ID)
if idStr, ok := req.ID.(string); ok && idStr != "" {
clientID = idStr
}
ch := m.Subscribe(clientID) ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID) defer m.Unsubscribe(clientID)
initialState := m.GetState() initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID.(int), ID: req.ID,
Result: &initialState, Result: &initialState,
}); err != nil { }); err != nil {
return return
@@ -154,7 +115,7 @@ func handleSubscribe(conn net.Conn, req Request, m *Manager) {
for state := range ch { for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID.(int), ID: req.ID,
Result: &state, Result: &state,
}); err != nil { }); err != nil {
return return
-13
View File
@@ -33,12 +33,6 @@ type DeviceUpdate struct {
Device Device `json:"device"` Device Device `json:"device"`
} }
type Request struct {
ID any `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
type Manager struct { type Manager struct {
logindBackend *LogindBackend logindBackend *LogindBackend
sysfsBackend *SysfsBackend sysfsBackend *SysfsBackend
@@ -112,13 +106,6 @@ type ddcCapability struct {
current int current int
} }
type SetBrightnessParams struct {
Device string `json:"device"`
Percent int `json:"percent"`
Exponential bool `json:"exponential,omitempty"`
Exponent float64 `json:"exponent,omitempty"`
}
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16) ch := make(chan State, 16)
+1 -7
View File
@@ -6,13 +6,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "browser.open": case "browser.open":
url, ok := req.Params["url"].(string) url, ok := req.Params["url"].(string)
+152 -168
View File
@@ -6,25 +6,21 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type CUPSEvent struct { type CUPSEvent struct {
Type string `json:"type"` Type string `json:"type"`
Data CUPSState `json:"data"` Data CUPSState `json:"data"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { type TestPageResult struct {
Success bool `json:"success"`
JobID int `json:"jobId"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method { switch req.Method {
case "cups.subscribe": case "cups.subscribe":
handleSubscribe(conn, req, manager) handleSubscribe(conn, req, manager)
@@ -79,20 +75,19 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetPrinters(conn net.Conn, req Request, manager *Manager) { func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
printers, err := manager.GetPrinters() printers, err := manager.GetPrinters()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, printers) models.Respond(conn, req.ID, printers)
} }
func handleGetJobs(conn net.Conn, req Request, manager *Manager) { func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -101,14 +96,13 @@ func handleGetJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, jobs) models.Respond(conn, req.ID, jobs)
} }
func handlePausePrinter(conn net.Conn, req Request, manager *Manager) { func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -116,13 +110,13 @@ func handlePausePrinter(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "paused"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
} }
func handleResumePrinter(conn net.Conn, req Request, manager *Manager) { func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -130,28 +124,27 @@ func handleResumePrinter(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "resumed"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
} }
func handleCancelJob(conn net.Conn, req Request, manager *Manager) { func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64) jobID, err := params.Int(req.Params, "jobID")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'jobid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
jobID := int(jobIDFloat)
if err := manager.CancelJob(jobID); err != nil { if err := manager.CancelJob(jobID); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job canceled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
} }
func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) { func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -159,10 +152,10 @@ func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "jobs canceled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
@@ -193,7 +186,7 @@ func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetDevices(conn net.Conn, req Request, manager *Manager) { func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
devices, err := manager.GetDevices() devices, err := manager.GetDevices()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -202,7 +195,7 @@ func handleGetDevices(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, devices) models.Respond(conn, req.ID, devices)
} }
func handleGetPPDs(conn net.Conn, req Request, manager *Manager) { func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
ppds, err := manager.GetPPDs() ppds, err := manager.GetPPDs()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -211,7 +204,7 @@ func handleGetPPDs(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, ppds) models.Respond(conn, req.ID, ppds)
} }
func handleGetClasses(conn net.Conn, req Request, manager *Manager) { func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
classes, err := manager.GetClasses() classes, err := manager.GetClasses()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -220,41 +213,41 @@ func handleGetClasses(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, classes) models.Respond(conn, req.ID, classes)
} }
func handleCreatePrinter(conn net.Conn, req Request, manager *Manager) { func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
name, ok := req.Params["name"].(string) name, err := params.StringNonEmpty(req.Params, "name")
if !ok || name == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
deviceURI, ok := req.Params["deviceURI"].(string) deviceURI, err := params.StringNonEmpty(req.Params, "deviceURI")
if !ok || deviceURI == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'deviceURI' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
ppd, ok := req.Params["ppd"].(string) ppd, err := params.StringNonEmpty(req.Params, "ppd")
if !ok || ppd == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ppd' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
shared, _ := req.Params["shared"].(bool) shared := params.BoolOpt(req.Params, "shared", false)
errorPolicy, _ := req.Params["errorPolicy"].(string) errorPolicy := params.StringOpt(req.Params, "errorPolicy", "")
information, _ := req.Params["information"].(string) information := params.StringOpt(req.Params, "information", "")
location, _ := req.Params["location"].(string) location := params.StringOpt(req.Params, "location", "")
if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil { if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer created"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
} }
func handleDeletePrinter(conn net.Conn, req Request, manager *Manager) { func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -262,13 +255,13 @@ func handleDeletePrinter(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer deleted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
} }
func handleAcceptJobs(conn net.Conn, req Request, manager *Manager) { func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -276,13 +269,13 @@ func handleAcceptJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "accepting jobs"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
} }
func handleRejectJobs(conn net.Conn, req Request, manager *Manager) { func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -290,19 +283,19 @@ func handleRejectJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "rejecting jobs"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
} }
func handleSetPrinterShared(conn net.Conn, req Request, manager *Manager) { func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
shared, ok := req.Params["shared"].(bool) shared, err := params.Bool(req.Params, "shared")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'shared' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -310,19 +303,19 @@ func handleSetPrinterShared(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sharing updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
} }
func handleSetPrinterLocation(conn net.Conn, req Request, manager *Manager) { func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
location, ok := req.Params["location"].(string) location, err := params.String(req.Params, "location")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -330,19 +323,19 @@ func handleSetPrinterLocation(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
} }
func handleSetPrinterInfo(conn net.Conn, req Request, manager *Manager) { func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
info, ok := req.Params["info"].(string) info, err := params.String(req.Params, "info")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'info' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -350,39 +343,33 @@ func handleSetPrinterInfo(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "info updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
} }
func handleMoveJob(conn net.Conn, req Request, manager *Manager) { func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64) jobID, err := params.Int(req.Params, "jobID")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
destPrinter, ok := req.Params["destPrinter"].(string)
if !ok || destPrinter == "" {
models.RespondError(conn, req.ID, "missing or invalid 'destPrinter' parameter")
return
}
if err := manager.MoveJob(int(jobIDFloat), destPrinter); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job moved"})
destPrinter, err := params.StringNonEmpty(req.Params, "destPrinter")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.MoveJob(jobID, destPrinter); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
} }
type TestPageResult struct { func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
Success bool `json:"success"` printerName, err := params.StringNonEmpty(req.Params, "printerName")
JobID int `json:"jobId"` if err != nil {
Message string `json:"message"` models.RespondError(conn, req.ID, err.Error())
}
func handlePrintTestPage(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return return
} }
@@ -394,16 +381,16 @@ func handlePrintTestPage(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"}) models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
} }
func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) { func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
className, ok := req.Params["className"].(string) className, err := params.StringNonEmpty(req.Params, "className")
if !ok || className == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -411,19 +398,19 @@ func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer added to class"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
} }
func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager) { func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
className, ok := req.Params["className"].(string) className, err := params.StringNonEmpty(req.Params, "className")
if !ok || className == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -431,13 +418,13 @@ func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager)
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer removed from class"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
} }
func handleDeleteClass(conn net.Conn, req Request, manager *Manager) { func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
className, ok := req.Params["className"].(string) className, err := params.StringNonEmpty(req.Params, "className")
if !ok || className == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -445,38 +432,35 @@ func handleDeleteClass(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "class deleted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
} }
func handleRestartJob(conn net.Conn, req Request, manager *Manager) { func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64) jobID, err := params.Int(req.Params, "jobID")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
if err := manager.RestartJob(int(jobIDFloat)); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn net.Conn, req Request, manager *Manager) { if err := manager.RestartJob(jobID); err != nil {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
holdUntil, _ := req.Params["holdUntil"].(string)
if holdUntil == "" {
holdUntil = "indefinite"
}
if err := manager.HoldJob(int(jobIDFloat), holdUntil); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job held"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
holdUntil := params.StringOpt(req.Params, "holdUntil", "indefinite")
if err := manager.HoldJob(jobID, holdUntil); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
} }
+46 -46
View File
@@ -43,7 +43,7 @@ func TestHandleGetPrinters(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getPrinters", Method: "cups.getPrinters",
} }
@@ -68,7 +68,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getPrinters", Method: "cups.getPrinters",
} }
@@ -100,7 +100,7 @@ func TestHandleGetJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getJobs", Method: "cups.getJobs",
Params: map[string]any{ Params: map[string]any{
@@ -127,7 +127,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getJobs", Method: "cups.getJobs",
Params: map[string]any{}, Params: map[string]any{},
@@ -152,7 +152,7 @@ func TestHandlePausePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.pausePrinter", Method: "cups.pausePrinter",
Params: map[string]any{ Params: map[string]any{
@@ -162,7 +162,7 @@ func TestHandlePausePrinter(t *testing.T) {
handlePausePrinter(conn, req, m) handlePausePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -179,7 +179,7 @@ func TestHandleResumePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.resumePrinter", Method: "cups.resumePrinter",
Params: map[string]any{ Params: map[string]any{
@@ -189,7 +189,7 @@ func TestHandleResumePrinter(t *testing.T) {
handleResumePrinter(conn, req, m) handleResumePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -206,7 +206,7 @@ func TestHandleCancelJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.cancelJob", Method: "cups.cancelJob",
Params: map[string]any{ Params: map[string]any{
@@ -216,7 +216,7 @@ func TestHandleCancelJob(t *testing.T) {
handleCancelJob(conn, req, m) handleCancelJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -233,7 +233,7 @@ func TestHandlePurgeJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.purgeJobs", Method: "cups.purgeJobs",
Params: map[string]any{ Params: map[string]any{
@@ -243,7 +243,7 @@ func TestHandlePurgeJobs(t *testing.T) {
handlePurgeJobs(conn, req, m) handlePurgeJobs(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -260,7 +260,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.unknownMethod", Method: "cups.unknownMethod",
} }
@@ -287,7 +287,7 @@ func TestHandleGetDevices(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getDevices"} req := models.Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m) handleGetDevices(conn, req, m)
var resp models.Response[[]Device] var resp models.Response[[]Device]
@@ -309,7 +309,7 @@ func TestHandleGetPPDs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getPPDs"} req := models.Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m) handleGetPPDs(conn, req, m)
var resp models.Response[[]PPD] var resp models.Response[[]PPD]
@@ -332,7 +332,7 @@ func TestHandleGetClasses(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getClasses"} req := models.Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m) handleGetClasses(conn, req, m)
var resp models.Response[[]PrinterClass] var resp models.Response[[]PrinterClass]
@@ -353,7 +353,7 @@ func TestHandleCreatePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.createPrinter", Method: "cups.createPrinter",
Params: map[string]any{ Params: map[string]any{
@@ -364,7 +364,7 @@ func TestHandleCreatePrinter(t *testing.T) {
} }
handleCreatePrinter(conn, req, m) handleCreatePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -377,7 +377,7 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}} req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m) handleCreatePrinter(conn, req, m)
var resp models.Response[any] var resp models.Response[any]
@@ -396,14 +396,14 @@ func TestHandleDeletePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.deletePrinter", Method: "cups.deletePrinter",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
} }
handleDeletePrinter(conn, req, m) handleDeletePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -419,14 +419,14 @@ func TestHandleAcceptJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.acceptJobs", Method: "cups.acceptJobs",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
} }
handleAcceptJobs(conn, req, m) handleAcceptJobs(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -442,14 +442,14 @@ func TestHandleRejectJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.rejectJobs", Method: "cups.rejectJobs",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
} }
handleRejectJobs(conn, req, m) handleRejectJobs(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -465,14 +465,14 @@ func TestHandleSetPrinterShared(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.setPrinterShared", Method: "cups.setPrinterShared",
Params: map[string]any{"printerName": "printer1", "shared": true}, Params: map[string]any{"printerName": "printer1", "shared": true},
} }
handleSetPrinterShared(conn, req, m) handleSetPrinterShared(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -488,14 +488,14 @@ func TestHandleSetPrinterLocation(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.setPrinterLocation", Method: "cups.setPrinterLocation",
Params: map[string]any{"printerName": "printer1", "location": "Office"}, Params: map[string]any{"printerName": "printer1", "location": "Office"},
} }
handleSetPrinterLocation(conn, req, m) handleSetPrinterLocation(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -511,14 +511,14 @@ func TestHandleSetPrinterInfo(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.setPrinterInfo", Method: "cups.setPrinterInfo",
Params: map[string]any{"printerName": "printer1", "info": "Main Printer"}, Params: map[string]any{"printerName": "printer1", "info": "Main Printer"},
} }
handleSetPrinterInfo(conn, req, m) handleSetPrinterInfo(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -534,14 +534,14 @@ func TestHandleMoveJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.moveJob", Method: "cups.moveJob",
Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"}, Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"},
} }
handleMoveJob(conn, req, m) handleMoveJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -557,7 +557,7 @@ func TestHandlePrintTestPage(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.printTestPage", Method: "cups.printTestPage",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
@@ -581,14 +581,14 @@ func TestHandleAddPrinterToClass(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.addPrinterToClass", Method: "cups.addPrinterToClass",
Params: map[string]any{"className": "office", "printerName": "printer1"}, Params: map[string]any{"className": "office", "printerName": "printer1"},
} }
handleAddPrinterToClass(conn, req, m) handleAddPrinterToClass(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -604,14 +604,14 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.removePrinterFromClass", Method: "cups.removePrinterFromClass",
Params: map[string]any{"className": "office", "printerName": "printer1"}, Params: map[string]any{"className": "office", "printerName": "printer1"},
} }
handleRemovePrinterFromClass(conn, req, m) handleRemovePrinterFromClass(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -627,14 +627,14 @@ func TestHandleDeleteClass(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.deleteClass", Method: "cups.deleteClass",
Params: map[string]any{"className": "office"}, Params: map[string]any{"className": "office"},
} }
handleDeleteClass(conn, req, m) handleDeleteClass(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -650,14 +650,14 @@ func TestHandleRestartJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.restartJob", Method: "cups.restartJob",
Params: map[string]any{"jobID": float64(1)}, Params: map[string]any{"jobID": float64(1)},
} }
handleRestartJob(conn, req, m) handleRestartJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -673,14 +673,14 @@ func TestHandleHoldJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.holdJob", Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1)}, Params: map[string]any{"jobID": float64(1)},
} }
handleHoldJob(conn, req, m) handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -696,14 +696,14 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.holdJob", Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"}, Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"},
} }
handleHoldJob(conn, req, m) handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
+6 -12
View File
@@ -8,18 +8,12 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct { type SuccessResult struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized") models.RespondError(conn, req.ID, "dwl manager not initialized")
return return
@@ -41,12 +35,12 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() state := manager.GetState()
models.Respond(conn, req.ID, state) models.Respond(conn, req.ID, state)
} }
func handleSetTags(conn net.Conn, req Request, manager *Manager) { func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string) output, ok := req.Params["output"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -73,7 +67,7 @@ func handleSetTags(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"})
} }
func handleSetClientTags(conn net.Conn, req Request, manager *Manager) { func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string) output, ok := req.Params["output"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -100,7 +94,7 @@ func handleSetClientTags(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"})
} }
func handleSetLayout(conn net.Conn, req Request, manager *Manager) { func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string) output, ok := req.Params["output"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -121,7 +115,7 @@ func handleSetLayout(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+4 -11
View File
@@ -6,22 +6,15 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
ID any `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, m *Manager) {
switch req.Method { switch req.Method {
case "evdev.getState": case "evdev.getState":
handleGetState(conn, req, m) handleGetState(conn, req, m)
default: default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method) models.RespondError(conn, req.ID, "unknown method: "+req.Method)
} }
} }
func handleGetState(conn net.Conn, req Request, m *Manager) { func handleGetState(conn net.Conn, req models.Request, m *Manager) {
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
+3 -3
View File
@@ -53,7 +53,7 @@ func TestHandleRequest(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "evdev.getState", Method: "evdev.getState",
Params: map[string]any{}, Params: map[string]any{},
@@ -82,7 +82,7 @@ func TestHandleRequest(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 456, ID: 456,
Method: "evdev.unknownMethod", Method: "evdev.unknownMethod",
Params: map[string]any{}, Params: map[string]any{},
@@ -111,7 +111,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 789, ID: 789,
Method: "evdev.getState", Method: "evdev.getState",
Params: map[string]any{}, Params: map[string]any{},
+9
View File
@@ -306,6 +306,15 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey] capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }
+7 -13
View File
@@ -8,18 +8,12 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct { type SuccessResult struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "extworkspace manager not initialized") models.RespondError(conn, req.ID, "extworkspace manager not initialized")
return return
@@ -43,12 +37,12 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() state := manager.GetState()
models.Respond(conn, req.ID, state) models.Respond(conn, req.ID, state)
} }
func handleActivateWorkspace(conn net.Conn, req Request, manager *Manager) { func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
groupID = "" groupID = ""
@@ -68,7 +62,7 @@ func handleActivateWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"})
} }
func handleDeactivateWorkspace(conn net.Conn, req Request, manager *Manager) { func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
groupID = "" groupID = ""
@@ -88,7 +82,7 @@ func handleDeactivateWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"})
} }
func handleRemoveWorkspace(conn net.Conn, req Request, manager *Manager) { func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
groupID = "" groupID = ""
@@ -108,7 +102,7 @@ func handleRemoveWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"})
} }
func handleCreateWorkspace(conn net.Conn, req Request, manager *Manager) { func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter") models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
@@ -129,7 +123,7 @@ func handleCreateWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+40 -52
View File
@@ -5,21 +5,10 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Value string `json:"value,omitempty"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "freedesktop.getState": case "freedesktop.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -44,15 +33,14 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleSetIconFile(conn net.Conn, req Request, manager *Manager) { func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
iconPath, ok := req.Params["path"].(string) iconPath, err := params.String(req.Params, "path")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'path' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -61,13 +49,13 @@ func handleSetIconFile(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon file set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
} }
func handleSetRealName(conn net.Conn, req Request, manager *Manager) { func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
name, ok := req.Params["name"].(string) name, err := params.String(req.Params, "name")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -76,13 +64,13 @@ func handleSetRealName(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "real name set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
} }
func handleSetEmail(conn net.Conn, req Request, manager *Manager) { func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
email, ok := req.Params["email"].(string) email, err := params.String(req.Params, "email")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'email' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -91,13 +79,13 @@ func handleSetEmail(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "email set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
} }
func handleSetLanguage(conn net.Conn, req Request, manager *Manager) { func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
language, ok := req.Params["language"].(string) language, err := params.String(req.Params, "language")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'language' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -106,13 +94,13 @@ func handleSetLanguage(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "language set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
} }
func handleSetLocation(conn net.Conn, req Request, manager *Manager) { func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
location, ok := req.Params["location"].(string) location, err := params.String(req.Params, "location")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -121,13 +109,13 @@ func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
} }
func handleGetUserIconFile(conn net.Conn, req Request, manager *Manager) { func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
username, ok := req.Params["username"].(string) username, err := params.String(req.Params, "username")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'username' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -137,10 +125,10 @@ func handleGetUserIconFile(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Value: iconFile}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
} }
func handleGetColorScheme(conn net.Conn, req Request, manager *Manager) { func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.updateSettingsState(); err != nil { if err := manager.updateSettingsState(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -150,10 +138,10 @@ func handleGetColorScheme(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme}) models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
} }
func handleSetIconTheme(conn net.Conn, req Request, manager *Manager) { func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
iconTheme, ok := req.Params["iconTheme"].(string) iconTheme, err := params.String(req.Params, "iconTheme")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'iconTheme' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -162,5 +150,5 @@ func handleSetIconTheme(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon theme set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon theme set"})
} }
@@ -74,10 +74,10 @@ func TestRespondError_Freedesktop(t *testing.T) {
func TestRespond_Freedesktop(t *testing.T) { func TestRespond_Freedesktop(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
result := SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -106,7 +106,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "freedesktop.getState"} req := models.Request{ID: 123, Method: "freedesktop.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -131,7 +131,7 @@ func TestHandleSetIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{}, Params: map[string]any{},
@@ -164,7 +164,7 @@ func TestHandleSetIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{ Params: map[string]any{
@@ -174,7 +174,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -196,7 +196,7 @@ func TestHandleSetIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{ Params: map[string]any{
@@ -206,7 +206,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -223,7 +223,7 @@ func TestHandleSetRealName(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setRealName", Method: "freedesktop.accounts.setRealName",
Params: map[string]any{}, Params: map[string]any{},
@@ -256,7 +256,7 @@ func TestHandleSetRealName(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setRealName", Method: "freedesktop.accounts.setRealName",
Params: map[string]any{ Params: map[string]any{
@@ -266,7 +266,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager) handleSetRealName(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -286,7 +286,7 @@ func TestHandleSetEmail(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setEmail", Method: "freedesktop.accounts.setEmail",
Params: map[string]any{}, Params: map[string]any{},
@@ -319,7 +319,7 @@ func TestHandleSetEmail(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setEmail", Method: "freedesktop.accounts.setEmail",
Params: map[string]any{ Params: map[string]any{
@@ -329,7 +329,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager) handleSetEmail(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -349,7 +349,7 @@ func TestHandleSetLanguage(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setLanguage", Method: "freedesktop.accounts.setLanguage",
Params: map[string]any{}, Params: map[string]any{},
@@ -374,7 +374,7 @@ func TestHandleSetLocation(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setLocation", Method: "freedesktop.accounts.setLocation",
Params: map[string]any{}, Params: map[string]any{},
@@ -399,7 +399,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.getUserIconFile", Method: "freedesktop.accounts.getUserIconFile",
Params: map[string]any{}, Params: map[string]any{},
@@ -426,7 +426,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.getUserIconFile", Method: "freedesktop.accounts.getUserIconFile",
Params: map[string]any{ Params: map[string]any{
@@ -436,7 +436,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager) handleGetUserIconFile(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -457,7 +457,7 @@ func TestHandleGetColorScheme(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager) handleGetColorScheme(conn, req, manager)
@@ -488,7 +488,7 @@ func TestHandleGetColorScheme(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager) handleGetColorScheme(conn, req, manager)
@@ -516,7 +516,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.unknown", Method: "freedesktop.unknown",
} }
@@ -533,7 +533,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.getState", Method: "freedesktop.getState",
} }
@@ -561,7 +561,7 @@ func TestHandleRequest(t *testing.T) {
for _, method := range tests { for _, method := range tests {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: method, Method: method,
Params: map[string]any{}, Params: map[string]any{},
+30 -41
View File
@@ -6,20 +6,10 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "loginctl.getState": case "loginctl.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -46,39 +36,38 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleLock(conn net.Conn, req Request, manager *Manager) { func handleLock(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Lock(); err != nil { if err := manager.Lock(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "locked"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked"})
} }
func handleUnlock(conn net.Conn, req Request, manager *Manager) { func handleUnlock(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Unlock(); err != nil { if err := manager.Unlock(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "unlocked"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "unlocked"})
} }
func handleActivate(conn net.Conn, req Request, manager *Manager) { func handleActivate(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Activate(); err != nil { if err := manager.Activate(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "activated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "activated"})
} }
func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) { func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
idle, ok := req.Params["idle"].(bool) idle, err := params.Bool(req.Params, "idle")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'idle' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -86,32 +75,32 @@ func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "idle hint set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
} }
func handleSetLockBeforeSuspend(conn net.Conn, req Request, manager *Manager) { func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool) enabled, err := params.Bool(req.Params, "enabled")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetLockBeforeSuspend(enabled) manager.SetLockBeforeSuspend(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "lock before suspend set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"})
} }
func handleSetSleepInhibitorEnabled(conn net.Conn, req Request, manager *Manager) { func handleSetSleepInhibitorEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool) enabled, err := params.Bool(req.Params, "enabled")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetSleepInhibitorEnabled(enabled) manager.SetSleepInhibitorEnabled(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sleep inhibitor setting updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
} }
func handleLockerReady(conn net.Conn, req Request, manager *Manager) { func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) {
manager.lockTimerMu.Lock() manager.lockTimerMu.Lock()
if manager.lockTimer != nil { if manager.lockTimer != nil {
manager.lockTimer.Stop() manager.lockTimer.Stop()
@@ -125,18 +114,18 @@ func handleLockerReady(conn net.Conn, req Request, manager *Manager) {
if manager.inSleepCycle.Load() { if manager.inSleepCycle.Load() {
manager.signalLockerReady() manager.signalLockerReady()
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "ok"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "ok"})
} }
func handleTerminate(conn net.Conn, req Request, manager *Manager) { func handleTerminate(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Terminate(); err != nil { if err := manager.Terminate(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "terminated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "terminated"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+28 -28
View File
@@ -58,10 +58,10 @@ func TestRespondError_Loginctl(t *testing.T) {
func TestRespond_Loginctl(t *testing.T) { func TestRespond_Loginctl(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
result := SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -86,7 +86,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.getState"} req := models.Request{ID: 123, Method: "loginctl.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -115,10 +115,10 @@ func TestHandleLock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.lock"} req := models.Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager) handleLock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -141,10 +141,10 @@ func TestHandleLock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.lock"} req := models.Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager) handleLock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -166,10 +166,10 @@ func TestHandleUnlock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.unlock"} req := models.Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager) handleUnlock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -192,10 +192,10 @@ func TestHandleUnlock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.unlock"} req := models.Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager) handleUnlock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -217,10 +217,10 @@ func TestHandleActivate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.activate"} req := models.Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager) handleActivate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -243,10 +243,10 @@ func TestHandleActivate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.activate"} req := models.Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager) handleActivate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -263,7 +263,7 @@ func TestHandleSetIdleHint(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.setIdleHint", Method: "loginctl.setIdleHint",
Params: map[string]any{}, Params: map[string]any{},
@@ -291,7 +291,7 @@ func TestHandleSetIdleHint(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.setIdleHint", Method: "loginctl.setIdleHint",
Params: map[string]any{ Params: map[string]any{
@@ -301,7 +301,7 @@ func TestHandleSetIdleHint(t *testing.T) {
handleSetIdleHint(conn, req, manager) handleSetIdleHint(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -324,7 +324,7 @@ func TestHandleSetIdleHint(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.setIdleHint", Method: "loginctl.setIdleHint",
Params: map[string]any{ Params: map[string]any{
@@ -334,7 +334,7 @@ func TestHandleSetIdleHint(t *testing.T) {
handleSetIdleHint(conn, req, manager) handleSetIdleHint(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -356,10 +356,10 @@ func TestHandleTerminate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.terminate"} req := models.Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager) handleTerminate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -382,10 +382,10 @@ func TestHandleTerminate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.terminate"} req := models.Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager) handleTerminate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -405,7 +405,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.unknown", Method: "loginctl.unknown",
} }
@@ -422,7 +422,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.getState", Method: "loginctl.getState",
} }
@@ -445,7 +445,7 @@ func TestHandleRequest(t *testing.T) {
manager.sessionObj = mockSessionObj manager.sessionObj = mockSessionObj
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.lock", Method: "loginctl.lock",
} }
@@ -470,7 +470,7 @@ func TestHandleSubscribe(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.subscribe"} req := models.Request{ID: 123, Method: "loginctl.subscribe"}
done := make(chan bool) done := make(chan bool)
go func() { go func() {
+6
View File
@@ -29,3 +29,9 @@ func Respond[T any](conn net.Conn, id int, result T) {
resp := Response[T]{ID: id, Result: &result} resp := Response[T]{ID: id, Result: &result}
json.NewEncoder(conn).Encode(resp) json.NewEncoder(conn).Encode(resp)
} }
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Value string `json:"value,omitempty"`
}
+123 -210
View File
@@ -7,20 +7,10 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "network.getState": case "network.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -89,32 +79,22 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleCredentialsSubmit(conn net.Conn, req Request, manager *Manager) { func handleCredentialsSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
log.Warnf("handleCredentialsSubmit: missing or invalid token parameter") log.Warnf("handleCredentialsSubmit: missing or invalid token parameter")
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
secretsRaw, ok := req.Params["secrets"].(map[string]any) secrets, err := params.StringMap(req.Params, "secrets")
if !ok { if err != nil {
log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter") log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter")
models.RespondError(conn, req.ID, "missing or invalid 'secrets' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
secrets := make(map[string]string) save := params.BoolOpt(req.Params, "save", true)
for k, v := range secretsRaw {
if str, ok := v.(string); ok {
secrets[k] = str
}
}
save := true
if saveParam, ok := req.Params["save"].(bool); ok {
save = saveParam
}
if err := manager.SubmitCredentials(token, secrets, save); err != nil { if err := manager.SubmitCredentials(token, secrets, save); err != nil {
log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err) log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err)
@@ -123,13 +103,13 @@ func handleCredentialsSubmit(conn net.Conn, req Request, manager *Manager) {
} }
log.Infof("handleCredentialsSubmit: credentials submitted successfully") log.Infof("handleCredentialsSubmit: credentials submitted successfully")
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials submitted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials submitted"})
} }
func handleCredentialsCancel(conn net.Conn, req Request, manager *Manager) { func handleCredentialsCancel(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -138,16 +118,15 @@ func handleCredentialsCancel(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials cancelled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials cancelled"})
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleScanWiFi(conn net.Conn, req Request, manager *Manager) { func handleScanWiFi(conn net.Conn, req models.Request, manager *Manager) {
device, _ := req.Params["device"].(string) device := params.StringOpt(req.Params, "device", "")
var err error var err error
if device != "" { if device != "" {
err = manager.ScanWiFiDevice(device) err = manager.ScanWiFiDevice(device)
@@ -158,33 +137,25 @@ func handleScanWiFi(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "scanning"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "scanning"})
} }
func handleGetWiFiNetworks(conn net.Conn, req Request, manager *Manager) { func handleGetWiFiNetworks(conn net.Conn, req models.Request, manager *Manager) {
networks := manager.GetWiFiNetworks() models.Respond(conn, req.ID, manager.GetWiFiNetworks())
models.Respond(conn, req.ID, networks)
} }
func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) { func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
var connReq ConnectionRequest var connReq ConnectionRequest
connReq.SSID = ssid connReq.SSID = ssid
connReq.Password = params.StringOpt(req.Params, "password", "")
if password, ok := req.Params["password"].(string); ok { connReq.Username = params.StringOpt(req.Params, "username", "")
connReq.Password = password connReq.Device = params.StringOpt(req.Params, "device", "")
}
if username, ok := req.Params["username"].(string); ok {
connReq.Username = username
}
if device, ok := req.Params["device"].(string); ok {
connReq.Device = device
}
if interactive, ok := req.Params["interactive"].(bool); ok { if interactive, ok := req.Params["interactive"].(bool); ok {
connReq.Interactive = interactive connReq.Interactive = interactive
@@ -206,27 +177,14 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
} }
} }
if anonymousIdentity, ok := req.Params["anonymousIdentity"].(string); ok { connReq.AnonymousIdentity = params.StringOpt(req.Params, "anonymousIdentity", "")
connReq.AnonymousIdentity = anonymousIdentity connReq.DomainSuffixMatch = params.StringOpt(req.Params, "domainSuffixMatch", "")
} connReq.EAPMethod = params.StringOpt(req.Params, "eapMethod", "")
if domainSuffixMatch, ok := req.Params["domainSuffixMatch"].(string); ok { connReq.Phase2Auth = params.StringOpt(req.Params, "phase2Auth", "")
connReq.DomainSuffixMatch = domainSuffixMatch connReq.CACertPath = params.StringOpt(req.Params, "caCertPath", "")
} connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "")
if eapMethod, ok := req.Params["eapMethod"].(string); ok { connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "")
connReq.EAPMethod = eapMethod
}
if phase2Auth, ok := req.Params["phase2Auth"].(string); ok {
connReq.Phase2Auth = phase2Auth
}
if caCertPath, ok := req.Params["caCertPath"].(string); ok {
connReq.CACertPath = caCertPath
}
if clientCertPath, ok := req.Params["clientCertPath"].(string); ok {
connReq.ClientCertPath = clientCertPath
}
if privateKeyPath, ok := req.Params["privateKeyPath"].(string); ok {
connReq.PrivateKeyPath = privateKeyPath
}
if useSystemCACerts, ok := req.Params["useSystemCACerts"].(bool); ok { if useSystemCACerts, ok := req.Params["useSystemCACerts"].(bool); ok {
connReq.UseSystemCACerts = &useSystemCACerts connReq.UseSystemCACerts = &useSystemCACerts
} }
@@ -236,11 +194,11 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) { func handleDisconnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
device, _ := req.Params["device"].(string) device := params.StringOpt(req.Params, "device", "")
var err error var err error
if device != "" { if device != "" {
err = manager.DisconnectWiFiDevice(device) err = manager.DisconnectWiFiDevice(device)
@@ -251,13 +209,13 @@ func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
} }
func handleForgetWiFi(conn net.Conn, req Request, manager *Manager) { func handleForgetWiFi(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -266,10 +224,10 @@ func handleForgetWiFi(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "forgotten"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "forgotten"})
} }
func handleToggleWiFi(conn net.Conn, req Request, manager *Manager) { func handleToggleWiFi(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.ToggleWiFi(); err != nil { if err := manager.ToggleWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -279,7 +237,7 @@ func handleToggleWiFi(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled}) models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled})
} }
func handleEnableWiFi(conn net.Conn, req Request, manager *Manager) { func handleEnableWiFi(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.EnableWiFi(); err != nil { if err := manager.EnableWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -287,7 +245,7 @@ func handleEnableWiFi(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": true}) models.Respond(conn, req.ID, map[string]bool{"enabled": true})
} }
func handleDisableWiFi(conn net.Conn, req Request, manager *Manager) { func handleDisableWiFi(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.DisableWiFi(); err != nil { if err := manager.DisableWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -295,29 +253,29 @@ func handleDisableWiFi(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": false}) models.Respond(conn, req.ID, map[string]bool{"enabled": false})
} }
func handleConnectEthernetSpecificConfig(conn net.Conn, req Request, manager *Manager) { func handleConnectEthernetSpecificConfig(conn net.Conn, req models.Request, manager *Manager) {
uuid, ok := req.Params["uuid"].(string) uuid, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
if err := manager.activateConnection(uuid); err != nil { if err := manager.activateConnection(uuid); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleConnectEthernet(conn net.Conn, req Request, manager *Manager) { func handleConnectEthernet(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.ConnectEthernet(); err != nil { if err := manager.ConnectEthernet(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleDisconnectEthernet(conn net.Conn, req Request, manager *Manager) { func handleDisconnectEthernet(conn net.Conn, req models.Request, manager *Manager) {
device, _ := req.Params["device"].(string) device := params.StringOpt(req.Params, "device", "")
var err error var err error
if device != "" { if device != "" {
err = manager.DisconnectEthernetDevice(device) err = manager.DisconnectEthernetDevice(device)
@@ -328,13 +286,13 @@ func handleDisconnectEthernet(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
} }
func handleSetPreference(conn net.Conn, req Request, manager *Manager) { func handleSetPreference(conn net.Conn, req models.Request, manager *Manager) {
preference, ok := req.Params["preference"].(string) preference, err := params.String(req.Params, "preference")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'preference' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -346,10 +304,10 @@ func handleSetPreference(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]string{"preference": preference}) models.Respond(conn, req.ID, map[string]string{"preference": preference})
} }
func handleGetNetworkInfo(conn net.Conn, req Request, manager *Manager) { func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -362,10 +320,10 @@ func handleGetNetworkInfo(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, network) models.Respond(conn, req.ID, network)
} }
func handleGetWiredNetworkInfo(conn net.Conn, req Request, manager *Manager) { func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
uuid, ok := req.Params["uuid"].(string) uuid, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -378,7 +336,7 @@ func handleGetWiredNetworkInfo(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, network) models.Respond(conn, req.ID, network)
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
@@ -408,7 +366,7 @@ func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleListVPNProfiles(conn net.Conn, req Request, manager *Manager) { func handleListVPNProfiles(conn net.Conn, req models.Request, manager *Manager) {
profiles, err := manager.ListVPNProfiles() profiles, err := manager.ListVPNProfiles()
if err != nil { if err != nil {
log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err) log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err)
@@ -419,7 +377,7 @@ func handleListVPNProfiles(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, profiles) models.Respond(conn, req.ID, profiles)
} }
func handleListActiveVPN(conn net.Conn, req Request, manager *Manager) { func handleListActiveVPN(conn net.Conn, req models.Request, manager *Manager) {
active, err := manager.ListActiveVPN() active, err := manager.ListActiveVPN()
if err != nil { if err != nil {
log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err) log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err)
@@ -430,27 +388,15 @@ func handleListActiveVPN(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, active) models.Respond(conn, req.ID, active)
} }
func handleConnectVPN(conn net.Conn, req Request, manager *Manager) { func handleConnectVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuidOrName"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid")
if !ok { if !ok {
name, nameOk := req.Params["name"].(string) log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter")
uuid, uuidOk := req.Params["uuid"].(string) models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
if nameOk { return
uuidOrName = name
} else if uuidOk {
uuidOrName = uuid
} else {
log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
return
}
} }
// Default to true - only allow one VPN connection at a time singleActive := params.BoolOpt(req.Params, "singleActive", true)
singleActive := true
if sa, ok := req.Params["singleActive"].(bool); ok {
singleActive = sa
}
if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil { if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil {
log.Warnf("handleConnectVPN: failed to connect: %v", err) log.Warnf("handleConnectVPN: failed to connect: %v", err)
@@ -458,23 +404,15 @@ func handleConnectVPN(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN connection initiated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN connection initiated"})
} }
func handleDisconnectVPN(conn net.Conn, req Request, manager *Manager) { func handleDisconnectVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuidOrName"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid")
if !ok { if !ok {
name, nameOk := req.Params["name"].(string) log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter")
uuid, uuidOk := req.Params["uuid"].(string) models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
if nameOk { return
uuidOrName = name
} else if uuidOk {
uuidOrName = uuid
} else {
log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
return
}
} }
if err := manager.DisconnectVPN(uuidOrName); err != nil { if err := manager.DisconnectVPN(uuidOrName); err != nil {
@@ -483,27 +421,21 @@ func handleDisconnectVPN(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN disconnected"})
} }
func handleDisconnectAllVPN(conn net.Conn, req Request, manager *Manager) { func handleDisconnectAllVPN(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.DisconnectAllVPN(); err != nil { if err := manager.DisconnectAllVPN(); err != nil {
log.Warnf("handleDisconnectAllVPN: failed: %v", err) log.Warnf("handleDisconnectAllVPN: failed: %v", err)
models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err))
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "All VPNs disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "All VPNs disconnected"})
} }
func handleClearVPNCredentials(conn net.Conn, req Request, manager *Manager) { func handleClearVPNCredentials(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok { if !ok {
log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter") log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter") models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter")
@@ -516,19 +448,19 @@ func handleClearVPNCredentials(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials cleared"})
} }
func handleSetWiFiAutoconnect(conn net.Conn, req Request, manager *Manager) { func handleSetWiFiAutoconnect(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
autoconnect, ok := req.Params["autoconnect"].(bool) autoconnect, err := params.Bool(req.Params, "autoconnect")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'autoconnect' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -537,10 +469,10 @@ func handleSetWiFiAutoconnect(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "autoconnect updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "autoconnect updated"})
} }
func handleListVPNPlugins(conn net.Conn, req Request, manager *Manager) { func handleListVPNPlugins(conn net.Conn, req models.Request, manager *Manager) {
plugins, err := manager.ListVPNPlugins() plugins, err := manager.ListVPNPlugins()
if err != nil { if err != nil {
log.Warnf("handleListVPNPlugins: failed to list plugins: %v", err) log.Warnf("handleListVPNPlugins: failed to list plugins: %v", err)
@@ -551,17 +483,14 @@ func handleListVPNPlugins(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, plugins) models.Respond(conn, req.ID, plugins)
} }
func handleImportVPN(conn net.Conn, req Request, manager *Manager) { func handleImportVPN(conn net.Conn, req models.Request, manager *Manager) {
filePath, ok := req.Params["file"].(string) filePath, ok := params.StringAlt(req.Params, "file", "path")
if !ok {
filePath, ok = req.Params["path"].(string)
}
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'file' or 'path' parameter") models.RespondError(conn, req.ID, "missing 'file' or 'path' parameter")
return return
} }
name, _ := req.Params["name"].(string) name := params.StringOpt(req.Params, "name", "")
result, err := manager.ImportVPN(filePath, name) result, err := manager.ImportVPN(filePath, name)
if err != nil { if err != nil {
@@ -573,14 +502,8 @@ func handleImportVPN(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, result) models.Respond(conn, req.ID, result)
} }
func handleGetVPNConfig(conn net.Conn, req Request, manager *Manager) { func handleGetVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter") models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter")
return return
@@ -596,10 +519,10 @@ func handleGetVPNConfig(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, config) models.Respond(conn, req.ID, config)
} }
func handleUpdateVPNConfig(conn net.Conn, req Request, manager *Manager) { func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
connUUID, ok := req.Params["uuid"].(string) connUUID, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -626,17 +549,11 @@ func handleUpdateVPNConfig(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN config updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN config updated"})
} }
func handleDeleteVPN(conn net.Conn, req Request, manager *Manager) { func handleDeleteVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter") models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter")
return return
@@ -648,23 +565,19 @@ func handleDeleteVPN(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN deleted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN deleted"})
} }
func handleSetVPNCredentials(conn net.Conn, req Request, manager *Manager) { func handleSetVPNCredentials(conn net.Conn, req models.Request, manager *Manager) {
connUUID, ok := req.Params["uuid"].(string) connUUID, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
username, _ := req.Params["username"].(string) username := params.StringOpt(req.Params, "username", "")
password, _ := req.Params["password"].(string) password := params.StringOpt(req.Params, "password", "")
save := params.BoolOpt(req.Params, "save", true)
save := true
if saveParam, ok := req.Params["save"].(bool); ok {
save = saveParam
}
if err := manager.SetVPNCredentials(connUUID, username, password, save); err != nil { if err := manager.SetVPNCredentials(connUUID, username, password, save); err != nil {
log.Warnf("handleSetVPNCredentials: failed to set credentials: %v", err) log.Warnf("handleSetVPNCredentials: failed to set credentials: %v", err)
@@ -672,5 +585,5 @@ func handleSetVPNCredentials(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials set"})
} }
@@ -53,10 +53,10 @@ func TestRespondError_Network(t *testing.T) {
func TestRespond_Network(t *testing.T) { func TestRespond_Network(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
result := SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -77,7 +77,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "network.getState"} req := models.Request{ID: 123, Method: "network.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -103,7 +103,7 @@ func TestHandleGetWiFiNetworks(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "network.wifi.networks"} req := models.Request{ID: 123, Method: "network.wifi.networks"}
handleGetWiFiNetworks(conn, req, manager) handleGetWiFiNetworks(conn, req, manager)
@@ -125,7 +125,7 @@ func TestHandleConnectWiFi(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.wifi.connect", Method: "network.wifi.connect",
Params: map[string]any{}, Params: map[string]any{},
@@ -149,7 +149,7 @@ func TestHandleSetPreference(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.preference.set", Method: "network.preference.set",
Params: map[string]any{}, Params: map[string]any{},
@@ -173,7 +173,7 @@ func TestHandleGetNetworkInfo(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.info", Method: "network.info",
Params: map[string]any{}, Params: map[string]any{},
@@ -199,7 +199,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.unknown", Method: "network.unknown",
} }
@@ -216,7 +216,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.getState", Method: "network.getState",
} }
+113
View File
@@ -0,0 +1,113 @@
package params
import "fmt"
func Get[T any](params map[string]any, key string) (T, error) {
val, ok := params[key].(T)
if !ok {
var zero T
return zero, fmt.Errorf("missing or invalid '%s' parameter", key)
}
return val, nil
}
func GetOpt[T any](params map[string]any, key string, def T) T {
if val, ok := params[key].(T); ok {
return val
}
return def
}
func String(params map[string]any, key string) (string, error) {
return Get[string](params, key)
}
func StringNonEmpty(params map[string]any, key string) (string, error) {
val, err := Get[string](params, key)
if err != nil || val == "" {
return "", fmt.Errorf("missing or invalid '%s' parameter", key)
}
return val, nil
}
func StringOpt(params map[string]any, key string, def string) string {
return GetOpt(params, key, def)
}
func Int(params map[string]any, key string) (int, error) {
val, err := Get[float64](params, key)
if err != nil {
return 0, err
}
return int(val), nil
}
func IntOpt(params map[string]any, key string, def int) int {
if val, ok := params[key].(float64); ok {
return int(val)
}
return def
}
func Float(params map[string]any, key string) (float64, error) {
return Get[float64](params, key)
}
func FloatOpt(params map[string]any, key string, def float64) float64 {
return GetOpt(params, key, def)
}
func Bool(params map[string]any, key string) (bool, error) {
return Get[bool](params, key)
}
func BoolOpt(params map[string]any, key string, def bool) bool {
return GetOpt(params, key, def)
}
func StringMap(params map[string]any, key string) (map[string]string, error) {
rawMap, err := Get[map[string]any](params, key)
if err != nil {
return nil, err
}
result := make(map[string]string)
for k, v := range rawMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result, nil
}
func StringMapOpt(params map[string]any, key string) map[string]string {
rawMap, ok := params[key].(map[string]any)
if !ok {
return nil
}
result := make(map[string]string)
for k, v := range rawMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result
}
func Any(params map[string]any, key string) (any, bool) {
val, ok := params[key]
return val, ok
}
func AnyMap(params map[string]any, key string) (map[string]any, bool) {
val, ok := params[key].(map[string]any)
return val, ok
}
func StringAlt(params map[string]any, keys ...string) (string, bool) {
for _, key := range keys {
if val, ok := params[key].(string); ok {
return val, true
}
}
return "", false
}
+154
View File
@@ -0,0 +1,154 @@
package params
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGet(t *testing.T) {
p := map[string]any{"key": "value"}
val, err := Get[string](p, "key")
assert.NoError(t, err)
assert.Equal(t, "value", val)
_, err = Get[string](p, "missing")
assert.Error(t, err)
_, err = Get[int](p, "key")
assert.Error(t, err)
}
func TestGetOpt(t *testing.T) {
p := map[string]any{"key": "value"}
assert.Equal(t, "value", GetOpt(p, "key", "default"))
assert.Equal(t, "default", GetOpt(p, "missing", "default"))
}
func TestString(t *testing.T) {
p := map[string]any{"s": "hello", "n": 123}
val, err := String(p, "s")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
_, err = String(p, "n")
assert.Error(t, err)
}
func TestStringNonEmpty(t *testing.T) {
p := map[string]any{"s": "hello", "empty": ""}
val, err := StringNonEmpty(p, "s")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
_, err = StringNonEmpty(p, "empty")
assert.Error(t, err)
_, err = StringNonEmpty(p, "missing")
assert.Error(t, err)
}
func TestStringOpt(t *testing.T) {
p := map[string]any{"s": "hello"}
assert.Equal(t, "hello", StringOpt(p, "s", "default"))
assert.Equal(t, "default", StringOpt(p, "missing", "default"))
}
func TestInt(t *testing.T) {
p := map[string]any{"n": float64(42), "s": "str"}
val, err := Int(p, "n")
assert.NoError(t, err)
assert.Equal(t, 42, val)
_, err = Int(p, "s")
assert.Error(t, err)
}
func TestIntOpt(t *testing.T) {
p := map[string]any{"n": float64(42)}
assert.Equal(t, 42, IntOpt(p, "n", 0))
assert.Equal(t, 99, IntOpt(p, "missing", 99))
}
func TestFloat(t *testing.T) {
p := map[string]any{"f": 3.14, "s": "str"}
val, err := Float(p, "f")
assert.NoError(t, err)
assert.Equal(t, 3.14, val)
_, err = Float(p, "s")
assert.Error(t, err)
}
func TestFloatOpt(t *testing.T) {
p := map[string]any{"f": 3.14}
assert.Equal(t, 3.14, FloatOpt(p, "f", 0))
assert.Equal(t, 1.0, FloatOpt(p, "missing", 1.0))
}
func TestBool(t *testing.T) {
p := map[string]any{"b": true, "s": "str"}
val, err := Bool(p, "b")
assert.NoError(t, err)
assert.True(t, val)
_, err = Bool(p, "s")
assert.Error(t, err)
}
func TestBoolOpt(t *testing.T) {
p := map[string]any{"b": true}
assert.True(t, BoolOpt(p, "b", false))
assert.True(t, BoolOpt(p, "missing", true))
}
func TestStringMap(t *testing.T) {
p := map[string]any{
"m": map[string]any{"a": "1", "b": "2", "c": 3},
}
val, err := StringMap(p, "m")
assert.NoError(t, err)
assert.Equal(t, map[string]string{"a": "1", "b": "2"}, val)
_, err = StringMap(p, "missing")
assert.Error(t, err)
}
func TestStringMapOpt(t *testing.T) {
p := map[string]any{
"m": map[string]any{"a": "1"},
}
assert.Equal(t, map[string]string{"a": "1"}, StringMapOpt(p, "m"))
assert.Nil(t, StringMapOpt(p, "missing"))
}
func TestAny(t *testing.T) {
p := map[string]any{"k": 123}
val, ok := Any(p, "k")
assert.True(t, ok)
assert.Equal(t, 123, val)
_, ok = Any(p, "missing")
assert.False(t, ok)
}
func TestAnyMap(t *testing.T) {
inner := map[string]any{"nested": true}
p := map[string]any{"m": inner}
val, ok := AnyMap(p, "m")
assert.True(t, ok)
assert.Equal(t, inner, val)
_, ok = AnyMap(p, "missing")
assert.False(t, ok)
}
func TestStringAlt(t *testing.T) {
p := map[string]any{"b": "found"}
val, ok := StringAlt(p, "a", "b", "c")
assert.True(t, ok)
assert.Equal(t, "found", val)
_, ok = StringAlt(p, "x", "y")
assert.False(t, ok)
}
+28 -31
View File
@@ -15,50 +15,47 @@ func HandleUninstall(conn net.Conn, req models.Request) {
return return
} }
registry, err := plugins.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
pluginList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err))
return
}
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.Name == name {
plugin = &p
break
}
}
if plugin == nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return
}
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return return
} }
installed, err := manager.IsInstalled(*plugin) // First try to find in registry (by name or ID)
registry, err := plugins.NewRegistry()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return return
} }
if !installed { pluginList, _ := registry.List()
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name)) plugin := plugins.FindByIDOrName(name, pluginList)
// If found in registry, use that
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err))
return
}
if !installed {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name))
return
}
if err := manager.Uninstall(*plugin); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall plugin: %v", err))
return
}
models.Respond(conn, req.ID, SuccessResult{
Success: true,
Message: fmt.Sprintf("plugin uninstalled: %s", plugin.Name),
})
return return
} }
if err := manager.Uninstall(*plugin); err != nil { // Not in registry - try to find and uninstall from installed plugins directly
models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall plugin: %v", err)) if err := manager.UninstallByIDOrName(name); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return return
} }
+26 -31
View File
@@ -15,50 +15,45 @@ func HandleUpdate(conn net.Conn, req models.Request) {
return return
} }
registry, err := plugins.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
pluginList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err))
return
}
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.Name == name {
plugin = &p
break
}
}
if plugin == nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return
}
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return return
} }
installed, err := manager.IsInstalled(*plugin) registry, err := plugins.NewRegistry()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return return
} }
if !installed { pluginList, _ := registry.List()
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name)) plugin := plugins.FindByIDOrName(name, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err))
return
}
if !installed {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name))
return
}
if err := manager.Update(*plugin); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to update plugin: %v", err))
return
}
models.Respond(conn, req.ID, SuccessResult{
Success: true,
Message: fmt.Sprintf("plugin updated: %s", plugin.Name),
})
return return
} }
if err := manager.Update(*plugin); err != nil { // Not in registry - try to update from installed plugins directly
models.RespondError(conn, req.ID, fmt.Sprintf("failed to update plugin: %v", err)) if err := manager.UpdateByIDOrName(name); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return return
} }
+12 -72
View File
@@ -27,12 +27,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "network manager not initialized") models.RespondError(conn, req.ID, "network manager not initialized")
return return
} }
netReq := network.Request{ network.HandleRequest(conn, req, networkManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
network.HandleRequest(conn, netReq, networkManager)
return return
} }
@@ -46,12 +41,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "loginctl manager not initialized") models.RespondError(conn, req.ID, "loginctl manager not initialized")
return return
} }
loginReq := loginctl.Request{ loginctl.HandleRequest(conn, req, loginctlManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
loginctl.HandleRequest(conn, loginReq, loginctlManager)
return return
} }
@@ -60,12 +50,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "freedesktop manager not initialized") models.RespondError(conn, req.ID, "freedesktop manager not initialized")
return return
} }
freedeskReq := freedesktop.Request{ freedesktop.HandleRequest(conn, req, freedesktopManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
freedesktop.HandleRequest(conn, freedeskReq, freedesktopManager)
return return
} }
@@ -74,12 +59,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "wayland manager not initialized") models.RespondError(conn, req.ID, "wayland manager not initialized")
return return
} }
waylandReq := wayland.Request{ wayland.HandleRequest(conn, req, waylandManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
wayland.HandleRequest(conn, waylandReq, waylandManager)
return return
} }
@@ -88,12 +68,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "bluetooth manager not initialized") models.RespondError(conn, req.ID, "bluetooth manager not initialized")
return return
} }
bluezReq := bluez.Request{ bluez.HandleRequest(conn, req, bluezManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
bluez.HandleRequest(conn, bluezReq, bluezManager)
return return
} }
@@ -102,12 +77,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "apppicker manager not initialized") models.RespondError(conn, req.ID, "apppicker manager not initialized")
return return
} }
appPickerReq := apppicker.Request{ apppicker.HandleRequest(conn, req, appPickerManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
apppicker.HandleRequest(conn, appPickerReq, appPickerManager)
return return
} }
@@ -116,12 +86,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "CUPS manager not initialized") models.RespondError(conn, req.ID, "CUPS manager not initialized")
return return
} }
cupsReq := cups.Request{ cups.HandleRequest(conn, req, cupsManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
cups.HandleRequest(conn, cupsReq, cupsManager)
return return
} }
@@ -130,12 +95,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "dwl manager not initialized") models.RespondError(conn, req.ID, "dwl manager not initialized")
return return
} }
dwlReq := dwl.Request{ dwl.HandleRequest(conn, req, dwlManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
dwl.HandleRequest(conn, dwlReq, dwlManager)
return return
} }
@@ -144,12 +104,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "brightness manager not initialized") models.RespondError(conn, req.ID, "brightness manager not initialized")
return return
} }
brightnessReq := brightness.Request{ brightness.HandleRequest(conn, req, brightnessManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
brightness.HandleRequest(conn, brightnessReq, brightnessManager)
return return
} }
@@ -170,12 +125,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
} }
extWorkspaceReq := extworkspace.Request{ extworkspace.HandleRequest(conn, req, extWorkspaceManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
extworkspace.HandleRequest(conn, extWorkspaceReq, extWorkspaceManager)
return return
} }
@@ -184,12 +134,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "wlroutput manager not initialized") models.RespondError(conn, req.ID, "wlroutput manager not initialized")
return return
} }
wlrOutputReq := wlroutput.Request{ wlroutput.HandleRequest(conn, req, wlrOutputManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
wlroutput.HandleRequest(conn, wlrOutputReq, wlrOutputManager)
return return
} }
@@ -198,12 +143,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "evdev manager not initialized") models.RespondError(conn, req.ID, "evdev manager not initialized")
return return
} }
evdevReq := evdev.Request{ evdev.HandleRequest(conn, req, evdevManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
evdev.HandleRequest(conn, evdevReq, evdevManager)
return return
} }
+125 -46
View File
@@ -2,8 +2,6 @@ package wayland
import ( import (
"math" "math"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type GammaRamp struct { type GammaRamp struct {
@@ -12,6 +10,126 @@ type GammaRamp struct {
Blue []uint16 Blue []uint16
} }
type rgb struct {
r, g, b float64
}
type xyz struct {
x, y, z float64
}
func illuminantD(temp int) (float64, float64, bool) {
var x float64
switch {
case temp >= 2500 && temp <= 7000:
t := float64(temp)
x = 0.244063 + 0.09911e3/t + 2.9678e6/(t*t) - 4.6070e9/(t*t*t)
case temp > 7000 && temp <= 25000:
t := float64(temp)
x = 0.237040 + 0.24748e3/t + 1.9018e6/(t*t) - 2.0064e9/(t*t*t)
default:
return 0, 0, false
}
y := -3*(x*x) + 2.870*x - 0.275
return x, y, true
}
func planckianLocus(temp int) (float64, float64, bool) {
var x, y float64
switch {
case temp >= 1667 && temp <= 4000:
t := float64(temp)
x = -0.2661239e9/(t*t*t) - 0.2343589e6/(t*t) + 0.8776956e3/t + 0.179910
if temp <= 2222 {
y = -1.1064814*(x*x*x) - 1.34811020*(x*x) + 2.18555832*x - 0.20219683
} else {
y = -0.9549476*(x*x*x) - 1.37418593*(x*x) + 2.09137015*x - 0.16748867
}
case temp > 4000 && temp < 25000:
t := float64(temp)
x = -3.0258469e9/(t*t*t) + 2.1070379e6/(t*t) + 0.2226347e3/t + 0.240390
y = 3.0817580*(x*x*x) - 5.87338670*(x*x) + 3.75112997*x - 0.37001483
default:
return 0, 0, false
}
return x, y, true
}
func srgbGamma(value, gamma float64) float64 {
if value <= 0.0031308 {
return 12.92 * value
}
return math.Pow(1.055*value, 1.0/gamma) - 0.055
}
func clamp01(v float64) float64 {
switch {
case v > 1.0:
return 1.0
case v < 0.0:
return 0.0
default:
return v
}
}
func xyzToSRGB(c xyz) rgb {
return rgb{
r: srgbGamma(clamp01(3.2404542*c.x-1.5371385*c.y-0.4985314*c.z), 2.2),
g: srgbGamma(clamp01(-0.9692660*c.x+1.8760108*c.y+0.0415560*c.z), 2.2),
b: srgbGamma(clamp01(0.0556434*c.x-0.2040259*c.y+1.0572252*c.z), 2.2),
}
}
func normalizeRGB(c *rgb) {
maxw := math.Max(c.r, math.Max(c.g, c.b))
if maxw > 0 {
c.r /= maxw
c.g /= maxw
c.b /= maxw
}
}
func calcWhitepoint(temp int) rgb {
if temp == 6500 {
return rgb{r: 1.0, g: 1.0, b: 1.0}
}
var wp xyz
switch {
case temp >= 25000:
x, y, _ := illuminantD(25000)
wp.x = x
wp.y = y
case temp >= 4000:
x, y, _ := illuminantD(temp)
wp.x = x
wp.y = y
case temp >= 2500:
x1, y1, _ := illuminantD(temp)
x2, y2, _ := planckianLocus(temp)
factor := float64(4000-temp) / 1500.0
sineFactor := (math.Cos(math.Pi*factor) + 1.0) / 2.0
wp.x = x1*sineFactor + x2*(1.0-sineFactor)
wp.y = y1*sineFactor + y2*(1.0-sineFactor)
default:
t := temp
if t < 1667 {
t = 1667
}
x, y, _ := planckianLocus(t)
wp.x = x
wp.y = y
}
wp.z = 1.0 - wp.x - wp.y
wpRGB := xyzToSRGB(wp)
normalizeRGB(&wpRGB)
return wpRGB
}
func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp { func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp {
ramp := GammaRamp{ ramp := GammaRamp{
Red: make([]uint16, size), Red: make([]uint16, size),
@@ -19,16 +137,13 @@ func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp {
Blue: make([]uint16, size), Blue: make([]uint16, size),
} }
wp := calcWhitepoint(temp)
for i := uint32(0); i < size; i++ { for i := uint32(0); i < size; i++ {
val := float64(i) / float64(size-1) val := float64(i) / float64(size-1)
ramp.Red[i] = uint16(clamp01(math.Pow(val*wp.r, 1.0/gamma)) * 65535.0)
valGamma := math.Pow(val, 1.0/gamma) ramp.Green[i] = uint16(clamp01(math.Pow(val*wp.g, 1.0/gamma)) * 65535.0)
ramp.Blue[i] = uint16(clamp01(math.Pow(val*wp.b, 1.0/gamma)) * 65535.0)
r, g, b := temperatureToRGB(temp)
ramp.Red[i] = uint16(utils.Clamp(valGamma*r*65535.0, 0, 65535))
ramp.Green[i] = uint16(utils.Clamp(valGamma*g*65535.0, 0, 65535))
ramp.Blue[i] = uint16(utils.Clamp(valGamma*b*65535.0, 0, 65535))
} }
return ramp return ramp
@@ -50,39 +165,3 @@ func GenerateIdentityRamp(size uint32) GammaRamp {
return ramp return ramp
} }
func temperatureToRGB(temp int) (float64, float64, float64) {
tempK := float64(temp) / 100.0
var r, g, b float64
if tempK <= 66 {
r = 1.0
} else {
r = tempK - 60
r = 329.698727446 * math.Pow(r, -0.1332047592)
r = utils.Clamp(r, 0, 255) / 255.0
}
if tempK <= 66 {
g = tempK
g = 99.4708025861*math.Log(g) - 161.1195681661
g = utils.Clamp(g, 0, 255) / 255.0
} else {
g = tempK - 60
g = 288.1221695283 * math.Pow(g, -0.0755148492)
g = utils.Clamp(g, 0, 255) / 255.0
}
if tempK >= 66 {
b = 1.0
} else if tempK <= 19 {
b = 0.0
} else {
b = tempK - 10
b = 138.5177312231*math.Log(b) - 305.0447927307
b = utils.Clamp(b, 0, 255) / 255.0
}
return r, g, b
}
+13 -13
View File
@@ -54,7 +54,7 @@ func TestGenerateGammaRamp(t *testing.T) {
} }
} }
func TestTemperatureToRGB(t *testing.T) { func TestCalcWhitepoint(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
temp int temp int
@@ -67,32 +67,32 @@ func TestTemperatureToRGB(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) {
r, g, b := temperatureToRGB(tt.temp) wp := calcWhitepoint(tt.temp)
if r < 0 || r > 1 { if wp.r < 0 || wp.r > 1 {
t.Errorf("red out of range: %f", r) t.Errorf("red out of range: %f", wp.r)
} }
if g < 0 || g > 1 { if wp.g < 0 || wp.g > 1 {
t.Errorf("green out of range: %f", g) t.Errorf("green out of range: %f", wp.g)
} }
if b < 0 || b > 1 { if wp.b < 0 || wp.b > 1 {
t.Errorf("blue out of range: %f", b) t.Errorf("blue out of range: %f", wp.b)
} }
}) })
} }
} }
func TestTemperatureProgression(t *testing.T) { func TestWhitepointProgression(t *testing.T) {
temps := []int{3000, 4000, 5000, 6000, 6500} temps := []int{3000, 4000, 5000, 6000, 6500}
var prevBlue float64 var prevBlue float64
for i, temp := range temps { for i, temp := range temps {
_, _, b := temperatureToRGB(temp) wp := calcWhitepoint(temp)
if i > 0 && b < prevBlue { if i > 0 && wp.b < prevBlue {
t.Errorf("blue should increase with temperature, %d->%d: %f->%f", t.Errorf("blue should increase with temperature, %d->%d: %f->%f",
temps[i-1], temp, prevBlue, b) temps[i-1], temp, prevBlue, wp.b)
} }
prevBlue = b prevBlue = wp.b
} }
} }
+42 -51
View File
@@ -7,20 +7,10 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "wayland manager not initialized") models.RespondError(conn, req.ID, "wayland manager not initialized")
return return
@@ -48,26 +38,27 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleSetTemperature(conn net.Conn, req Request, manager *Manager) { func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
var lowTemp, highTemp int var lowTemp, highTemp int
if temp, ok := req.Params["temp"].(float64); ok { if temp, ok := req.Params["temp"].(float64); ok {
lowTemp = int(temp) lowTemp = int(temp)
highTemp = int(temp) highTemp = int(temp)
} else { } else {
low, okLow := req.Params["low"].(float64) low, err := params.Float(req.Params, "low")
high, okHigh := req.Params["high"].(float64) if err != nil {
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
if !okLow || !okHigh { return
}
high, err := params.Float(req.Params, "high")
if err != nil {
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')") models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
return return
} }
lowTemp = int(low) lowTemp = int(low)
highTemp = int(high) highTemp = int(high)
} }
@@ -77,19 +68,19 @@ func handleSetTemperature(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "temperature set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "temperature set"})
} }
func handleSetLocation(conn net.Conn, req Request, manager *Manager) { func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
lat, ok := req.Params["latitude"].(float64) lat, err := params.Float(req.Params, "latitude")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'latitude' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
lon, ok := req.Params["longitude"].(float64) lon, err := params.Float(req.Params, "longitude")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'longitude' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -98,30 +89,30 @@ func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
} }
func handleSetManualTimes(conn net.Conn, req Request, manager *Manager) { func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
sunriseParam := req.Params["sunrise"] sunriseParam := req.Params["sunrise"]
sunsetParam := req.Params["sunset"] sunsetParam := req.Params["sunset"]
if sunriseParam == nil || sunsetParam == nil { if sunriseParam == nil || sunsetParam == nil {
manager.ClearManualTimes() manager.ClearManualTimes()
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return return
} }
sunriseStr, ok := sunriseParam.(string) sunriseStr, ok := sunriseParam.(string)
if !ok || sunriseStr == "" { if !ok || sunriseStr == "" {
manager.ClearManualTimes() manager.ClearManualTimes()
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return return
} }
sunsetStr, ok := sunsetParam.(string) sunsetStr, ok := sunsetParam.(string)
if !ok || sunsetStr == "" { if !ok || sunsetStr == "" {
manager.ClearManualTimes() manager.ClearManualTimes()
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return return
} }
@@ -142,24 +133,24 @@ func handleSetManualTimes(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times set"})
} }
func handleSetUseIPLocation(conn net.Conn, req Request, manager *Manager) { func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
use, ok := req.Params["use"].(bool) use, err := params.Bool(req.Params, "use")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'use' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetUseIPLocation(use) manager.SetUseIPLocation(use)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "IP location preference set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "IP location preference set"})
} }
func handleSetGamma(conn net.Conn, req Request, manager *Manager) { func handleSetGamma(conn net.Conn, req models.Request, manager *Manager) {
gamma, ok := req.Params["gamma"].(float64) gamma, err := params.Float(req.Params, "gamma")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'gamma' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -168,21 +159,21 @@ func handleSetGamma(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "gamma set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "gamma set"})
} }
func handleSetEnabled(conn net.Conn, req Request, manager *Manager) { func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool) enabled, err := params.Bool(req.Params, "enabled")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetEnabled(enabled) manager.SetEnabled(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "enabled state set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "enabled state set"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
File diff suppressed because it is too large Load Diff
+95 -59
View File
@@ -6,81 +6,117 @@ import (
) )
const ( const (
degToRad = math.Pi / 180.0 degToRad = math.Pi / 180.0
radToDeg = 180.0 / math.Pi radToDeg = 180.0 / math.Pi
solarNoon = 12.0
sunriseAngle = -0.833
) )
func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes { type SunCondition int
utcDate := date.UTC()
year, month, day := utcDate.Date()
loc := date.Location()
dayOfYear := utcDate.YearDay() const (
SunNormal SunCondition = iota
SunMidnightSun
SunPolarNight
)
gamma := 2 * math.Pi / 365 * float64(dayOfYear-1) type SunTimes struct {
Dawn time.Time
Sunrise time.Time
Sunset time.Time
Night time.Time
}
eqTime := 229.18 * (0.000075 + func daysInYear(year int) int {
0.001868*math.Cos(gamma) - if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
0.032077*math.Sin(gamma) - return 366
0.014615*math.Cos(2*gamma) - }
0.040849*math.Sin(2*gamma)) return 365
}
decl := 0.006918 - func dateOrbitAngle(t time.Time) float64 {
0.399912*math.Cos(gamma) + return 2 * math.Pi / float64(daysInYear(t.Year())) * float64(t.YearDay()-1)
0.070257*math.Sin(gamma) - }
0.006758*math.Cos(2*gamma) +
0.000907*math.Sin(2*gamma) -
0.002697*math.Cos(3*gamma) +
0.00148*math.Sin(3*gamma)
func equationOfTime(orbitAngle float64) float64 {
return 4 * (0.000075 +
0.001868*math.Cos(orbitAngle) -
0.032077*math.Sin(orbitAngle) -
0.014615*math.Cos(2*orbitAngle) -
0.040849*math.Sin(2*orbitAngle))
}
func sunDeclination(orbitAngle float64) float64 {
return 0.006918 -
0.399912*math.Cos(orbitAngle) +
0.070257*math.Sin(orbitAngle) -
0.006758*math.Cos(2*orbitAngle) +
0.000907*math.Sin(2*orbitAngle) -
0.002697*math.Cos(3*orbitAngle) +
0.00148*math.Sin(3*orbitAngle)
}
func sunHourAngle(latRad, declination, targetSunRad float64) float64 {
return math.Acos(math.Cos(targetSunRad)/
math.Cos(latRad)*math.Cos(declination) -
math.Tan(latRad)*math.Tan(declination))
}
func hourAngleToSeconds(hourAngle, eqtime float64) float64 {
return radToDeg * (4.0*math.Pi - 4*hourAngle - eqtime) * 60
}
func sunCondition(latRad, declination float64) SunCondition {
signLat := latRad >= 0
signDecl := declination >= 0
if signLat == signDecl {
return SunMidnightSun
}
return SunPolarNight
}
func CalculateSunTimesWithTwilight(lat, lon float64, date time.Time, elevTwilight, elevDaylight float64) (SunTimes, SunCondition) {
latRad := lat * degToRad latRad := lat * degToRad
elevTwilightRad := (90.833 - elevTwilight) * degToRad
elevDaylightRad := (90.833 - elevDaylight) * degToRad
cosHourAngle := (math.Sin(sunriseAngle*degToRad) - utc := date.UTC()
math.Sin(latRad)*math.Sin(decl)) / orbitAngle := dateOrbitAngle(utc)
(math.Cos(latRad) * math.Cos(decl)) decl := sunDeclination(orbitAngle)
eqtime := equationOfTime(orbitAngle)
if cosHourAngle > 1 { haTwilight := sunHourAngle(latRad, decl, elevTwilightRad)
return SunTimes{ haDaylight := sunHourAngle(latRad, decl, elevDaylightRad)
Sunrise: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
Sunset: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc), if math.IsNaN(haTwilight) || math.IsNaN(haDaylight) {
} cond := sunCondition(latRad, decl)
} return SunTimes{}, cond
if cosHourAngle < -1 {
return SunTimes{
Sunrise: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
Sunset: time.Date(year, month, day, 23, 59, 59, 0, time.UTC).In(loc),
}
} }
hourAngle := math.Acos(cosHourAngle) * radToDeg dayStart := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC)
lonOffset := time.Duration(-lon*4) * time.Minute
sunriseTime := solarNoon - hourAngle/15.0 - lon/15.0 - eqTime/60.0 dawnSecs := hourAngleToSeconds(math.Abs(haTwilight), eqtime)
sunsetTime := solarNoon + hourAngle/15.0 - lon/15.0 - eqTime/60.0 sunriseSecs := hourAngleToSeconds(math.Abs(haDaylight), eqtime)
sunsetSecs := hourAngleToSeconds(-math.Abs(haDaylight), eqtime)
sunrise := timeOfDayToTime(sunriseTime, year, month, day, time.UTC).In(loc) nightSecs := hourAngleToSeconds(-math.Abs(haTwilight), eqtime)
sunset := timeOfDayToTime(sunsetTime, year, month, day, time.UTC).In(loc)
return SunTimes{ return SunTimes{
Sunrise: sunrise, Dawn: dayStart.Add(time.Duration(dawnSecs)*time.Second + lonOffset).In(date.Location()),
Sunset: sunset, Sunrise: dayStart.Add(time.Duration(sunriseSecs)*time.Second + lonOffset).In(date.Location()),
} Sunset: dayStart.Add(time.Duration(sunsetSecs)*time.Second + lonOffset).In(date.Location()),
Night: dayStart.Add(time.Duration(nightSecs)*time.Second + lonOffset).In(date.Location()),
}, SunNormal
} }
func timeOfDayToTime(hours float64, year int, month time.Month, day int, loc *time.Location) time.Time { func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes {
h := int(hours) times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
m := int((hours - float64(h)) * 60) switch cond {
s := int(((hours-float64(h))*60 - float64(m)) * 60) case SunMidnightSun:
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
if h < 0 { dayEnd := dayStart.Add(24*time.Hour - time.Second)
h += 24 return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayEnd, Night: dayEnd}
day-- case SunPolarNight:
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayStart, Night: dayStart}
} }
if h >= 24 { return times
h -= 24
day++
}
return time.Date(year, month, day, h, m, s, 0, loc)
} }
+32 -23
View File
@@ -340,38 +340,47 @@ func TestCalculateNextTransition(t *testing.T) {
} }
} }
func TestTimeOfDayToTime(t *testing.T) { func TestSunTimesWithTwilight(t *testing.T) {
lat := 40.7128
lon := -74.0060
date := time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local)
times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
if cond != SunNormal {
t.Errorf("expected SunNormal, got %v", cond)
}
if !times.Dawn.Before(times.Sunrise) {
t.Error("dawn should be before sunrise")
}
if !times.Sunrise.Before(times.Sunset) {
t.Error("sunrise should be before sunset")
}
if !times.Sunset.Before(times.Night) {
t.Error("sunset should be before night")
}
}
func TestSunConditions(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
hours float64 lat float64
expected time.Time date time.Time
expected SunCondition
}{ }{
{ {
name: "noon", name: "normal_conditions",
hours: 12.0, lat: 40.0,
expected: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local), date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.UTC),
}, expected: SunNormal,
{
name: "half_past",
hours: 12.5,
expected: time.Date(2024, 6, 21, 12, 30, 0, 0, time.Local),
},
{
name: "early_morning",
hours: 6.25,
expected: time.Date(2024, 6, 21, 6, 15, 0, 0, time.Local),
}, },
} }
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 := timeOfDayToTime(tt.hours, 2024, 6, 21, time.Local) _, cond := CalculateSunTimesWithTwilight(tt.lat, 0, tt.date, -6.0, 3.0)
if cond != tt.expected {
if result.Hour() != tt.expected.Hour() { t.Errorf("expected condition %v, got %v", tt.expected, cond)
t.Errorf("hour = %d, want %d", result.Hour(), tt.expected.Hour())
}
if result.Minute() != tt.expected.Minute() {
t.Errorf("minute = %d, want %d", result.Minute(), tt.expected.Minute())
} }
}) })
} }
+46 -28
View File
@@ -11,18 +11,28 @@ import (
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
type GammaState int
const (
StateNormal GammaState = iota
StateTransition
StateStatic
)
type Config struct { type Config struct {
Outputs []string Outputs []string
LowTemp int LowTemp int
HighTemp int HighTemp int
Latitude *float64 Latitude *float64
Longitude *float64 Longitude *float64
UseIPLocation bool UseIPLocation bool
ManualSunrise *time.Time ManualSunrise *time.Time
ManualSunset *time.Time ManualSunset *time.Time
ManualDuration *time.Duration ManualDuration *time.Duration
Gamma float64 Gamma float64
Enabled bool Enabled bool
ElevationTwilight float64
ElevationDaylight float64
} }
type State struct { type State struct {
@@ -31,13 +41,24 @@ type State struct {
NextTransition time.Time `json:"nextTransition"` NextTransition time.Time `json:"nextTransition"`
SunriseTime time.Time `json:"sunriseTime"` SunriseTime time.Time `json:"sunriseTime"`
SunsetTime time.Time `json:"sunsetTime"` SunsetTime time.Time `json:"sunsetTime"`
DawnTime time.Time `json:"dawnTime"`
NightTime time.Time `json:"nightTime"`
IsDay bool `json:"isDay"` IsDay bool `json:"isDay"`
SunPosition float64 `json:"sunPosition"`
} }
type cmd struct { type cmd struct {
fn func() fn func()
} }
type sunSchedule struct {
times SunTimes
condition SunCondition
dawnStepTime time.Duration
nightStepTime time.Duration
calcDay time.Time
}
type Manager struct { type Manager struct {
config Config config Config
configMutex sync.RWMutex configMutex sync.RWMutex
@@ -60,10 +81,9 @@ type Manager struct {
updateTrigger chan struct{} updateTrigger chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
currentTemp int schedule sunSchedule
targetTemp int scheduleMutex sync.RWMutex
transitionMutex sync.RWMutex gammaState GammaState
transitionChan chan int
cachedIPLat *float64 cachedIPLat *float64
cachedIPLon *float64 cachedIPLon *float64
@@ -80,7 +100,6 @@ type Manager struct {
type outputState struct { type outputState struct {
id uint32 id uint32
name string
registryName uint32 registryName uint32
output *wlclient.Output output *wlclient.Output
gammaControl any gammaControl any
@@ -91,18 +110,15 @@ type outputState struct {
lastFailTime time.Time lastFailTime time.Time
} }
type SunTimes struct {
Sunrise time.Time
Sunset time.Time
}
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
Outputs: []string{}, Outputs: []string{},
LowTemp: 4000, LowTemp: 4000,
HighTemp: 6500, HighTemp: 6500,
Gamma: 1.0, Gamma: 1.0,
Enabled: false, Enabled: false,
ElevationTwilight: -6.0,
ElevationDaylight: 3.0,
} }
} }
@@ -140,8 +156,7 @@ func (m *Manager) GetState() State {
if m.state == nil { if m.state == nil {
return State{} return State{}
} }
stateCopy := *m.state return *m.state
return stateCopy
} }
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
@@ -185,5 +200,8 @@ func stateChanged(old, new *State) bool {
if old.Config.Enabled != new.Config.Enabled { if old.Config.Enabled != new.Config.Enabled {
return true return true
} }
if old.SunPosition != new.SunPosition {
return true
}
return false return false
} }
+6 -18
View File
@@ -11,17 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type HeadConfig struct { type HeadConfig struct {
Name string `json:"name"` Name string `json:"name"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@@ -42,7 +31,7 @@ type ConfigurationRequest struct {
Test bool `json:"test"` Test bool `json:"test"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "wlroutput manager not initialized") models.RespondError(conn, req.ID, "wlroutput manager not initialized")
return return
@@ -62,12 +51,11 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleApplyConfiguration(conn net.Conn, req Request, manager *Manager, test bool) { func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
headsParam, ok := req.Params["heads"] headsParam, ok := req.Params["heads"]
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'heads' parameter") models.RespondError(conn, req.ID, "missing 'heads' parameter")
@@ -95,10 +83,10 @@ func handleApplyConfiguration(conn net.Conn, req Request, manager *Manager, test
if test { if test {
msg = "configuration test succeeded" msg = "configuration test succeeded"
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: msg}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: msg})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+74 -2
View File
@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@@ -118,6 +120,59 @@ func (m Model) viewInstallingPackages() string {
return b.String() return b.String()
} }
func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
config, ok := distros.Registry[distroID]
if !ok {
return "dms"
}
var isGit bool
for _, dep := range dependencies {
if dep.Name == "dms (DankMaterialShell)" {
isGit = dep.Variant == deps.VariantGit
break
}
}
switch config.Family {
case distros.FamilyArch:
if isGit {
return "dms-shell-git"
}
return "dms-shell-bin"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit {
return "dms-git"
}
return "dms"
default:
return "dms"
}
}
func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
config, ok := distros.Registry[distroID]
if !ok {
return ""
}
if config.Family == distros.FamilyGentoo {
return "rm -rf ~/.config/quickshell/dms && sudo rm /usr/local/bin/dms"
}
pkg := dmsPackageName(distroID, dependencies)
switch config.Family {
case distros.FamilyArch:
return "sudo pacman -Rs " + pkg
case distros.FamilyFedora:
return "sudo dnf remove " + pkg
case distros.FamilyUbuntu, distros.FamilyDebian:
return "sudo apt remove " + pkg
case distros.FamilySUSE:
return "sudo zypper remove " + pkg
default:
return ""
}
}
func (m Model) viewInstallComplete() string { func (m Model) viewInstallComplete() string {
var b strings.Builder var b strings.Builder
@@ -132,7 +187,6 @@ func (m Model) viewInstallComplete() string {
b.WriteString(success) b.WriteString(success)
b.WriteString("\n\n") b.WriteString("\n\n")
// Show what was accomplished
accomplishments := []string{ accomplishments := []string{
"• Window manager and dependencies installed", "• Window manager and dependencies installed",
"• Terminal and development tools configured", "• Terminal and development tools configured",
@@ -146,8 +200,26 @@ func (m Model) viewInstallComplete() string {
} }
b.WriteString("\n") b.WriteString("\n")
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\" \n\nPress Enter to exit.") info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\"")
b.WriteString(info) b.WriteString(info)
b.WriteString("\n\n")
theme := TerminalTheme()
cmdStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Accent))
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("journalctl --user -u dms") + "\n")
if m.osInfo != nil {
if cmd := uninstallCommand(m.osInfo.Distribution.ID, m.dependencies); cmd != "" {
b.WriteString(labelStyle.Render(" Uninstall: ") + cmdStyle.Render(cmd) + "\n")
}
}
b.WriteString("\n")
b.WriteString(m.styles.Normal.Render("Press Enter to exit."))
if m.logFilePath != "" { if m.logFilePath != "" {
b.WriteString("\n\n") b.WriteString("\n\n")
+1 -1
View File
@@ -40,7 +40,7 @@ func (m Model) viewWelcome() string {
subtitle := lipgloss.NewStyle(). subtitle := lipgloss.NewStyle().
Foreground(lipgloss.Color(theme.Subtle)). Foreground(lipgloss.Color(theme.Subtle)).
Italic(true). Italic(true).
Render("Quickstart for a Dank Desktop") Render("Quickstart for a Dank Desktop")
b.WriteString(decorator) b.WriteString(decorator)
b.WriteString("\n") b.WriteString("\n")
+8
View File
@@ -0,0 +1,8 @@
package utils
import "os/exec"
func CommandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
+52
View File
@@ -0,0 +1,52 @@
package utils
import (
"os"
"path/filepath"
"strings"
)
func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded)
if strings.HasPrefix(expanded, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expanded = filepath.Join(home, expanded[1:])
}
return expanded, nil
}
func XDGConfigHome() string {
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
return configHome
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".config")
}
return filepath.Join(os.TempDir(), ".config")
}
func XDGCacheHome() string {
if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" {
return cacheHome
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".cache")
}
return filepath.Join(os.TempDir(), ".cache")
}
func XDGDataHome() string {
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
return dataHome
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".local", "share")
}
return filepath.Join(os.TempDir(), ".local", "share")
}
+106
View File
@@ -0,0 +1,106 @@
package utils
import (
"os"
"path/filepath"
"testing"
)
func TestExpandPathTilde(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result, err := ExpandPath("~/test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := filepath.Join(home, "test")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestExpandPathEnvVar(t *testing.T) {
t.Setenv("TEST_PATH_VAR", "/custom/path")
result, err := ExpandPath("$TEST_PATH_VAR/subdir")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != "/custom/path/subdir" {
t.Errorf("expected /custom/path/subdir, got %s", result)
}
}
func TestExpandPathAbsolute(t *testing.T) {
result, err := ExpandPath("/absolute/path")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != "/absolute/path" {
t.Errorf("expected /absolute/path, got %s", result)
}
}
func TestXDGConfigHomeDefault(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result := XDGConfigHome()
expected := filepath.Join(home, ".config")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestXDGConfigHomeCustom(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
result := XDGConfigHome()
if result != "/custom/config" {
t.Errorf("expected /custom/config, got %s", result)
}
}
func TestXDGCacheHomeDefault(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result := XDGCacheHome()
expected := filepath.Join(home, ".cache")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestXDGCacheHomeCustom(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "/custom/cache")
result := XDGCacheHome()
if result != "/custom/cache" {
t.Errorf("expected /custom/cache, got %s", result)
}
}
func TestXDGDataHomeDefault(t *testing.T) {
t.Setenv("XDG_DATA_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result := XDGDataHome()
expected := filepath.Join(home, ".local", "share")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestXDGDataHomeCustom(t *testing.T) {
t.Setenv("XDG_DATA_HOME", "/custom/data")
result := XDGDataHome()
if result != "/custom/data" {
t.Errorf("expected /custom/data, got %s", result)
}
}
+56
View File
@@ -0,0 +1,56 @@
package utils
func Filter[T any](items []T, predicate func(T) bool) []T {
var result []T
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}
func Find[T any](items []T, predicate func(T) bool) (T, bool) {
for _, item := range items {
if predicate(item) {
return item, true
}
}
var zero T
return zero, false
}
func Map[T, U any](items []T, transform func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = transform(item)
}
return result
}
func Contains[T comparable](items []T, target T) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func Any[T any](items []T, predicate func(T) bool) bool {
for _, item := range items {
if predicate(item) {
return true
}
}
return false
}
func All[T any](items []T, predicate func(T) bool) bool {
for _, item := range items {
if !predicate(item) {
return false
}
}
return true
}
+72
View File
@@ -0,0 +1,72 @@
package utils
import (
"testing"
)
func TestFilter(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
if len(evens) != 2 || evens[0] != 2 || evens[1] != 4 {
t.Errorf("expected [2, 4], got %v", evens)
}
}
func TestFilterEmpty(t *testing.T) {
result := Filter([]int{1, 2, 3}, func(n int) bool { return n > 10 })
if len(result) != 0 {
t.Errorf("expected empty slice, got %v", result)
}
}
func TestFind(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
val, found := Find(nums, func(n int) bool { return n == 3 })
if !found || val != 3 {
t.Errorf("expected 3, got %v (found=%v)", val, found)
}
}
func TestFindNotFound(t *testing.T) {
nums := []int{1, 2, 3}
val, found := Find(nums, func(n int) bool { return n == 99 })
if found || val != 0 {
t.Errorf("expected zero value not found, got %v (found=%v)", val, found)
}
}
func TestMap(t *testing.T) {
nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
if len(doubled) != 3 || doubled[0] != 2 || doubled[1] != 4 || doubled[2] != 6 {
t.Errorf("expected [2, 4, 6], got %v", doubled)
}
}
func TestMapTypeConversion(t *testing.T) {
nums := []int{1, 2, 3}
strs := Map(nums, func(n int) string { return string(rune('a' + n - 1)) })
if strs[0] != "a" || strs[1] != "b" || strs[2] != "c" {
t.Errorf("expected [a, b, c], got %v", strs)
}
}
func TestContains(t *testing.T) {
nums := []int{1, 2, 3}
if !Contains(nums, 2) {
t.Error("expected to contain 2")
}
if Contains(nums, 99) {
t.Error("expected not to contain 99")
}
}
func TestAny(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
if !Any(nums, func(n int) bool { return n > 4 }) {
t.Error("expected any > 4")
}
if Any(nums, func(n int) bool { return n > 10 }) {
t.Error("expected none > 10")
}
}

Some files were not shown because too many files have changed in this diff Show More