1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-28 07:22:50 -05:00

Compare commits

..

68 Commits

Author SHA1 Message Date
bbedward
4a68ce35a3 hyprland: remove focus grab from dankpopout 2025-11-16 11:44:59 -05:00
bbedward
eb4655fcbc rebase woes 2025-11-16 11:37:19 -05:00
bbedward
6eb349c9d4 barmask: fix multi-screen handling 2025-11-16 11:33:47 -05:00
bbedward
0a8a7895b3 hyprland: use FocusGrab for ondemand windows 2025-11-16 11:33:47 -05:00
bbedward
73c82a4dd9 dankbar/mask: extra polish for Hyprland maybe 2025-11-16 11:33:47 -05:00
bbedward
ccf28fc4e7 dankbar: add a mask while popouts are open
- Retains ability to click items on the bar, while another is open
2025-11-16 11:32:47 -05:00
bbedward
64ec5be919 wallpaper: empty input region 2025-11-15 23:41:24 -05:00
bbedward
3916512d66 systemtray: fix erroneous undefined condition 2025-11-15 21:46:34 -05:00
bbedward
e2f426a1bd Revert "systemtray: fix UI thread freeze when opening menu on Hyprland"
This reverts commit 4cb652abd9.
2025-11-15 21:42:50 -05:00
bbedward
aa1df8dfcf core: more syncmap conversions 2025-11-15 20:00:47 -05:00
bbedward
67557555f2 core: refactor to use a generic-compatible syncmap 2025-11-15 19:45:19 -05:00
bbedward
4cb652abd9 systemtray: fix UI thread freeze when opening menu on Hyprland
- Similar pattern as fix from Noctalia
2025-11-15 17:57:23 -05:00
bbedward
d11868b99f systray: don't try to force focus of menus 2025-11-15 14:57:47 -05:00
bbedward
1798417e6a systemtray: don't take keyboard focus
- bricks hyprland
2025-11-15 14:48:13 -05:00
github-actions[bot]
43dc3e5bb1 nix: update vendorHash for go.mod changes 2025-11-15 19:43:35 +00:00
bbedward
91891a14ed core/wayland: thread-safety meta fixes + cleanups + hypr workaround
- fork go-wayland/client and modify to make it thread-safe internally
- use sync.Map and atomic values in many places to cut down on mutex
  boilerplate
- do not create extworkspace client unless explicitly requested
2025-11-15 14:41:00 -05:00
bbedward
20f7d60147 settings: various consistency issues fixed
part of #725
2025-11-15 12:05:44 -05:00
bbedward
7e17e7d37a osd: fix opacity
part of #725
2025-11-15 11:43:05 -05:00
bbedward
cbb244f785 osd: add option to disable each OSD 2025-11-15 11:36:33 -05:00
Sunner
1c264d858b Follow symlinks when searching for sessions (#728) 2025-11-15 10:29:34 -05:00
bbedward
217037c2ae evdev: fix test 2025-11-14 23:26:14 -05:00
bbedward
b4dbd0b69c evdev: enhance keyboard detection for capslock 2025-11-14 23:22:06 -05:00
github-actions[bot]
89a2b5c00b chore: bump version to v0.5.2 2025-11-15 00:31:06 +00:00
bbedward
929b6dae1a widgets: fix some 0-width issues 2025-11-14 19:26:51 -05:00
Pi Home Server
52fe493da9 Feature/privacy widget - Settings to force icons on (#715)
* Update

* Update

* Update

* Update

* Update

* Set default to false

* Update SettingsData.qml

Set default visibility to false

* privacy widget: fix truncated settings menu

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-11-14 19:16:17 -05:00
purian23
3e6be3e762 Greet path updates 2025-11-14 17:54:35 -05:00
purian23
7a8cc449b9 Add local ACL greeter permissions to dms core installer 2025-11-14 16:32:06 -05:00
purian23
8f5a9d6e9f Update dms greeter to scan system & local directories 2025-11-14 15:36:14 -05:00
bbedward
1c5e31fea9 greeter: allow mangowc as compositor 2025-11-14 14:51:28 -05:00
claymorwan
fd08ae18ab feat: plugin layer namespace (#717) 2025-11-14 14:50:29 -05:00
bbedward
a7eb3de06e dankbar: configurable auto-hide delay 2025-11-14 14:00:37 -05:00
bbedward
8902dd7c44 launcher: grid re-style and customizable column counts 2025-11-14 13:54:44 -05:00
bbedward
6387d8400c osd: account for bar position when on bottom 2025-11-14 13:47:26 -05:00
bbedward
597cacb9cc matugen: update gtk4/gtk3-dark colors
- also some change to dankinstall to use niri/xwls from system repos,
  too lazy to split the commits
2025-11-14 13:20:59 -05:00
bbedward
3e285ad9ff dankdash: remove useless tint rectangle
part of #716
2025-11-14 13:09:46 -05:00
bbedward
cc1fa89790 clock: use precision minutes instead of seconds, unless needed
part of #716
2025-11-14 12:42:23 -05:00
bbedward
b0ed007751 core/dankinstall: more deb fixes 2025-11-14 12:22:13 -05:00
bbedward
e1e2650d2b core/dankinstall: fix hyprland util manual compile on debian 2025-11-14 12:13:49 -05:00
bbedward
b23f17b633 core/dankinstall: fix hyprpicker build 2025-11-14 12:07:03 -05:00
github-actions[bot]
818e40b2df nix: update vendorHash for go.mod changes 2025-11-14 17:06:06 +00:00
bbedward
5685e39631 core: improve evdev capslock detection, wayland context fixes 2025-11-14 12:04:47 -05:00
kritag
72534b7674 adding tokyonight, everforest, nord and rose-pine themes (#714)
Co-authored-by: Kristian Tagesen <kristian.tagesen@tietoevry.com>
2025-11-14 11:40:26 -05:00
bbedward
328490d23d powermenu: smarter positioning in control center 2025-11-14 10:45:16 -05:00
bbedward
97a0696930 clock: fix overview clock when seconds is on 2025-11-14 10:29:41 -05:00
bbedward
cb4e0660e0 dock: add reveal IPCs 2025-11-14 10:08:16 -05:00
bbedward
67c642de4c keybinds: add toggleWithPath 2025-11-14 09:03:27 -05:00
bbedward
0d7c2e1024 core/cli: fix keybind provider path override 2025-11-14 08:56:16 -05:00
bbedward
16a779a41b powermenu: restore grid as an option
fixes #712
2025-11-14 08:51:15 -05:00
purian23
c4ca3c8644 Add root dms-cli build script 2025-11-14 00:22:49 -05:00
bbedward
aabcbe34f3 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-14 00:06:50 -05:00
bbedward
f06626e441 dock: use modded app IDs for grouping logic
fixes #710
2025-11-14 00:06:27 -05:00
purian23
c4e1a71776 Relocate notification tests to scripts dir 2025-11-13 23:53:18 -05:00
bbedward
77e6c16bd2 core/extworkspace: fix some thread-safety issues 2025-11-13 23:52:32 -05:00
purian23
9d1fac3570 Relocate Nix dir under distro/nix 2025-11-13 23:47:00 -05:00
bbedward
b7aeaa7fc5 systemtray: better hide/unhide behavioro 2025-11-13 22:49:30 -05:00
bbedward
f6d8c9ff61 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 22:41:47 -05:00
bbedward
0490794d6c dankbar: add caps lock indicator widget 2025-11-13 22:41:33 -05:00
github-actions[bot]
335c83dd3c nix: update vendorHash for go.mod changes 2025-11-14 03:26:50 +00:00
bbedward
91da720c26 i18n:update translations 2025-11-13 22:25:22 -05:00
bbedward
b6ac744a68 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 22:24:51 -05:00
bbedward
526c4092fd evdev: add evdev monitor for caps lock state 2025-11-13 22:24:27 -05:00
github-actions[bot]
ed06dda384 nix: update vendorHash for go.mod changes 2025-11-14 02:54:15 +00:00
bbedward
6465b11e9b core: ensure all NM tests use mock backend + re-orgs + dep updates 2025-11-13 21:44:03 -05:00
purian23
b2879878a1 feat: Priority pinned items in Control Center 2025-11-13 21:23:54 -05:00
bbedward
3e17b086fb ci: add docs to release archive 2025-11-13 20:19:54 -05:00
purian23
0545e6bcda Remove release tags 2025-11-13 20:01:38 -05:00
purian23
27a907433f Test Copr workflow update 2025-11-13 19:40:16 -05:00
purian23
69616800e3 Release update 2025-11-13 18:54:01 -05:00
197 changed files with 14900 additions and 2321 deletions

View File

@@ -7,6 +7,10 @@ on:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)' description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false required: false
default: '' default: ''
release:
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
required: false
default: '1'
jobs: jobs:
build-and-upload: build-and-upload:
@@ -19,6 +23,7 @@ jobs:
- name: Determine version - name: Determine version
id: version id: version
run: | run: |
# Get version from manual input or latest release
if [ -n "${{ github.event.inputs.version }}" ]; then if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}" VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION" echo "Using manual version: $VERSION"
@@ -27,8 +32,14 @@ jobs:
echo "Using latest release version: $VERSION" echo "Using latest release version: $VERSION"
fi fi
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$RELEASE" ]; then
RELEASE="1"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "✅ Building DMS stable version: $VERSION" echo "release=$RELEASE" >> $GITHUB_OUTPUT
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
- name: Setup build environment - name: Setup build environment
run: | run: |
@@ -57,6 +68,7 @@ jobs:
- name: Generate stable spec file - name: Generate stable spec file
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')" CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF' cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
@@ -68,7 +80,7 @@ jobs:
Name: dms Name: dms
Version: %{version} Version: %{version}
Release: 1%{?dist} Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary} Summary: %{pkg_summary}
License: MIT License: MIT
@@ -212,16 +224,17 @@ jobs:
%{_bindir}/dgop %{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1 * 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 - 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
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}" echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
echo "" echo ""
echo "=== Spec file preview ===" echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec head -40 ~/rpmbuild/SPECS/dms.spec
@@ -295,7 +308,7 @@ jobs:
run: | run: |
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY

View File

@@ -35,6 +35,14 @@ jobs:
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
- name: Format check
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
- name: Run tests - name: Run tests
run: go test -v ./... run: go test -v ./...
@@ -168,6 +176,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${{ github.ref_name }}
git checkout ${{ github.ref_name }}
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -255,6 +268,9 @@ jobs:
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Create QML source package (exclude build artifacts and git files) # Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
# Tar the CONTENTS of quickshell/, not the directory itself # Tar the CONTENTS of quickshell/, not the directory itself
(cd quickshell && tar --exclude='.git' \ (cd quickshell && tar --exclude='.git' \
--exclude='.github' \ --exclude='.github' \
@@ -291,6 +307,11 @@ jobs:
fi fi
done done
# Copy docs directory
if [ -d "docs" ]; then
cp -r docs _temp_full/
fi
# Create installation guide # Create installation guide
cat > _temp_full/INSTALL.md << 'EOFINSTALL' cat > _temp_full/INSTALL.md << 'EOFINSTALL'
# DankMaterialShell Installation # DankMaterialShell Installation

View File

@@ -36,8 +36,10 @@ DankMaterialShell/
│ ├── cmd/ # dms CLI and dankinstall binaries │ ├── cmd/ # dms CLI and dankinstall binaries
│ ├── internal/ # System integration, IPC, distro support │ ├── internal/ # System integration, IPC, distro support
│ └── pkg/ # Shared packages │ └── pkg/ # Shared packages
├── distro/ # Distribution packaging (Fedora RPM specs) ├── distro/ # Distribution packaging
├── nix/ # NixOS/home-manager modules │ ├── fedora/ # Fedora RPM specs
│ ├── debian/ # Debian packaging
│ └── nix/ # NixOS/home-manager modules
└── flake.nix # Nix flake for declarative installation └── flake.nix # Nix flake for declarative installation
``` ```
@@ -136,8 +138,7 @@ See component-specific documentation:
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules - **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
- **[core/](core/)** - Go backend, CLI tools, and system integration - **[core/](core/)** - Go backend, CLI tools, and system integration
- **[distro/](distro/)** - Distribution packaging - **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
- **[nix/](nix/)** - NixOS and home-manager modules
### Building from Source ### Building from Source

48
core/.mockery.yml Normal file
View File

@@ -0,0 +1,48 @@
with-expecter: true
dir: "internal/mocks/{{.InterfaceDirRelative}}"
mockname: "Mock{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
packages:
github.com/Wifx/gonetworkmanager/v2:
interfaces:
NetworkManager:
Device:
DeviceWireless:
AccessPoint:
Connection:
Settings:
ActiveConnection:
IP4Config:
net:
interfaces:
Conn:
github.com/AvengeMedia/danklinux/internal/plugins:
interfaces:
GitClient:
github.com/godbus/dbus/v5:
interfaces:
BusObject:
github.com/AvengeMedia/danklinux/internal/server/brightness:
config:
dir: "internal/mocks/brightness"
outpkg: mocks_brightness
interfaces:
DBusConn:
github.com/AvengeMedia/danklinux/internal/server/network:
config:
dir: "internal/mocks/network"
outpkg: mocks_network
interfaces:
Backend:
github.com/AvengeMedia/danklinux/internal/server/cups:
config:
dir: "internal/mocks/cups"
outpkg: mocks_cups
interfaces:
CUPSClientInterface:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
config:
dir: "internal/mocks/evdev"
outpkg: mocks_evdev
interfaces:
EvdevDevice:

View File

@@ -31,6 +31,7 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
- DDC/CI protocol - External monitor brightness control (like `ddcutil`) - DDC/CI protocol - External monitor brightness control (like `ddcutil`)
- Backlight control - Internal display brightness via `login1` or sysfs - Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management - LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
**Plugin System** **Plugin System**
- Plugin registry integration - Plugin registry integration

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/logger" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui" "github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -12,7 +12,7 @@ import (
var Version = "dev" var Version = "dev"
func main() { func main() {
fileLogger, err := logger.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)
fmt.Println("Continuing without file logging...") fmt.Println("Continuing without file logging...")

View File

@@ -34,9 +34,7 @@ var keybindsShowCmd = &cobra.Command{
} }
func init() { func init() {
keybindsShowCmd.Flags().String("hyprland-path", "$HOME/.config/hypr", "Path to Hyprland config directory") keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsShowCmd.Flags().String("mangowc-path", "$HOME/.config/mango", "Path to MangoWC config directory")
keybindsShowCmd.Flags().String("sway-path", "$HOME/.config/sway", "Path to Sway config directory")
keybindsCmd.AddCommand(keybindsListCmd) keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd) keybindsCmd.AddCommand(keybindsShowCmd)
@@ -89,25 +87,34 @@ func runKeybindsList(cmd *cobra.Command, args []string) {
func runKeybindsShow(cmd *cobra.Command, args []string) { func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0] providerName := args[0]
registry := keybinds.GetDefaultRegistry() registry := keybinds.GetDefaultRegistry()
if providerName == "hyprland" { customPath, _ := cmd.Flags().GetString("path")
hyprlandPath, _ := cmd.Flags().GetString("hyprland-path") if customPath != "" {
hyprlandProvider := providers.NewHyprlandProvider(hyprlandPath) var provider keybinds.Provider
registry.Register(hyprlandProvider) switch providerName {
} case "hyprland":
provider = providers.NewHyprlandProvider(customPath)
case "mangowc":
provider = providers.NewMangoWCProvider(customPath)
case "sway":
provider = providers.NewSwayProvider(customPath)
default:
log.Fatalf("Provider %s does not support custom path", providerName)
}
if providerName == "mangowc" { sheet, err := provider.GetCheatSheet()
mangowcPath, _ := cmd.Flags().GetString("mangowc-path") if err != nil {
mangowcProvider := providers.NewMangoWCProvider(mangowcPath) log.Fatalf("Error getting cheatsheet: %v", err)
registry.Register(mangowcProvider) }
}
if providerName == "sway" { output, err := json.MarshalIndent(sheet, "", " ")
swayPath, _ := cmd.Flags().GetString("sway-path") if err != nil {
swayProvider := providers.NewSwayProvider(swayPath) log.Fatalf("Error generating JSON: %v", err)
registry.Register(swayProvider) }
fmt.Fprintln(os.Stdout, string(output))
return
} }
provider, err := registry.Get(providerName) provider, err := registry.Get(providerName)

View File

@@ -5,61 +5,64 @@ go 1.24.6
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.1.0 github.com/godbus/dbus/v5 v5.1.0
github.com/spf13/cobra v1.9.1 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.5.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.42.0 // indirect golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.47.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/ansi v0.11.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.38.0
golang.org/x/sys v0.36.0 golang.org/x/text v0.31.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -14,27 +14,33 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -44,23 +50,29 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA= github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM= github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w= github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU= github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd h1:30HEd5KKVM7GgMJ1GSNuYxuZXEg8Pdlngp6T51faxoc= github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd/go.mod h1:lz8PQr/p79XpFq5ODVBwRJu5LnOF8Et7j95ehqmCMJU= github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
@@ -79,8 +91,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -91,7 +103,6 @@ github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -101,36 +112,33 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 h1:iTAt1me6SBYsuzrl/CmrxtATPlOG/pVviosM3DhUdKE= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34/go.mod h1:jzmUN5lUAv2O8e63OvcauV4S30rIZ1BvF/PNYE37vDo= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -209,7 +209,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") "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)
} }

View File

@@ -165,7 +165,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = f.getNiriMapping(variants["niri"]) packages["niri"] = f.getNiriMapping(variants["niri"])
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"} packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
} }
return packages return packages
@@ -203,7 +203,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: RepoTypeCOPR, RepoURL: "yalter/niri"} return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
} }
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {

View File

@@ -478,6 +478,95 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
return fmt.Errorf("failed to create cache directory: %w", err) return fmt.Errorf("failed to create cache directory: %w", err)
} }
// Install hyprutils first
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.05,
Step: "Building hyprutils dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
}
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
return fmt.Errorf("failed to create hyprutils directory: %w", err)
}
defer os.RemoveAll(hyprutilsDir)
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
if err := cloneUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprutils: %w", err)
}
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-DBUILD_TESTING=off",
"-S", ".",
"-B", "./build")
configureUtilsCmd.Dir = hyprutilsDir
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
return fmt.Errorf("failed to configure hyprutils: %w", err)
}
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
buildUtilsCmd.Dir = hyprutilsDir
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
return fmt.Errorf("failed to build hyprutils: %w", err)
}
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installUtilsCmd.Dir = hyprutilsDir
if err := installUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprutils: %w", err)
}
// Install hyprwayland-scanner
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Building hyprwayland-scanner dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
}
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
if err := os.MkdirAll(scannerDir, 0755); err != nil {
return fmt.Errorf("failed to create scanner directory: %w", err)
}
defer os.RemoveAll(scannerDir)
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
if err := cloneScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
}
configureScannerCmd := exec.CommandContext(ctx, "cmake",
"-DCMAKE_INSTALL_PREFIX=/usr",
"-B", "build")
configureScannerCmd.Dir = scannerDir
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
}
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
buildScannerCmd.Dir = scannerDir
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
}
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installScannerCmd.Dir = scannerDir
if err := installScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
}
// Now build hyprpicker
tmpDir := filepath.Join(cacheDir, "hyprpicker-build") tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil { if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err) return fmt.Errorf("failed to create temp directory: %w", err)
@@ -486,7 +575,7 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
Progress: 0.2, Progress: 0.35,
Step: "Cloning hyprpicker repository...", Step: "Cloning hyprpicker repository...",
IsComplete: false, IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git", CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
@@ -499,16 +588,39 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
Progress: 0.4, Progress: 0.45,
Step: "Building hyprpicker...", Step: "Configuring hyprpicker build...",
IsComplete: false, IsComplete: false,
CommandInfo: "make all", CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
} }
buildCmd := exec.CommandContext(ctx, "make", "all") configureCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-S", ".",
"-B", "./build")
configureCmd.Dir = tmpDir
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
output, err := configureCmd.CombinedOutput()
if err != nil {
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
Step: "Building hyprpicker...",
IsComplete: false,
CommandInfo: "cmake --build build --target hyprpicker",
}
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
buildCmd.Dir = tmpDir buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := buildCmd.Run(); err != nil { if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
return fmt.Errorf("failed to build hyprpicker: %w", err) return fmt.Errorf("failed to build hyprpicker: %w", err)
} }
@@ -518,10 +630,10 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
Step: "Installing hyprpicker...", Step: "Installing hyprpicker...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: "sudo make install", CommandInfo: "sudo cmake --install build",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install") installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installCmd.Dir = tmpDir installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err) return fmt.Errorf("failed to install hyprpicker: %w", err)

View File

@@ -227,6 +227,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local"), ".local directory"}, {filepath.Join(homeDir, ".local"), ".local directory"},
{filepath.Join(homeDir, ".cache"), ".cache directory"}, {filepath.Join(homeDir, ".cache"), ".cache directory"},
{filepath.Join(homeDir, ".local", "state"), ".local/state directory"}, {filepath.Join(homeDir, ".local", "state"), ".local/state directory"},
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
} }
logFunc("\nSetting up parent directory ACLs for greeter user access...") logFunc("\nSetting up parent directory ACLs for greeter user access...")
@@ -239,8 +240,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
} }
} }
// Set ACL to allow greeter user execute (traverse) permission // Set ACL to allow greeter user read+execute permission (for session discovery)
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:x", dir.path); err != nil { if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path)) logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
continue continue
@@ -287,6 +288,8 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"}, {filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"}, {filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"}, {filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
{filepath.Join(homeDir, ".local", "share", "xsessions"), "xsessions"},
} }
for _, dir := range configDirs { for _, dir := range configDirs {

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/hyprland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
) )
@@ -26,7 +25,7 @@ func (h *HyprlandProvider) Name() string {
} }
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := hyprland.ParseKeys(h.configPath) section, err := ParseHyprlandKeys(h.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse hyprland config: %w", err) return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
} }
@@ -41,7 +40,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
}, nil }, nil
} }
func (h *HyprlandProvider) convertSection(section *hyprland.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -86,7 +85,7 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
} }
} }
func (h *HyprlandProvider) convertKeybind(kb *hyprland.KeyBinding, subcategory string) keybinds.Keybind { func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
key := h.formatKey(kb) key := h.formatKey(kb)
desc := kb.Comment desc := kb.Comment
@@ -108,7 +107,7 @@ func (h *HyprlandProvider) generateDescription(dispatcher, params string) string
return dispatcher return dispatcher
} }
func (h *HyprlandProvider) formatKey(kb *hyprland.KeyBinding) string { func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1) parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...) parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key) parts = append(parts, kb.Key)

View File

@@ -1,4 +1,4 @@
package hyprland package providers
import ( import (
"os" "os"
@@ -15,7 +15,7 @@ const (
var ModSeparators = []rune{'+', ' '} var ModSeparators = []rune{'+', ' '}
type KeyBinding struct { type HyprlandKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
Dispatcher string `json:"dispatcher"` Dispatcher string `json:"dispatcher"`
@@ -23,25 +23,25 @@ type KeyBinding struct {
Comment string `json:"comment"` Comment string `json:"comment"`
} }
type Section struct { type HyprlandSection struct {
Children []Section `json:"children"` Children []HyprlandSection `json:"children"`
Keybinds []KeyBinding `json:"keybinds"` Keybinds []HyprlandKeyBinding `json:"keybinds"`
Name string `json:"name"` Name string `json:"name"`
} }
type Parser struct { type HyprlandParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
} }
func NewParser() *Parser { func NewHyprlandParser() *HyprlandParser {
return &Parser{ return &HyprlandParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
} }
} }
func (p *Parser) ReadContent(directory string) error { func (p *HyprlandParser) ReadContent(directory string) error {
expandedDir := os.ExpandEnv(directory) expandedDir := os.ExpandEnv(directory)
expandedDir = filepath.Clean(expandedDir) expandedDir = filepath.Clean(expandedDir)
if strings.HasPrefix(expandedDir, "~") { if strings.HasPrefix(expandedDir, "~") {
@@ -87,7 +87,7 @@ func (p *Parser) ReadContent(directory string) error {
return nil return nil
} }
func autogenerateComment(dispatcher, params string) string { func hyprlandAutogenerateComment(dispatcher, params string) string {
switch dispatcher { switch dispatcher {
case "resizewindow": case "resizewindow":
return "Resize window" return "Resize window"
@@ -196,7 +196,7 @@ func autogenerateComment(dispatcher, params string) string {
} }
} }
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding { func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
line := p.contentLines[lineNumber] line := p.contentLines[lineNumber]
parts := strings.SplitN(line, "=", 2) parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 { if len(parts) < 2 {
@@ -232,7 +232,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
return nil return nil
} }
} else { } else {
comment = autogenerateComment(dispatcher, params) comment = hyprlandAutogenerateComment(dispatcher, params)
} }
var modList []string var modList []string
@@ -256,7 +256,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
return &KeyBinding{ return &HyprlandKeyBinding{
Mods: modList, Mods: modList,
Key: key, Key: key,
Dispatcher: dispatcher, Dispatcher: dispatcher,
@@ -265,7 +265,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section { func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
titleRegex := regexp.MustCompile(TitleRegex) titleRegex := regexp.MustCompile(TitleRegex)
for p.readingLine < len(p.contentLines) { for p.readingLine < len(p.contentLines) {
@@ -283,9 +283,9 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
sectionName := strings.TrimSpace(line[headingScope+1:]) sectionName := strings.TrimSpace(line[headingScope+1:])
p.readingLine++ p.readingLine++
childSection := &Section{ childSection := &HyprlandSection{
Children: []Section{}, Children: []HyprlandSection{},
Keybinds: []KeyBinding{}, Keybinds: []HyprlandKeyBinding{},
Name: sectionName, Name: sectionName,
} }
result := p.getBindsRecursive(childSection, headingScope) result := p.getBindsRecursive(childSection, headingScope)
@@ -312,18 +312,18 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
return currentContent return currentContent
} }
func (p *Parser) ParseKeys() *Section { func (p *HyprlandParser) ParseKeys() *HyprlandSection {
p.readingLine = 0 p.readingLine = 0
rootSection := &Section{ rootSection := &HyprlandSection{
Children: []Section{}, Children: []HyprlandSection{},
Keybinds: []KeyBinding{}, Keybinds: []HyprlandKeyBinding{},
Name: "", Name: "",
} }
return p.getBindsRecursive(rootSection, 0) return p.getBindsRecursive(rootSection, 0)
} }
func ParseKeys(path string) (*Section, error) { func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
parser := NewParser() parser := NewHyprlandParser()
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package hyprland package providers
import ( import (
"os" "os"
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func TestAutogenerateComment(t *testing.T) { func TestHyprlandAutogenerateComment(t *testing.T) {
tests := []struct { tests := []struct {
dispatcher string dispatcher string
params string params string
@@ -51,25 +51,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.dispatcher+"_"+tt.params, func(t *testing.T) { t.Run(tt.dispatcher+"_"+tt.params, func(t *testing.T) {
result := autogenerateComment(tt.dispatcher, tt.params) result := hyprlandAutogenerateComment(tt.dispatcher, tt.params)
if result != tt.expected { if result != tt.expected {
t.Errorf("autogenerateComment(%q, %q) = %q, want %q", t.Errorf("hyprlandAutogenerateComment(%q, %q) = %q, want %q",
tt.dispatcher, tt.params, result, tt.expected) tt.dispatcher, tt.params, result, tt.expected)
} }
}) })
} }
} }
func TestGetKeybindAtLine(t *testing.T) { func TestHyprlandGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *KeyBinding expected *HyprlandKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
line: "bind = SUPER, Q, killactive", line: "bind = SUPER, Q, killactive",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "Q", Key: "Q",
Dispatcher: "killactive", Dispatcher: "killactive",
@@ -80,7 +80,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_params", name: "keybind_with_params",
line: "bind = SUPER, left, movefocus, l", line: "bind = SUPER, left, movefocus, l",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "left", Key: "left",
Dispatcher: "movefocus", Dispatcher: "movefocus",
@@ -91,7 +91,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_comment", name: "keybind_with_comment",
line: "bind = SUPER, T, exec, kitty # Open terminal", line: "bind = SUPER, T, exec, kitty # Open terminal",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "T", Key: "T",
Dispatcher: "exec", Dispatcher: "exec",
@@ -107,7 +107,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_mods", name: "keybind_multiple_mods",
line: "bind = SUPER+SHIFT, F, fullscreen, 0", line: "bind = SUPER+SHIFT, F, fullscreen, 0",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER", "SHIFT"}, Mods: []string{"SUPER", "SHIFT"},
Key: "F", Key: "F",
Dispatcher: "fullscreen", Dispatcher: "fullscreen",
@@ -118,7 +118,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_no_mods", name: "keybind_no_mods",
line: "bind = , Print, exec, screenshot", line: "bind = , Print, exec, screenshot",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
Dispatcher: "exec", Dispatcher: "exec",
@@ -130,7 +130,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewHyprlandParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -171,7 +171,7 @@ func TestGetKeybindAtLine(t *testing.T) {
} }
} }
func TestParseKeysWithSections(t *testing.T) { func TestHyprlandParseKeysWithSections(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "hyprland.conf") configFile := filepath.Join(tmpDir, "hyprland.conf")
@@ -191,9 +191,9 @@ bind = SUPER, T, exec, kitty # Terminal
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseHyprlandKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseHyprlandKeys failed: %v", err)
} }
if len(section.Children) != 2 { if len(section.Children) != 2 {
@@ -236,7 +236,7 @@ bind = SUPER, T, exec, kitty # Terminal
} }
} }
func TestParseKeysWithCommentBinds(t *testing.T) { func TestHyprlandParseKeysWithCommentBinds(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "test.conf")
@@ -249,9 +249,9 @@ bind = SUPER, B, exec, app2
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseHyprlandKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseHyprlandKeys failed: %v", err)
} }
if len(section.Keybinds) != 3 { if len(section.Keybinds) != 3 {
@@ -269,7 +269,7 @@ bind = SUPER, B, exec, app2
} }
} }
func TestReadContentMultipleFiles(t *testing.T) { func TestHyprlandReadContentMultipleFiles(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "a.conf") file1 := filepath.Join(tmpDir, "a.conf")
@@ -285,7 +285,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewParser() parser := NewHyprlandParser()
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -296,7 +296,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
} }
} }
func TestReadContentErrors(t *testing.T) { func TestHyprlandReadContentErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
@@ -313,7 +313,7 @@ func TestReadContentErrors(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) {
_, err := ParseKeys(tt.path) _, err := ParseHyprlandKeys(tt.path)
if err == nil { if err == nil {
t.Error("Expected error, got nil") t.Error("Expected error, got nil")
} }
@@ -321,7 +321,7 @@ func TestReadContentErrors(t *testing.T) {
} }
} }
func TestReadContentWithTildeExpansion(t *testing.T) { func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Skip("Cannot get home directory") t.Skip("Cannot get home directory")
@@ -343,7 +343,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewParser() parser := NewHyprlandParser()
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -352,8 +352,8 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
} }
} }
func TestKeybindWithParamsContainingCommas(t *testing.T) { func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
parser := NewParser() parser := NewHyprlandParser()
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"} parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -368,7 +368,7 @@ func TestKeybindWithParamsContainingCommas(t *testing.T) {
} }
} }
func TestEmptyAndCommentLines(t *testing.T) { func TestHyprlandEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "test.conf")
@@ -385,9 +385,9 @@ bind = SUPER, T, exec, kitty
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseHyprlandKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseHyprlandKeys failed: %v", err)
} }
if len(section.Keybinds) != 2 { if len(section.Keybinds) != 2 {

View File

@@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
) )
type MangoWCProvider struct { type MangoWCProvider struct {
@@ -26,7 +25,7 @@ func (m *MangoWCProvider) Name() string {
} }
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
keybinds_list, err := mangowc.ParseKeys(m.configPath) keybinds_list, err := ParseMangoWCKeys(m.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse mangowc config: %w", err) return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
} }
@@ -83,7 +82,7 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
} }
} }
func (m *MangoWCProvider) convertKeybind(kb *mangowc.KeyBinding) keybinds.Keybind { func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
key := m.formatKey(kb) key := m.formatKey(kb)
desc := kb.Comment desc := kb.Comment
@@ -104,7 +103,7 @@ func (m *MangoWCProvider) generateDescription(command, params string) string {
return command return command
} }
func (m *MangoWCProvider) formatKey(kb *mangowc.KeyBinding) string { func (m *MangoWCProvider) formatKey(kb *MangoWCKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1) parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...) parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key) parts = append(parts, kb.Key)

View File

@@ -1,4 +1,4 @@
package mangowc package providers
import ( import (
"os" "os"
@@ -8,12 +8,12 @@ import (
) )
const ( const (
HideComment = "[hidden]" MangoWCHideComment = "[hidden]"
) )
var ModSeparators = []rune{'+', ' '} var MangoWCModSeparators = []rune{'+', ' '}
type KeyBinding struct { type MangoWCKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
Command string `json:"command"` Command string `json:"command"`
@@ -21,19 +21,19 @@ type KeyBinding struct {
Comment string `json:"comment"` Comment string `json:"comment"`
} }
type Parser struct { type MangoWCParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
} }
func NewParser() *Parser { func NewMangoWCParser() *MangoWCParser {
return &Parser{ return &MangoWCParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
} }
} }
func (p *Parser) ReadContent(path string) error { func (p *MangoWCParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath := os.ExpandEnv(path)
expandedPath = filepath.Clean(expandedPath) expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") { if strings.HasPrefix(expandedPath, "~") {
@@ -82,7 +82,7 @@ func (p *Parser) ReadContent(path string) error {
return nil return nil
} }
func autogenerateComment(command, params string) string { func mangowcAutogenerateComment(command, params string) string {
switch command { switch command {
case "spawn", "spawn_shell": case "spawn", "spawn_shell":
return params return params
@@ -196,7 +196,7 @@ func autogenerateComment(command, params string) string {
} }
} }
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding { func (p *MangoWCParser) getKeybindAtLine(lineNumber int) *MangoWCKeyBinding {
if lineNumber >= len(p.contentLines) { if lineNumber >= len(p.contentLines) {
return nil return nil
} }
@@ -220,7 +220,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
comment = strings.TrimSpace(parts[1]) comment = strings.TrimSpace(parts[1])
} }
if strings.HasPrefix(comment, HideComment) { if strings.HasPrefix(comment, MangoWCHideComment) {
return nil return nil
} }
@@ -239,16 +239,16 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
if comment == "" { if comment == "" {
comment = autogenerateComment(command, params) comment = mangowcAutogenerateComment(command, params)
} }
var modList []string var modList []string
if mods != "" && !strings.EqualFold(mods, "none") { if mods != "" && !strings.EqualFold(mods, "none") {
modstring := mods + string(ModSeparators[0]) modstring := mods + string(MangoWCModSeparators[0])
p := 0 p := 0
for index, char := range modstring { for index, char := range modstring {
isModSep := false isModSep := false
for _, sep := range ModSeparators { for _, sep := range MangoWCModSeparators {
if char == sep { if char == sep {
isModSep = true isModSep = true
break break
@@ -265,7 +265,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
_ = bindType _ = bindType
return &KeyBinding{ return &MangoWCKeyBinding{
Mods: modList, Mods: modList,
Key: key, Key: key,
Command: command, Command: command,
@@ -274,8 +274,8 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
func (p *Parser) ParseKeys() []KeyBinding { func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
var keybinds []KeyBinding var keybinds []MangoWCKeyBinding
for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ { for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ {
line := p.contentLines[lineNumber] line := p.contentLines[lineNumber]
@@ -296,8 +296,8 @@ func (p *Parser) ParseKeys() []KeyBinding {
return keybinds return keybinds
} }
func ParseKeys(path string) ([]KeyBinding, error) { func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
parser := NewParser() parser := NewMangoWCParser()
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package mangowc package providers
import ( import (
"os" "os"
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func TestAutogenerateComment(t *testing.T) { func TestMangoWCAutogenerateComment(t *testing.T) {
tests := []struct { tests := []struct {
command string command string
params string params string
@@ -60,25 +60,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.command+"_"+tt.params, func(t *testing.T) { t.Run(tt.command+"_"+tt.params, func(t *testing.T) {
result := autogenerateComment(tt.command, tt.params) result := mangowcAutogenerateComment(tt.command, tt.params)
if result != tt.expected { if result != tt.expected {
t.Errorf("autogenerateComment(%q, %q) = %q, want %q", t.Errorf("mangowcAutogenerateComment(%q, %q) = %q, want %q",
tt.command, tt.params, result, tt.expected) tt.command, tt.params, result, tt.expected)
} }
}) })
} }
} }
func TestGetKeybindAtLine(t *testing.T) { func TestMangoWCGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *KeyBinding expected *MangoWCKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
line: "bind=ALT,q,killclient,", line: "bind=ALT,q,killclient,",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "q", Key: "q",
Command: "killclient", Command: "killclient",
@@ -89,7 +89,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_params", name: "keybind_with_params",
line: "bind=ALT,Left,focusdir,left", line: "bind=ALT,Left,focusdir,left",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "Left", Key: "Left",
Command: "focusdir", Command: "focusdir",
@@ -100,7 +100,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_comment", name: "keybind_with_comment",
line: "bind=Alt,t,spawn,kitty # Open terminal", line: "bind=Alt,t,spawn,kitty # Open terminal",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"Alt"}, Mods: []string{"Alt"},
Key: "t", Key: "t",
Command: "spawn", Command: "spawn",
@@ -116,7 +116,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_mods", name: "keybind_multiple_mods",
line: "bind=SUPER+SHIFT,Up,exchange_client,up", line: "bind=SUPER+SHIFT,Up,exchange_client,up",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"SUPER", "SHIFT"}, Mods: []string{"SUPER", "SHIFT"},
Key: "Up", Key: "Up",
Command: "exchange_client", Command: "exchange_client",
@@ -127,7 +127,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_no_mods", name: "keybind_no_mods",
line: "bind=NONE,Print,spawn,screenshot", line: "bind=NONE,Print,spawn,screenshot",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
Command: "spawn", Command: "spawn",
@@ -138,7 +138,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_params", name: "keybind_multiple_params",
line: "bind=Ctrl,1,view,1,0", line: "bind=Ctrl,1,view,1,0",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"Ctrl"}, Mods: []string{"Ctrl"},
Key: "1", Key: "1",
Command: "view", Command: "view",
@@ -149,7 +149,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "bindl_flag", name: "bindl_flag",
line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock", line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"SUPER", "ALT"}, Mods: []string{"SUPER", "ALT"},
Key: "l", Key: "l",
Command: "spawn", Command: "spawn",
@@ -160,7 +160,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_spaces", name: "keybind_with_spaces",
line: "bind = SUPER, r, reload_config", line: "bind = SUPER, r, reload_config",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "r", Key: "r",
Command: "reload_config", Command: "reload_config",
@@ -172,7 +172,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewMangoWCParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -213,7 +213,7 @@ func TestGetKeybindAtLine(t *testing.T) {
} }
} }
func TestParseKeys(t *testing.T) { func TestMangoWCParseKeys(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -242,9 +242,9 @@ bind=Ctrl,2,view,2,0
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
keybinds, err := ParseKeys(configFile) keybinds, err := ParseMangoWCKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseMangoWCKeys failed: %v", err)
} }
expectedCount := 7 expectedCount := 7
@@ -267,7 +267,7 @@ bind=Ctrl,2,view,2,0
} }
} }
func TestReadContentMultipleFiles(t *testing.T) { func TestMangoWCReadContentMultipleFiles(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "a.conf") file1 := filepath.Join(tmpDir, "a.conf")
@@ -283,7 +283,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewParser() parser := NewMangoWCParser()
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -294,7 +294,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
} }
} }
func TestReadContentSingleFile(t *testing.T) { func TestMangoWCReadContentSingleFile(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -304,7 +304,7 @@ func TestReadContentSingleFile(t *testing.T) {
t.Fatalf("Failed to write config: %v", err) t.Fatalf("Failed to write config: %v", err)
} }
parser := NewParser() parser := NewMangoWCParser()
if err := parser.ReadContent(configFile); err != nil { if err := parser.ReadContent(configFile); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -315,7 +315,7 @@ func TestReadContentSingleFile(t *testing.T) {
} }
} }
func TestReadContentErrors(t *testing.T) { func TestMangoWCReadContentErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
@@ -332,7 +332,7 @@ func TestReadContentErrors(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) {
_, err := ParseKeys(tt.path) _, err := ParseMangoWCKeys(tt.path)
if err == nil { if err == nil {
t.Error("Expected error, got nil") t.Error("Expected error, got nil")
} }
@@ -340,7 +340,7 @@ func TestReadContentErrors(t *testing.T) {
} }
} }
func TestReadContentWithTildeExpansion(t *testing.T) { func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Skip("Cannot get home directory") t.Skip("Cannot get home directory")
@@ -362,7 +362,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewParser() parser := NewMangoWCParser()
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -371,7 +371,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
} }
} }
func TestEmptyAndCommentLines(t *testing.T) { func TestMangoWCEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -388,9 +388,9 @@ bind=Alt,t,spawn,kitty
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
keybinds, err := ParseKeys(configFile) keybinds, err := ParseMangoWCKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseMangoWCKeys failed: %v", err)
} }
if len(keybinds) != 2 { if len(keybinds) != 2 {
@@ -398,7 +398,7 @@ bind=Alt,t,spawn,kitty
} }
} }
func TestInvalidBindLines(t *testing.T) { func TestMangoWCInvalidBindLines(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
@@ -419,7 +419,7 @@ func TestInvalidBindLines(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewMangoWCParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -430,7 +430,7 @@ func TestInvalidBindLines(t *testing.T) {
} }
} }
func TestRealWorldConfig(t *testing.T) { func TestMangoWCRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -462,9 +462,9 @@ bind=Ctrl,3,view,3,0
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
keybinds, err := ParseKeys(configFile) keybinds, err := ParseMangoWCKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseMangoWCKeys failed: %v", err)
} }
if len(keybinds) < 14 { if len(keybinds) < 14 {

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
) )
func TestMangoWCProviderName(t *testing.T) { func TestMangoWCProviderName(t *testing.T) {
@@ -88,12 +86,12 @@ func TestMangoWCCategorizeByCommand(t *testing.T) {
func TestMangoWCFormatKey(t *testing.T) { func TestMangoWCFormatKey(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *mangowc.KeyBinding keybind *MangoWCKeyBinding
expected string expected string
}{ }{
{ {
name: "single_mod", name: "single_mod",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "q", Key: "q",
}, },
@@ -101,7 +99,7 @@ func TestMangoWCFormatKey(t *testing.T) {
}, },
{ {
name: "multiple_mods", name: "multiple_mods",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"SUPER", "SHIFT"}, Mods: []string{"SUPER", "SHIFT"},
Key: "Up", Key: "Up",
}, },
@@ -109,7 +107,7 @@ func TestMangoWCFormatKey(t *testing.T) {
}, },
{ {
name: "no_mods", name: "no_mods",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
}, },
@@ -131,13 +129,13 @@ func TestMangoWCFormatKey(t *testing.T) {
func TestMangoWCConvertKeybind(t *testing.T) { func TestMangoWCConvertKeybind(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *mangowc.KeyBinding keybind *MangoWCKeyBinding
wantKey string wantKey string
wantDesc string wantDesc string
}{ }{
{ {
name: "with_comment", name: "with_comment",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "t", Key: "t",
Command: "spawn", Command: "spawn",
@@ -149,7 +147,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
}, },
{ {
name: "without_comment", name: "without_comment",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "r", Key: "r",
Command: "reload_config", Command: "reload_config",
@@ -161,7 +159,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
}, },
{ {
name: "with_params_no_comment", name: "with_params_no_comment",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"CTRL"}, Mods: []string{"CTRL"},
Key: "1", Key: "1",
Command: "view", Command: "view",

View File

@@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
) )
type SwayProvider struct { type SwayProvider struct {
@@ -26,7 +25,7 @@ func (s *SwayProvider) Name() string {
} }
func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := sway.ParseKeys(s.configPath) section, err := ParseSwayKeys(s.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse sway config: %w", err) return nil, fmt.Errorf("failed to parse sway config: %w", err)
} }
@@ -41,7 +40,7 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
}, nil }, nil
} }
func (s *SwayProvider) convertSection(section *sway.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (s *SwayProvider) convertSection(section *SwaySection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -89,7 +88,7 @@ func (s *SwayProvider) categorizeByCommand(command string) string {
} }
} }
func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) keybinds.Keybind { func (s *SwayProvider) convertKeybind(kb *SwayKeyBinding, subcategory string) keybinds.Keybind {
key := s.formatKey(kb) key := s.formatKey(kb)
desc := kb.Comment desc := kb.Comment
@@ -104,7 +103,7 @@ func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) k
} }
} }
func (s *SwayProvider) formatKey(kb *sway.KeyBinding) string { func (s *SwayProvider) formatKey(kb *SwayKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1) parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...) parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key) parts = append(parts, kb.Key)

View File

@@ -1,4 +1,4 @@
package sway package providers
import ( import (
"os" "os"
@@ -8,40 +8,40 @@ import (
) )
const ( const (
TitleRegex = "#+!" SwayTitleRegex = "#+!"
HideComment = "[hidden]" SwayHideComment = "[hidden]"
) )
var ModSeparators = []rune{'+', ' '} var SwayModSeparators = []rune{'+', ' '}
type KeyBinding struct { type SwayKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
Command string `json:"command"` Command string `json:"command"`
Comment string `json:"comment"` Comment string `json:"comment"`
} }
type Section struct { type SwaySection struct {
Children []Section `json:"children"` Children []SwaySection `json:"children"`
Keybinds []KeyBinding `json:"keybinds"` Keybinds []SwayKeyBinding `json:"keybinds"`
Name string `json:"name"` Name string `json:"name"`
} }
type Parser struct { type SwayParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
variables map[string]string variables map[string]string
} }
func NewParser() *Parser { func NewSwayParser() *SwayParser {
return &Parser{ return &SwayParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
variables: make(map[string]string), variables: make(map[string]string),
} }
} }
func (p *Parser) ReadContent(path string) error { func (p *SwayParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath := os.ExpandEnv(path)
expandedPath = filepath.Clean(expandedPath) expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") { if strings.HasPrefix(expandedPath, "~") {
@@ -88,7 +88,7 @@ func (p *Parser) ReadContent(path string) error {
return nil return nil
} }
func (p *Parser) parseVariables() { func (p *SwayParser) parseVariables() {
setRegex := regexp.MustCompile(`^\s*set\s+\$(\w+)\s+(.+)$`) setRegex := regexp.MustCompile(`^\s*set\s+\$(\w+)\s+(.+)$`)
for _, line := range p.contentLines { for _, line := range p.contentLines {
matches := setRegex.FindStringSubmatch(line) matches := setRegex.FindStringSubmatch(line)
@@ -100,7 +100,7 @@ func (p *Parser) parseVariables() {
} }
} }
func (p *Parser) expandVariables(text string) string { func (p *SwayParser) expandVariables(text string) string {
result := text result := text
for varName, varValue := range p.variables { for varName, varValue := range p.variables {
result = strings.ReplaceAll(result, "$"+varName, varValue) result = strings.ReplaceAll(result, "$"+varName, varValue)
@@ -108,7 +108,7 @@ func (p *Parser) expandVariables(text string) string {
return result return result
} }
func autogenerateComment(command string) string { func swayAutogenerateComment(command string) string {
command = strings.TrimSpace(command) command = strings.TrimSpace(command)
if strings.HasPrefix(command, "exec ") { if strings.HasPrefix(command, "exec ") {
@@ -200,7 +200,7 @@ func autogenerateComment(command string) string {
} }
} }
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding { func (p *SwayParser) getKeybindAtLine(lineNumber int) *SwayKeyBinding {
if lineNumber >= len(p.contentLines) { if lineNumber >= len(p.contentLines) {
return nil return nil
} }
@@ -223,7 +223,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
comment = strings.TrimSpace(parts[1]) comment = strings.TrimSpace(parts[1])
} }
if strings.HasPrefix(comment, HideComment) { if strings.HasPrefix(comment, SwayHideComment) {
return nil return nil
} }
@@ -249,11 +249,11 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
var modList []string var modList []string
var key string var key string
modstring := keyCombo + string(ModSeparators[0]) modstring := keyCombo + string(SwayModSeparators[0])
pos := 0 pos := 0
for index, char := range modstring { for index, char := range modstring {
isModSep := false isModSep := false
for _, sep := range ModSeparators { for _, sep := range SwayModSeparators {
if char == sep { if char == sep {
isModSep = true isModSep = true
break break
@@ -262,7 +262,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
if isModSep { if isModSep {
if index-pos > 0 { if index-pos > 0 {
part := modstring[pos:index] part := modstring[pos:index]
if isMod(part) { if swayIsMod(part) {
modList = append(modList, part) modList = append(modList, part)
} else { } else {
key = part key = part
@@ -273,12 +273,12 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
if comment == "" { if comment == "" {
comment = autogenerateComment(command) comment = swayAutogenerateComment(command)
} }
_ = flags _ = flags
return &KeyBinding{ return &SwayKeyBinding{
Mods: modList, Mods: modList,
Key: key, Key: key,
Command: command, Command: command,
@@ -286,7 +286,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
func isMod(s string) bool { func swayIsMod(s string) bool {
s = strings.ToLower(s) s = strings.ToLower(s)
if s == "mod1" || s == "mod2" || s == "mod3" || s == "mod4" || s == "mod5" || if s == "mod1" || s == "mod2" || s == "mod3" || s == "mod4" || s == "mod5" ||
s == "shift" || s == "control" || s == "ctrl" || s == "alt" || s == "super" || s == "shift" || s == "control" || s == "ctrl" || s == "alt" || s == "super" ||
@@ -307,8 +307,8 @@ func isMod(s string) bool {
return false return false
} }
func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section { func (p *SwayParser) getBindsRecursive(currentContent *SwaySection, scope int) *SwaySection {
titleRegex := regexp.MustCompile(TitleRegex) titleRegex := regexp.MustCompile(SwayTitleRegex)
for p.readingLine < len(p.contentLines) { for p.readingLine < len(p.contentLines) {
line := p.contentLines[p.readingLine] line := p.contentLines[p.readingLine]
@@ -325,9 +325,9 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
sectionName := strings.TrimSpace(line[headingScope+1:]) sectionName := strings.TrimSpace(line[headingScope+1:])
p.readingLine++ p.readingLine++
childSection := &Section{ childSection := &SwaySection{
Children: []Section{}, Children: []SwaySection{},
Keybinds: []KeyBinding{}, Keybinds: []SwayKeyBinding{},
Name: sectionName, Name: sectionName,
} }
result := p.getBindsRecursive(childSection, headingScope) result := p.getBindsRecursive(childSection, headingScope)
@@ -348,18 +348,18 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
return currentContent return currentContent
} }
func (p *Parser) ParseKeys() *Section { func (p *SwayParser) ParseKeys() *SwaySection {
p.readingLine = 0 p.readingLine = 0
rootSection := &Section{ rootSection := &SwaySection{
Children: []Section{}, Children: []SwaySection{},
Keybinds: []KeyBinding{}, Keybinds: []SwayKeyBinding{},
Name: "", Name: "",
} }
return p.getBindsRecursive(rootSection, 0) return p.getBindsRecursive(rootSection, 0)
} }
func ParseKeys(path string) (*Section, error) { func ParseSwayKeys(path string) (*SwaySection, error) {
parser := NewParser() parser := NewSwayParser()
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package sway package providers
import ( import (
"os" "os"
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func TestAutogenerateComment(t *testing.T) { func TestSwayAutogenerateComment(t *testing.T) {
tests := []struct { tests := []struct {
command string command string
expected string expected string
@@ -46,25 +46,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) { t.Run(tt.command, func(t *testing.T) {
result := autogenerateComment(tt.command) result := swayAutogenerateComment(tt.command)
if result != tt.expected { if result != tt.expected {
t.Errorf("autogenerateComment(%q) = %q, want %q", t.Errorf("swayAutogenerateComment(%q) = %q, want %q",
tt.command, result, tt.expected) tt.command, result, tt.expected)
} }
}) })
} }
} }
func TestGetKeybindAtLine(t *testing.T) { func TestSwayGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *KeyBinding expected *SwayKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
line: "bindsym Mod4+q kill", line: "bindsym Mod4+q kill",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "q", Key: "q",
Command: "kill", Command: "kill",
@@ -74,7 +74,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_exec", name: "keybind_with_exec",
line: "bindsym Mod4+t exec kitty", line: "bindsym Mod4+t exec kitty",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "t", Key: "t",
Command: "exec kitty", Command: "exec kitty",
@@ -84,7 +84,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_comment", name: "keybind_with_comment",
line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher", line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "Space", Key: "Space",
Command: "exec dms ipc call spotlight toggle", Command: "exec dms ipc call spotlight toggle",
@@ -99,7 +99,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_mods", name: "keybind_multiple_mods",
line: "bindsym Mod4+Shift+e exit", line: "bindsym Mod4+Shift+e exit",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4", "Shift"}, Mods: []string{"Mod4", "Shift"},
Key: "e", Key: "e",
Command: "exit", Command: "exit",
@@ -109,7 +109,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_no_mods", name: "keybind_no_mods",
line: "bindsym Print exec grim screenshot.png", line: "bindsym Print exec grim screenshot.png",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
Command: "exec grim screenshot.png", Command: "exec grim screenshot.png",
@@ -119,7 +119,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_flags", name: "keybind_with_flags",
line: "bindsym --release Mod4+x exec notify-send released", line: "bindsym --release Mod4+x exec notify-send released",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "x", Key: "x",
Command: "exec notify-send released", Command: "exec notify-send released",
@@ -129,7 +129,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_focus_direction", name: "keybind_focus_direction",
line: "bindsym Mod4+Left focus left", line: "bindsym Mod4+Left focus left",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "Left", Key: "Left",
Command: "focus left", Command: "focus left",
@@ -139,7 +139,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_workspace", name: "keybind_workspace",
line: "bindsym Mod4+1 workspace number 1", line: "bindsym Mod4+1 workspace number 1",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "1", Key: "1",
Command: "workspace number 1", Command: "workspace number 1",
@@ -150,7 +150,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewSwayParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -188,7 +188,7 @@ func TestGetKeybindAtLine(t *testing.T) {
} }
} }
func TestVariableExpansion(t *testing.T) { func TestSwayVariableExpansion(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -204,9 +204,9 @@ bindsym $mod+d exec $menu
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(configFile) section, err := ParseSwayKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Keybinds) != 2 { if len(section.Keybinds) != 2 {
@@ -229,7 +229,7 @@ bindsym $mod+d exec $menu
} }
} }
func TestParseKeysWithSections(t *testing.T) { func TestSwayParseKeysWithSections(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -251,9 +251,9 @@ bindsym $mod+t exec kitty # Terminal
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseSwayKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Children) != 2 { if len(section.Children) != 2 {
@@ -296,7 +296,7 @@ bindsym $mod+t exec kitty # Terminal
} }
} }
func TestReadContentErrors(t *testing.T) { func TestSwayReadContentErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
@@ -313,7 +313,7 @@ func TestReadContentErrors(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) {
_, err := ParseKeys(tt.path) _, err := ParseSwayKeys(tt.path)
if err == nil { if err == nil {
t.Error("Expected error, got nil") t.Error("Expected error, got nil")
} }
@@ -321,7 +321,7 @@ func TestReadContentErrors(t *testing.T) {
} }
} }
func TestReadContentWithTildeExpansion(t *testing.T) { func TestSwayReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Skip("Cannot get home directory") t.Skip("Cannot get home directory")
@@ -343,7 +343,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewParser() parser := NewSwayParser()
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -352,7 +352,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
} }
} }
func TestEmptyAndCommentLines(t *testing.T) { func TestSwayEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -369,9 +369,9 @@ bindsym Mod4+t exec kitty
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(configFile) section, err := ParseSwayKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Keybinds) != 2 { if len(section.Keybinds) != 2 {
@@ -379,7 +379,7 @@ bindsym Mod4+t exec kitty
} }
} }
func TestRealWorldConfig(t *testing.T) { func TestSwayRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -408,9 +408,9 @@ bindsym $mod+Shift+1 move container to workspace number 1
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(configFile) section, err := ParseSwayKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Keybinds) < 9 { if len(section.Keybinds) < 9 {
@@ -444,7 +444,7 @@ bindsym $mod+Shift+1 move container to workspace number 1
} }
} }
func TestIsMod(t *testing.T) { func TestSwayIsMod(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
expected bool expected bool
@@ -462,9 +462,9 @@ func TestIsMod(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) { t.Run(tt.input, func(t *testing.T) {
result := isMod(tt.input) result := swayIsMod(tt.input)
if result != tt.expected { if result != tt.expected {
t.Errorf("isMod(%q) = %v, want %v", tt.input, result, tt.expected) t.Errorf("swayIsMod(%q) = %v, want %v", tt.input, result, tt.expected)
} }
}) })
} }

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
) )
func TestSwayProviderName(t *testing.T) { func TestSwayProviderName(t *testing.T) {
@@ -76,12 +74,12 @@ func TestSwayCategorizeByCommand(t *testing.T) {
func TestSwayFormatKey(t *testing.T) { func TestSwayFormatKey(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *sway.KeyBinding keybind *SwayKeyBinding
expected string expected string
}{ }{
{ {
name: "single_mod", name: "single_mod",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "q", Key: "q",
}, },
@@ -89,7 +87,7 @@ func TestSwayFormatKey(t *testing.T) {
}, },
{ {
name: "multiple_mods", name: "multiple_mods",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4", "Shift"}, Mods: []string{"Mod4", "Shift"},
Key: "e", Key: "e",
}, },
@@ -97,7 +95,7 @@ func TestSwayFormatKey(t *testing.T) {
}, },
{ {
name: "no_mods", name: "no_mods",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
}, },
@@ -119,13 +117,13 @@ func TestSwayFormatKey(t *testing.T) {
func TestSwayConvertKeybind(t *testing.T) { func TestSwayConvertKeybind(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *sway.KeyBinding keybind *SwayKeyBinding
wantKey string wantKey string
wantDesc string wantDesc string
}{ }{
{ {
name: "with_comment", name: "with_comment",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "t", Key: "t",
Command: "exec kitty", Command: "exec kitty",
@@ -136,7 +134,7 @@ func TestSwayConvertKeybind(t *testing.T) {
}, },
{ {
name: "without_comment", name: "without_comment",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "r", Key: "r",
Command: "reload", Command: "reload",

View File

@@ -1,4 +1,4 @@
package logger package log
import ( import (
"bufio" "bufio"

View File

@@ -0,0 +1,295 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_evdev
import (
go_evdev "github.com/holoplot/go-evdev"
mock "github.com/stretchr/testify/mock"
)
// MockEvdevDevice is an autogenerated mock type for the EvdevDevice type
type MockEvdevDevice struct {
mock.Mock
}
type MockEvdevDevice_Expecter struct {
mock *mock.Mock
}
func (_m *MockEvdevDevice) EXPECT() *MockEvdevDevice_Expecter {
return &MockEvdevDevice_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockEvdevDevice) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockEvdevDevice_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockEvdevDevice_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) Close() *MockEvdevDevice_Close_Call {
return &MockEvdevDevice_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockEvdevDevice_Close_Call) Run(run func()) *MockEvdevDevice_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_Close_Call) Return(_a0 error) *MockEvdevDevice_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockEvdevDevice_Close_Call) RunAndReturn(run func() error) *MockEvdevDevice_Close_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with no fields
func (_m *MockEvdevDevice) Name() (string, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func() (string, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockEvdevDevice_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockEvdevDevice_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) Name() *MockEvdevDevice_Name_Call {
return &MockEvdevDevice_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockEvdevDevice_Name_Call) Run(run func()) *MockEvdevDevice_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_Name_Call) Return(_a0 string, _a1 error) *MockEvdevDevice_Name_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockEvdevDevice_Name_Call) RunAndReturn(run func() (string, error)) *MockEvdevDevice_Name_Call {
_c.Call.Return(run)
return _c
}
// Path provides a mock function with no fields
func (_m *MockEvdevDevice) Path() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Path")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockEvdevDevice_Path_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Path'
type MockEvdevDevice_Path_Call struct {
*mock.Call
}
// Path is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) Path() *MockEvdevDevice_Path_Call {
return &MockEvdevDevice_Path_Call{Call: _e.mock.On("Path")}
}
func (_c *MockEvdevDevice_Path_Call) Run(run func()) *MockEvdevDevice_Path_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_Path_Call) Return(_a0 string) *MockEvdevDevice_Path_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockEvdevDevice_Path_Call) RunAndReturn(run func() string) *MockEvdevDevice_Path_Call {
_c.Call.Return(run)
return _c
}
// ReadOne provides a mock function with no fields
func (_m *MockEvdevDevice) ReadOne() (*go_evdev.InputEvent, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ReadOne")
}
var r0 *go_evdev.InputEvent
var r1 error
if rf, ok := ret.Get(0).(func() (*go_evdev.InputEvent, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *go_evdev.InputEvent); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*go_evdev.InputEvent)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockEvdevDevice_ReadOne_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadOne'
type MockEvdevDevice_ReadOne_Call struct {
*mock.Call
}
// ReadOne is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) ReadOne() *MockEvdevDevice_ReadOne_Call {
return &MockEvdevDevice_ReadOne_Call{Call: _e.mock.On("ReadOne")}
}
func (_c *MockEvdevDevice_ReadOne_Call) Run(run func()) *MockEvdevDevice_ReadOne_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_ReadOne_Call) Return(_a0 *go_evdev.InputEvent, _a1 error) *MockEvdevDevice_ReadOne_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockEvdevDevice_ReadOne_Call) RunAndReturn(run func() (*go_evdev.InputEvent, error)) *MockEvdevDevice_ReadOne_Call {
_c.Call.Return(run)
return _c
}
// State provides a mock function with given fields: t
func (_m *MockEvdevDevice) State(t go_evdev.EvType) (go_evdev.StateMap, error) {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for State")
}
var r0 go_evdev.StateMap
var r1 error
if rf, ok := ret.Get(0).(func(go_evdev.EvType) (go_evdev.StateMap, error)); ok {
return rf(t)
}
if rf, ok := ret.Get(0).(func(go_evdev.EvType) go_evdev.StateMap); ok {
r0 = rf(t)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(go_evdev.StateMap)
}
}
if rf, ok := ret.Get(1).(func(go_evdev.EvType) error); ok {
r1 = rf(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockEvdevDevice_State_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'State'
type MockEvdevDevice_State_Call struct {
*mock.Call
}
// State is a helper method to define mock.On call
// - t go_evdev.EvType
func (_e *MockEvdevDevice_Expecter) State(t interface{}) *MockEvdevDevice_State_Call {
return &MockEvdevDevice_State_Call{Call: _e.mock.On("State", t)}
}
func (_c *MockEvdevDevice_State_Call) Run(run func(t go_evdev.EvType)) *MockEvdevDevice_State_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(go_evdev.EvType))
})
return _c
}
func (_c *MockEvdevDevice_State_Call) Return(_a0 go_evdev.StateMap, _a1 error) *MockEvdevDevice_State_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockEvdevDevice_State_Call) RunAndReturn(run func(go_evdev.EvType) (go_evdev.StateMap, error)) *MockEvdevDevice_State_Call {
_c.Call.Return(run)
return _c
}
// NewMockEvdevDevice creates a new instance of MockEvdevDevice. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockEvdevDevice(t interface {
mock.TestingT
Cleanup(func())
}) *MockEvdevDevice {
mock := &MockEvdevDevice{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,12 +1,12 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml // XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
// //
// dwl_ipc_unstable_v2 Protocol Copyright: // dwl_ipc_unstable_v2 Protocol Copyright:
package dwl_ipc package dwl_ipc
import "github.com/yaslama/go-wayland/wayland/client" import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the // It can be used to match the [client.RegistryGlobalEvent.Interface] in the

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : ext-workspace-v1.xml // XML file : ext-workspace-v1.xml
// //
// ext_workspace_v1 Protocol Copyright: // ext_workspace_v1 Protocol Copyright:
@@ -35,7 +35,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
// registerServerProxy registers a proxy with a server-assigned ID. // registerServerProxy registers a proxy with a server-assigned ID.
@@ -61,8 +62,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
return return
} }
objectsMap := reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap.SetMapIndex(reflect.ValueOf(serverID), reflect.ValueOf(proxy)) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap.Store(serverID, proxy)
} }
// ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml // XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml
// //
// wlr_gamma_control_unstable_v1 Protocol Copyright: // wlr_gamma_control_unstable_v1 Protocol Copyright:
@@ -31,7 +31,7 @@
package wlr_gamma_control package wlr_gamma_control
import ( import (
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml // XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml
// //
// wlr_output_management_unstable_v1 Protocol Copyright: // wlr_output_management_unstable_v1 Protocol Copyright:
@@ -33,7 +33,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) { func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
@@ -47,9 +48,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
if !objectsField.IsValid() { if !objectsField.IsValid() {
return return
} }
objectsField = reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap := objectsField.Interface().(map[uint32]client.Proxy) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap[serverID] = proxy objectsMap.Store(serverID, proxy)
} }
// ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].

View File

@@ -30,17 +30,13 @@ func NewManager() (*Manager, error) {
PairedDevices: []Device{}, PairedDevices: []Device{},
ConnectedDevices: []Device{}, ConnectedDevices: []Device{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan BluetoothState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dbusConn: conn,
dbusConn: conn, signals: make(chan *dbus.Signal, 256),
signals: make(chan *dbus.Signal, 256), dirty: make(chan struct{}, 1),
pairingSubscribers: make(map[string]chan PairingPrompt), eventQueue: make(chan func(), 32),
pairingSubMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
pendingPairings: make(map[string]bool),
eventQueue: make(chan func(), 32),
} }
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
@@ -360,12 +356,7 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
if hasPaired { if hasPaired {
if paired, ok := pairedVar.Value().(bool); ok && paired { if paired, ok := pairedVar.Value().(bool); ok && paired {
devicePath := string(path) devicePath := string(path)
m.pendingPairingsMux.Lock() _, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
wasPending := m.pendingPairings[devicePath]
if wasPending {
delete(m.pendingPairings, devicePath)
}
m.pendingPairingsMux.Unlock()
if wasPending { if wasPending {
select { select {
@@ -430,28 +421,20 @@ func (m *Manager) notifier() {
} }
m.updateDevices() m.updateDevices()
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -484,48 +467,36 @@ func (m *Manager) snapshotState() BluetoothState {
func (m *Manager) Subscribe(id string) chan BluetoothState { func (m *Manager) Subscribe(id string) chan BluetoothState {
ch := make(chan BluetoothState, 64) ch := make(chan BluetoothState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribePairing(id string) chan PairingPrompt { func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
ch := make(chan PairingPrompt, 16) ch := make(chan PairingPrompt, 16)
m.pairingSubMutex.Lock() m.pairingSubscribers.Store(id, ch)
m.pairingSubscribers[id] = ch
m.pairingSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribePairing(id string) { func (m *Manager) UnsubscribePairing(id string) {
m.pairingSubMutex.Lock() if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.pairingSubscribers[id]; ok {
close(ch) close(ch)
delete(m.pairingSubscribers, id)
} }
m.pairingSubMutex.Unlock()
} }
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) { func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
m.pairingSubMutex.RLock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
defer m.pairingSubMutex.RUnlock()
for _, ch := range m.pairingSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error { func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error {
@@ -566,17 +537,13 @@ func (m *Manager) SetPowered(powered bool) error {
} }
func (m *Manager) PairDevice(devicePath string) error { func (m *Manager) PairDevice(devicePath string) error {
m.pendingPairingsMux.Lock() m.pendingPairings.Store(devicePath, true)
m.pendingPairings[devicePath] = true
m.pendingPairingsMux.Unlock()
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
err := obj.Call(device1Iface+".Pair", 0).Err err := obj.Call(device1Iface+".Pair", 0).Err
if err != nil { if err != nil {
m.pendingPairingsMux.Lock() m.pendingPairings.Delete(devicePath)
delete(m.pendingPairings, devicePath)
m.pendingPairingsMux.Unlock()
} }
return err return err
@@ -618,19 +585,17 @@ func (m *Manager) Close() {
m.agent.Close() m.agent.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan BluetoothState) return true
m.subMutex.Unlock() })
m.pairingSubMutex.Lock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
for _, ch := range m.pairingSubscribers {
close(ch) close(ch)
} m.pairingSubscribers.Delete(key)
m.pairingSubscribers = make(map[string]chan PairingPrompt) return true
m.pairingSubMutex.Unlock() })
if m.dbusConn != nil { if m.dbusConn != nil {
m.dbusConn.Close() m.dbusConn.Close()

View File

@@ -3,22 +3,19 @@ package bluez
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest
broadcastPrompt func(PairingPrompt) broadcastPrompt func(PairingPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply),
requests: make(map[string]PromptRequest),
broadcastPrompt: broadcastPrompt, broadcastPrompt: broadcastPrompt,
} }
} }
@@ -30,10 +27,8 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := PairingPrompt{ prompt := PairingPrompt{
@@ -53,10 +48,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -75,10 +67,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
} }
@@ -92,8 +81,6 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() b.pending.Delete(token)
delete(b.pending, token) b.requests.Delete(token)
delete(b.requests, token)
b.mu.Unlock()
} }

View File

@@ -3,6 +3,7 @@ package bluez
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -59,22 +60,19 @@ type PairingPrompt struct {
type Manager struct { type Manager struct {
state *BluetoothState state *BluetoothState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan BluetoothState subscribers syncmap.Map[string, chan BluetoothState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dbusConn *dbus.Conn dbusConn *dbus.Conn
signals chan *dbus.Signal signals chan *dbus.Signal
sigWG sync.WaitGroup sigWG sync.WaitGroup
agent *BluezAgent agent *BluezAgent
promptBroker PromptBroker promptBroker PromptBroker
pairingSubscribers map[string]chan PairingPrompt pairingSubscribers syncmap.Map[string, chan PairingPrompt]
pairingSubMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *BluetoothState lastNotifiedState *BluetoothState
adapterPath dbus.ObjectPath adapterPath dbus.ObjectPath
pendingPairings map[string]bool pendingPairings syncmap.Map[string, bool]
pendingPairingsMux sync.Mutex
eventQueue chan func() eventQueue chan func()
eventWg sync.WaitGroup eventWg sync.WaitGroup
} }

View File

@@ -24,7 +24,6 @@ const (
func NewDDCBackend() (*DDCBackend, error) { func NewDDCBackend() (*DDCBackend, error) {
b := &DDCBackend{ b := &DDCBackend{
devices: make(map[string]*ddcDevice),
scanInterval: 30 * time.Second, scanInterval: 30 * time.Second,
debounceTimers: make(map[string]*time.Timer), debounceTimers: make(map[string]*time.Timer),
debouncePending: make(map[string]ddcPendingSet), debouncePending: make(map[string]ddcPendingSet),
@@ -53,10 +52,10 @@ func (b *DDCBackend) scanI2CDevices() error {
return nil return nil
} }
b.devicesMutex.Lock() b.devices.Range(func(key string, value *ddcDevice) bool {
defer b.devicesMutex.Unlock() b.devices.Delete(key)
return true
b.devices = make(map[string]*ddcDevice) })
for i := 0; i < 32; i++ { for i := 0; i < 32; i++ {
busPath := fmt.Sprintf("/dev/i2c-%d", i) busPath := fmt.Sprintf("/dev/i2c-%d", i)
@@ -64,7 +63,6 @@ func (b *DDCBackend) scanI2CDevices() error {
continue continue
} }
// Skip SMBus, GPU internal buses (e.g. AMDGPU SMU) to prevent GPU hangs
if isIgnorableI2CBus(i) { if isIgnorableI2CBus(i) {
log.Debugf("Skipping ignorable i2c-%d", i) log.Debugf("Skipping ignorable i2c-%d", i)
continue continue
@@ -77,7 +75,7 @@ func (b *DDCBackend) scanI2CDevices() error {
id := fmt.Sprintf("ddc:i2c-%d", i) id := fmt.Sprintf("ddc:i2c-%d", i)
dev.id = id dev.id = id
b.devices[id] = dev b.devices.Store(id, dev)
log.Debugf("found DDC device on i2c-%d", i) log.Debugf("found DDC device on i2c-%d", i)
} }
@@ -164,12 +162,9 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
log.Debugf("DDC scan error: %v", err) log.Debugf("DDC scan error: %v", err)
} }
b.devicesMutex.Lock() devices := make([]Device, 0)
defer b.devicesMutex.Unlock()
devices := make([]Device, 0, len(b.devices)) b.devices.Range(func(id string, dev *ddcDevice) bool {
for id, dev := range b.devices {
devices = append(devices, Device{ devices = append(devices, Device{
Class: ClassDDC, Class: ClassDDC,
ID: id, ID: id,
@@ -179,7 +174,8 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
CurrentPercent: dev.lastBrightness, CurrentPercent: dev.lastBrightness,
Backend: "ddc", Backend: "ddc",
}) })
} return true
})
return devices, nil return devices, nil
} }
@@ -189,9 +185,7 @@ func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callb
} }
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error { func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
b.devicesMutex.RLock() _, ok := b.devices.Load(id)
_, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -202,8 +196,6 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
b.debounceMutex.Lock() b.debounceMutex.Lock()
defer b.debounceMutex.Unlock()
b.debouncePending[id] = ddcPendingSet{ b.debouncePending[id] = ddcPendingSet{
percent: value, percent: value,
callback: callback, callback: callback,
@@ -234,14 +226,13 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
}) })
} }
b.debounceMutex.Unlock()
return nil return nil
} }
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error { func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error {
b.devicesMutex.RLock() dev, ok := b.devices.Load(id)
dev, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -266,9 +257,8 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
return fmt.Errorf("get current capability: %w", err) return fmt.Errorf("get current capability: %w", err)
} }
max = cap.max max = cap.max
b.devicesMutex.Lock()
dev.max = max dev.max = max
b.devicesMutex.Unlock() b.devices.Store(id, dev)
} }
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil { if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil {
@@ -277,10 +267,9 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
log.Debugf("set %s to %d/%d", id, value, max) log.Debugf("set %s to %d/%d", id, value, max)
b.devicesMutex.Lock()
dev.max = max dev.max = max
dev.lastBrightness = value dev.lastBrightness = value
b.devicesMutex.Unlock() b.devices.Store(id, dev)
return nil return nil
} }

View File

@@ -15,10 +15,8 @@ func NewManager() (*Manager, error) {
func NewManagerWithOptions(exponential bool) (*Manager, error) { func NewManagerWithOptions(exponential bool) (*Manager, error) {
m := &Manager{ m := &Manager{
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate), exponential: exponential,
stopChan: make(chan struct{}),
exponential: exponential,
} }
go m.initLogind() go m.initLogind()
@@ -360,20 +358,13 @@ func (m *Manager) broadcastDeviceUpdate(deviceID string) {
update := DeviceUpdate{Device: *targetDevice} update := DeviceUpdate{Device: *targetDevice}
m.subMutex.RLock()
defer m.subMutex.RUnlock()
if len(m.updateSubscribers) == 0 {
log.Debugf("No update subscribers for device: %s", deviceID)
return
}
log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent) log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent)
for _, ch := range m.updateSubscribers { m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
select { select {
case ch <- update: case ch <- update:
default: default:
} }
} return true
})
} }

View File

@@ -13,9 +13,8 @@ import (
func NewSysfsBackend() (*SysfsBackend, error) { func NewSysfsBackend() (*SysfsBackend, error) {
b := &SysfsBackend{ b := &SysfsBackend{
basePath: "/sys/class", basePath: "/sys/class",
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
@@ -26,9 +25,6 @@ func NewSysfsBackend() (*SysfsBackend, error) {
} }
func (b *SysfsBackend) scanDevices() error { func (b *SysfsBackend) scanDevices() error {
b.deviceCacheMutex.Lock()
defer b.deviceCacheMutex.Unlock()
for _, class := range b.classes { for _, class := range b.classes {
classPath := filepath.Join(b.basePath, class) classPath := filepath.Join(b.basePath, class)
entries, err := os.ReadDir(classPath) entries, err := os.ReadDir(classPath)
@@ -68,13 +64,13 @@ func (b *SysfsBackend) scanDevices() error {
} }
deviceID := fmt.Sprintf("%s:%s", class, entry.Name()) deviceID := fmt.Sprintf("%s:%s", class, entry.Name())
b.deviceCache[deviceID] = &sysfsDevice{ b.deviceCache.Store(deviceID, &sysfsDevice{
class: deviceClass, class: deviceClass,
id: deviceID, id: deviceID,
name: entry.Name(), name: entry.Name(),
maxBrightness: maxBrightness, maxBrightness: maxBrightness,
minValue: minValue, minValue: minValue,
} })
log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness) log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness)
} }
@@ -106,19 +102,16 @@ func shouldSuppressDevice(name string) bool {
} }
func (b *SysfsBackend) GetDevices() ([]Device, error) { func (b *SysfsBackend) GetDevices() ([]Device, error) {
b.deviceCacheMutex.RLock() devices := make([]Device, 0)
defer b.deviceCacheMutex.RUnlock()
devices := make([]Device, 0, len(b.deviceCache)) b.deviceCache.Range(func(key string, dev *sysfsDevice) bool {
for _, dev := range b.deviceCache {
if shouldSuppressDevice(dev.name) { if shouldSuppressDevice(dev.name) {
continue return true
} }
parts := strings.SplitN(dev.id, ":", 2) parts := strings.SplitN(dev.id, ":", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue return true
} }
class := parts[0] class := parts[0]
@@ -130,13 +123,13 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
brightnessData, err := os.ReadFile(brightnessPath) brightnessData, err := os.ReadFile(brightnessPath)
if err != nil { if err != nil {
log.Debugf("failed to read brightness for %s: %v", dev.id, err) log.Debugf("failed to read brightness for %s: %v", dev.id, err)
continue return true
} }
current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData)))
if err != nil { if err != nil {
log.Debugf("failed to parse brightness for %s: %v", dev.id, err) log.Debugf("failed to parse brightness for %s: %v", dev.id, err)
continue return true
} }
percent := b.ValueToPercent(current, dev, false) percent := b.ValueToPercent(current, dev, false)
@@ -150,16 +143,14 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
CurrentPercent: percent, CurrentPercent: percent,
Backend: "sysfs", Backend: "sysfs",
}) })
} return true
})
return devices, nil return devices, nil
} }
func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) {
b.deviceCacheMutex.RLock() dev, ok := b.deviceCache.Load(id)
defer b.deviceCacheMutex.RUnlock()
dev, ok := b.deviceCache[id]
if !ok { if !ok {
return nil, fmt.Errorf("device not found: %s", id) return nil, fmt.Errorf("device not found: %s", id)
} }

View File

@@ -31,9 +31,8 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -41,13 +40,11 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -105,9 +102,8 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -115,13 +111,11 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -175,9 +169,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
} }
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -185,13 +178,11 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: nil, logindBackend: nil,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: false, logindReady: false,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -240,9 +231,8 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"leds"}, classes: []string{"leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -250,13 +240,11 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{

View File

@@ -160,26 +160,21 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
} }
b := &SysfsBackend{ b := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
t.Fatalf("scanDevices() error = %v", err) t.Fatalf("scanDevices() error = %v", err)
} }
if len(b.deviceCache) != 2 {
t.Errorf("expected 2 devices, got %d", len(b.deviceCache))
}
backlightID := "backlight:test_backlight" backlightID := "backlight:test_backlight"
if _, ok := b.deviceCache[backlightID]; !ok { if _, ok := b.deviceCache.Load(backlightID); !ok {
t.Errorf("backlight device not found") t.Errorf("backlight device not found")
} }
ledID := "leds:test_led" ledID := "leds:test_led"
if _, ok := b.deviceCache[ledID]; !ok { if _, ok := b.deviceCache.Load(ledID); !ok {
t.Errorf("LED device not found") t.Errorf("LED device not found")
} }
} }

View File

@@ -3,6 +3,8 @@ package brightness
import ( import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type DeviceClass string type DeviceClass string
@@ -51,9 +53,8 @@ type Manager struct {
stateMutex sync.RWMutex stateMutex sync.RWMutex
state State state State
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
updateSubscribers map[string]chan DeviceUpdate updateSubscribers syncmap.Map[string, chan DeviceUpdate]
subMutex sync.RWMutex
broadcastMutex sync.Mutex broadcastMutex sync.Mutex
broadcastTimer *time.Timer broadcastTimer *time.Timer
@@ -67,8 +68,7 @@ type SysfsBackend struct {
basePath string basePath string
classes []string classes []string
deviceCache map[string]*sysfsDevice deviceCache syncmap.Map[string, *sysfsDevice]
deviceCacheMutex sync.RWMutex
} }
type sysfsDevice struct { type sysfsDevice struct {
@@ -80,8 +80,7 @@ type sysfsDevice struct {
} }
type DDCBackend struct { type DDCBackend struct {
devices map[string]*ddcDevice devices syncmap.Map[string, *ddcDevice]
devicesMutex sync.RWMutex
scanMutex sync.Mutex scanMutex sync.Mutex
lastScan time.Time lastScan time.Time
@@ -121,36 +120,31 @@ type SetBrightnessParams struct {
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)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate { func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate {
ch := make(chan DeviceUpdate, 16) ch := make(chan DeviceUpdate, 16)
m.subMutex.Lock() m.updateSubscribers.Store(id, ch)
m.updateSubscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeUpdates(id string) { func (m *Manager) UnsubscribeUpdates(id string) {
m.subMutex.Lock() if val, ok := m.updateSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.updateSubscribers[id]; ok { close(val)
close(ch)
delete(m.updateSubscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
@@ -158,15 +152,13 @@ func (m *Manager) NotifySubscribers() {
state := m.state state := m.state
m.stateMutex.RUnlock() m.stateMutex.RUnlock()
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
defer m.subMutex.RUnlock()
for _, ch := range m.subscribers {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
@@ -178,16 +170,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
for _, ch := range m.updateSubscribers { })
m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
close(ch) close(ch)
} m.updateSubscribers.Delete(key)
m.updateSubscribers = make(map[string]chan DeviceUpdate) return true
m.subMutex.Unlock() })
if m.logindBackend != nil { if m.logindBackend != nil {
m.logindBackend.Close() m.logindBackend.Close()

View File

@@ -35,13 +35,11 @@ func NewManager() (*Manager, error) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: client, client: client,
baseURL: baseURL, baseURL: baseURL,
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
subMutex: sync.RWMutex{},
} }
if err := m.updateState(); err != nil { if err := m.updateState(); err != nil {
@@ -142,28 +140,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -199,10 +190,14 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subMutex.Lock()
wasEmpty := len(m.subscribers) == 0 wasEmpty := true
m.subscribers[id] = ch m.subscribers.Range(func(key string, ch chan CUPSState) bool {
m.subMutex.Unlock() wasEmpty = false
return false
})
m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if wasEmpty && m.subscription != nil {
if err := m.subscription.Start(); err != nil { if err := m.subscription.Start(); err != nil {
@@ -217,13 +212,15 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
isEmpty := len(m.subscribers) == 0
m.subMutex.Unlock() isEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
isEmpty = false
return false
})
if isEmpty && m.subscription != nil { if isEmpty && m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
@@ -241,12 +238,11 @@ func (m *Manager) Close() {
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan CUPSState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan CUPSState) return true
m.subMutex.Unlock() })
} }
func stateChanged(old, new *CUPSState) bool { func stateChanged(old, new *CUPSState) bool {

View File

@@ -13,10 +13,9 @@ func TestNewManager(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: nil, client: nil,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -35,10 +34,9 @@ func TestManager_GetState(t *testing.T) {
}, },
}, },
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
state := m.GetState() state := m.GetState()
@@ -53,18 +51,28 @@ func TestManager_Subscribe(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 1, len(m.subscribers))
count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client") m.Unsubscribe("test-client")
assert.Equal(t, 0, len(m.subscribers)) count = 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
@@ -74,10 +82,9 @@ func TestManager_Close(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
m.eventWG.Add(1) m.eventWG.Add(1)
@@ -93,7 +100,12 @@ func TestManager_Close(t *testing.T) {
}() }()
m.Close() m.Close()
assert.Equal(t, 0, len(m.subscribers)) count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestStateChanged(t *testing.T) { func TestStateChanged(t *testing.T) {

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type CUPSState struct { type CUPSState struct {
@@ -39,8 +40,7 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan CUPSState subscribers syncmap.Map[string, chan CUPSState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
eventWG sync.WaitGroup eventWG sync.WaitGroup
dirty chan struct{} dirty chan struct{}

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
@@ -13,13 +13,13 @@ import (
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
outputs: make(map[uint32]*outputState), ctx: display.Context(),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 128),
outputSetupReq: make(chan uint32, 16), outputSetupReq: make(chan uint32, 16),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
layouts: make([]string, 0), layouts: make([]string, 0),
} }
if err := m.setupRegistry(); err != nil { if err := m.setupRegistry(); err != nil {
@@ -55,10 +55,7 @@ func (m *Manager) waylandActor() {
case c := <-m.cmdq: case c := <-m.cmdq:
c.fn() c.fn()
case outputID := <-m.outputSetupReq: case outputID := <-m.outputSetupReq:
m.outputsMutex.RLock() out, exists := m.outputs.Load(outputID)
out, exists := m.outputs[outputID]
m.outputsMutex.RUnlock()
if !exists { if !exists {
log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID) log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
continue continue
@@ -86,7 +83,6 @@ func (m *Manager) waylandActor() {
func (m *Manager) setupRegistry() error { func (m *Manager) setupRegistry() error {
log.Info("DWL: starting registry setup") log.Info("DWL: starting registry setup")
ctx := m.display.Context()
registry, err := m.display.GetRegistry() registry, err := m.display.GetRegistry()
if err != nil { if err != nil {
@@ -102,7 +98,7 @@ func (m *Manager) setupRegistry() error {
switch e.Interface { switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName: case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName) log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName)
manager := dwl_ipc.NewZdwlIpcManagerV2(ctx) manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx)
version := e.Version version := e.Version
if version > 1 { if version > 1 {
version = 1 version = 1
@@ -128,7 +124,7 @@ func (m *Manager) setupRegistry() error {
} }
case "wl_output": case "wl_output":
log.Debugf("DWL: found wl_output (name=%d)", e.Name) log.Debugf("DWL: found wl_output (name=%d)", e.Name)
output := wlclient.NewOutput(ctx) output := wlclient.NewOutput(m.ctx)
outState := &outputState{ outState := &outputState{
registryName: e.Name, registryName: e.Name,
@@ -156,9 +152,7 @@ func (m *Manager) setupRegistry() error {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name outputRegNames[outputID] = e.Name
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[outputID] = outState
m.outputsMutex.Unlock()
if m.manager != nil { if m.manager != nil {
select { select {
@@ -176,17 +170,16 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock()
var outToRelease *outputState var outToRelease *outputState
for id, out := range m.outputs { m.outputs.Range(func(id uint32, out *outputState) bool {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("DWL: Output %d removed", id) log.Infof("DWL: Output %d removed", id)
outToRelease = out outToRelease = out
delete(m.outputs, id) m.outputs.Delete(id)
break return false
} }
} return true
m.outputsMutex.Unlock() })
if outToRelease != nil { if outToRelease != nil {
if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil { if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
@@ -236,14 +229,11 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
return fmt.Errorf("failed to get dwl output: %w", err) return fmt.Errorf("failed to get dwl output: %w", err)
} }
m.outputsMutex.Lock() outState, exists := m.outputs.Load(output.ID())
outState, exists := m.outputs[output.ID()]
if !exists { if !exists {
m.outputsMutex.Unlock()
return fmt.Errorf("output state not found for id %d", output.ID()) return fmt.Errorf("output state not found for id %d", output.ID())
} }
outState.ipcOutput = ipcOutput outState.ipcOutput = ipcOutput
m.outputsMutex.Unlock()
ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
outState.active = e.Active outState.active = e.Active
@@ -300,11 +290,10 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.outputsMutex.RLock()
outputs := make(map[string]*OutputState) outputs := make(map[string]*OutputState)
activeOutput := "" activeOutput := ""
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -326,8 +315,8 @@ func (m *Manager) updateState() {
if out.active != 0 { if out.active != 0 {
activeOutput = name activeOutput = name
} }
} return true
m.outputsMutex.RUnlock() })
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -365,14 +354,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -381,15 +362,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("DWL: subscriber channel full, dropping update") log.Warn("DWL: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -407,11 +387,9 @@ func (m *Manager) ensureOutputSetup(out *outputState) error {
} }
func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error { func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
m.outputsMutex.RLock() availableOutputs := make([]string, 0)
availableOutputs := make([]string, 0, len(m.outputs))
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -419,10 +397,10 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
availableOutputs = append(availableOutputs, name) availableOutputs = append(availableOutputs, name)
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs) return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
@@ -444,20 +422,18 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
} }
func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error { func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -479,20 +455,18 @@ func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint3
} }
func (m *Manager) SetLayout(outputName string, index uint32) error { func (m *Manager) SetLayout(outputName string, index uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -518,21 +492,19 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok { if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
ipcOut.Release() ipcOut.Release()
} }
} m.outputs.Delete(key)
m.outputs = make(map[uint32]*outputState) return true
m.outputsMutex.Unlock() })
if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok { if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
mgr.Release() mgr.Release()

View File

@@ -3,7 +3,8 @@ package dwl
import ( import (
"sync" "sync"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type TagState struct { type TagState struct {
@@ -36,11 +37,11 @@ type cmd struct {
type Manager struct { type Manager struct {
display *wlclient.Display display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry registry *wlclient.Registry
manager interface{} manager interface{}
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
tagCount uint32 tagCount uint32
layouts []string layouts []string
@@ -51,8 +52,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -91,19 +91,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -0,0 +1,27 @@
package evdev
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type Request struct {
ID interface{} `json:"id"`
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, m *Manager) {
switch req.Method {
case "evdev.getState":
handleGetState(conn, req, m)
default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method)
}
}
func handleGetState(conn net.Conn, req Request, m *Manager) {
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}

View File

@@ -0,0 +1,130 @@
package evdev
import (
"bytes"
"encoding/json"
"errors"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type mockNetConn struct {
net.Conn
readBuf *bytes.Buffer
writeBuf *bytes.Buffer
closed bool
}
func newMockNetConn() *mockNetConn {
return &mockNetConn{
readBuf: &bytes.Buffer{},
writeBuf: &bytes.Buffer{},
}
}
func (m *mockNetConn) Read(b []byte) (n int, err error) {
return m.readBuf.Read(b)
}
func (m *mockNetConn) Write(b []byte) (n int, err error) {
return m.writeBuf.Write(b)
}
func (m *mockNetConn) Close() error {
m.closed = true
return nil
}
func TestHandleRequest(t *testing.T) {
t.Run("getState request", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true},
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
req := Request{
ID: 123,
Method: "evdev.getState",
Params: map[string]interface{}{},
}
HandleRequest(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Available)
assert.True(t, resp.Result.CapsLock)
})
t.Run("unknown method", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
req := Request{
ID: 456,
Method: "evdev.unknownMethod",
Params: map[string]interface{}{},
}
HandleRequest(conn, req, m)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 456, resp.ID)
assert.NotEmpty(t, resp.Error)
assert.Contains(t, resp.Error, "unknown method")
})
}
func TestHandleGetState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
req := Request{
ID: 789,
Method: "evdev.getState",
Params: map[string]interface{}{},
}
handleGetState(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 789, resp.ID)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Available)
assert.False(t, resp.Result.CapsLock)
}

View File

@@ -0,0 +1,404 @@
package evdev
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev"
)
const (
evKeyType = 0x01
evLedType = 0x11
keyCapslockKey = 58
ledCapslockKey = 1
keyStateOn = 1
)
type EvdevDevice interface {
Name() (string, error)
Path() string
Close() error
ReadOne() (*evdev.InputEvent, error)
State(t evdev.EvType) (evdev.StateMap, error)
}
type Manager struct {
devices []EvdevDevice
devicesMutex sync.RWMutex
monitoredPaths map[string]bool
state State
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
closeChan chan struct{}
closeOnce sync.Once
watcher *fsnotify.Watcher
}
func NewManager() (*Manager, error) {
devices, err := findKeyboards()
if err != nil {
return nil, fmt.Errorf("failed to find keyboards: %w", err)
}
initialCapsLock := readInitialCapsLockState(devices[0])
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Warnf("Failed to create fsnotify watcher, hotplug detection disabled: %v", err)
watcher = nil
} else if err := watcher.Add("/dev/input"); err != nil {
log.Warnf("Failed to watch /dev/input, hotplug detection disabled: %v", err)
watcher.Close()
watcher = nil
}
monitoredPaths := make(map[string]bool)
for _, device := range devices {
monitoredPaths[device.Path()] = true
}
m := &Manager{
devices: devices,
monitoredPaths: monitoredPaths,
state: State{Available: true, CapsLock: initialCapsLock},
closeChan: make(chan struct{}),
watcher: watcher,
}
for i, device := range devices {
go m.monitorDevice(device, i)
}
if watcher != nil {
go m.watchForNewKeyboards()
}
return m, nil
}
func readInitialCapsLockState(device EvdevDevice) bool {
ledStates, err := device.State(evLedType)
if err != nil {
log.Debugf("Could not read LED state: %v", err)
return false
}
return ledStates[ledCapslockKey]
}
func findKeyboards() ([]EvdevDevice, error) {
pattern := "/dev/input/event*"
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("failed to glob input devices: %w", err)
}
if len(matches) == 0 {
return nil, fmt.Errorf("no input devices found")
}
var keyboards []EvdevDevice
for _, path := range matches {
device, err := evdev.Open(path)
if err != nil {
continue
}
if !isKeyboard(device) {
device.Close()
continue
}
deviceName, _ := device.Name()
log.Debugf("Found keyboard: %s at %s", deviceName, path)
keyboards = append(keyboards, device)
}
if len(keyboards) == 0 {
return nil, fmt.Errorf("no keyboard device found")
}
return keyboards, nil
}
func isKeyboard(device EvdevDevice) bool {
deviceName, err := device.Name()
if err != nil {
return false
}
name := strings.ToLower(deviceName)
switch {
case strings.Contains(name, "keyboard"):
return true
case strings.Contains(name, "kbd"):
return true
case strings.Contains(name, "input") && strings.Contains(name, "key"):
return true
}
keyStates, err := device.State(evKeyType)
if err != nil {
return false
}
hasKeyA := len(keyStates) > 30
hasKeyZ := len(keyStates) > 44
hasEnter := len(keyStates) > 28
return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100
}
func (m *Manager) watchForNewKeyboards() {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic in keyboard hotplug monitor: %v", r)
}
}()
for {
select {
case <-m.closeChan:
return
case event, ok := <-m.watcher.Events:
if !ok {
return
}
if !strings.HasPrefix(filepath.Base(event.Name), "event") {
continue
}
if event.Op&fsnotify.Create == fsnotify.Create {
time.Sleep(100 * time.Millisecond)
m.devicesMutex.Lock()
if m.monitoredPaths[event.Name] {
m.devicesMutex.Unlock()
continue
}
device, err := evdev.Open(event.Name)
if err != nil {
m.devicesMutex.Unlock()
continue
}
if !isKeyboard(device) {
device.Close()
m.devicesMutex.Unlock()
continue
}
deviceName, _ := device.Name()
log.Debugf("Hotplugged keyboard: %s at %s", deviceName, event.Name)
m.devices = append(m.devices, device)
m.monitoredPaths[event.Name] = true
deviceIndex := len(m.devices) - 1
m.devicesMutex.Unlock()
go m.monitorDevice(device, deviceIndex)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
m.devicesMutex.Lock()
if !m.monitoredPaths[event.Name] {
m.devicesMutex.Unlock()
continue
}
delete(m.monitoredPaths, event.Name)
for i, device := range m.devices {
if device != nil && device.Path() == event.Name {
log.Debugf("Keyboard removed: %s", event.Name)
device.Close()
m.devices[i] = nil
break
}
}
m.devicesMutex.Unlock()
}
case err, ok := <-m.watcher.Errors:
if !ok {
return
}
log.Warnf("Keyboard hotplug watcher error: %v", err)
}
}
}
func (m *Manager) monitorDevice(device EvdevDevice, deviceIndex int) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic in evdev monitor: %v", r)
}
}()
for {
select {
case <-m.closeChan:
return
default:
}
event, err := device.ReadOne()
if err != nil {
if isClosedError(err) {
return
}
log.Warnf("Failed to read evdev event: %v", err)
time.Sleep(100 * time.Millisecond)
continue
}
if event == nil {
continue
}
if event.Type == evKeyType && event.Code == keyCapslockKey && event.Value == keyStateOn {
time.Sleep(50 * time.Millisecond)
m.readAndUpdateCapsLockState(deviceIndex)
} else if event.Type == evLedType && event.Code == ledCapslockKey {
capsLockState := event.Value == keyStateOn
m.updateCapsLockStateDirect(capsLockState)
}
}
}
func isClosedError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
switch {
case strings.Contains(errStr, "closed"):
return true
case strings.Contains(errStr, "bad file descriptor"):
return true
default:
return false
}
}
func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
m.devicesMutex.RLock()
if deviceIndex >= len(m.devices) {
m.devicesMutex.RUnlock()
return
}
device := m.devices[deviceIndex]
m.devicesMutex.RUnlock()
ledStates, err := device.State(evLedType)
if err != nil {
log.Warnf("Failed to read LED state: %v", err)
return
}
capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState)
}
func (m *Manager) updateCapsLockStateDirect(capsLockState bool) {
m.stateMutex.Lock()
if m.state.CapsLock == capsLockState {
m.stateMutex.Unlock()
return
}
m.state.CapsLock = capsLockState
newState := m.state
m.stateMutex.Unlock()
log.Debugf("Caps lock state: %v", newState.CapsLock)
m.notifySubscribers(newState)
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
return m.state
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) notifySubscribers(state State) {
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- state:
default:
}
return true
})
}
func (m *Manager) Close() {
m.closeOnce.Do(func() {
close(m.closeChan)
if m.watcher != nil {
m.watcher.Close()
}
m.devicesMutex.Lock()
for _, device := range m.devices {
if device == nil {
continue
}
if err := device.Close(); err != nil && !isClosedError(err) {
log.Warnf("Error closing evdev device: %v", err)
}
}
m.devicesMutex.Unlock()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
})
}
func InitializeManager() (*Manager, error) {
if os.Getuid() != 0 && !hasInputGroupAccess() {
return nil, fmt.Errorf("insufficient permissions to access input devices")
}
return NewManager()
}
func hasInputGroupAccess() bool {
pattern := "/dev/input/event*"
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
return false
}
testFile, err := os.Open(matches[0])
if err != nil {
return false
}
testFile.Close()
return true
}

View File

@@ -0,0 +1,344 @@
package evdev
import (
"errors"
"testing"
evdev "github.com/holoplot/go-evdev"
"github.com/stretchr/testify/assert"
mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev"
)
func TestManager_Creation(t *testing.T) {
t.Run("manager created successfully with caps lock off", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
assert.NotNil(t, m)
assert.True(t, m.state.Available)
assert.False(t, m.state.CapsLock)
})
t.Run("manager created successfully with caps lock on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true},
closeChan: make(chan struct{}),
}
assert.NotNil(t, m)
assert.True(t, m.state.Available)
assert.True(t, m.state.CapsLock)
})
}
func TestManager_GetState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
state := m.GetState()
assert.True(t, state.Available)
assert.False(t, state.CapsLock)
}
func TestManager_Subscribe(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
assert.NotNil(t, ch)
count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
}
func TestManager_Unsubscribe(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client")
count = 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
select {
case _, ok := <-ch:
assert.False(t, ok, "channel should be closed")
default:
t.Error("channel should be closed")
}
}
func TestManager_UpdateCapsLock(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
ledStateOn := evdev.StateMap{ledCapslockKey: true}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOn, nil).Once()
go func() {
m.readAndUpdateCapsLockState(0)
}()
newState := <-ch
assert.True(t, newState.CapsLock)
ledStateOff := evdev.StateMap{ledCapslockKey: false}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOff, nil).Once()
go func() {
m.readAndUpdateCapsLockState(0)
}()
newState = <-ch
assert.False(t, newState.CapsLock)
}
func TestManager_Close(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Close().Return(nil).Once()
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch1 := m.Subscribe("client1")
ch2 := m.Subscribe("client2")
m.Close()
select {
case _, ok := <-ch1:
assert.False(t, ok, "channel 1 should be closed")
default:
t.Error("channel 1 should be closed")
}
select {
case _, ok := <-ch2:
assert.False(t, ok, "channel 2 should be closed")
default:
t.Error("channel 2 should be closed")
}
count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
m.Close()
}
func TestIsKeyboard(t *testing.T) {
tests := []struct {
name string
devName string
expected bool
}{
{"keyboard in name", "AT Translated Set 2 keyboard", true},
{"kbd in name", "USB kbd", true},
{"input and key", "input key device", true},
{"random device", "Mouse", false},
{"empty name", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Name().Return(tt.devName, nil).Once()
if !tt.expected {
mockDevice.EXPECT().State(evdev.EvType(evKeyType)).Return(evdev.StateMap{}, nil).Maybe()
}
result := isKeyboard(mockDevice)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsKeyboard_ErrorHandling(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Name().Return("", errors.New("device error")).Once()
result := isKeyboard(mockDevice)
assert.False(t, result)
}
func TestManager_MonitorDevice(t *testing.T) {
t.Run("caps lock key press updates state", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
capsLockEvent := &evdev.InputEvent{
Type: evKeyType,
Code: keyCapslockKey,
Value: keyStateOn,
}
ledState := evdev.StateMap{ledCapslockKey: true}
mockDevice.EXPECT().ReadOne().Return(capsLockEvent, nil).Once()
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("stop")).Maybe()
mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test")
go m.monitorDevice(mockDevice, 0)
state := <-ch
assert.True(t, state.CapsLock)
m.Close()
})
}
func TestIsClosedError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{"nil error", nil, false},
{"closed error", errors.New("device closed"), true},
{"bad file descriptor", errors.New("bad file descriptor"), true},
{"other error", errors.New("some other error"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isClosedError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestNotifySubscribers(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch1 := m.Subscribe("client1")
ch2 := m.Subscribe("client2")
newState := State{Available: true, CapsLock: true}
go m.notifySubscribers(newState)
state1 := <-ch1
state2 := <-ch2
assert.Equal(t, newState, state1)
assert.Equal(t, newState, state2)
m.Close()
}
func TestReadInitialCapsLockState(t *testing.T) {
t.Run("caps lock is on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: true,
}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice)
assert.True(t, result)
})
t.Run("caps lock is off", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: false,
}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice)
assert.False(t, result)
})
t.Run("error reading LED state", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
result := readInitialCapsLockState(mockDevice)
assert.False(t, result)
})
}
func TestHasInputGroupAccess(t *testing.T) {
result := hasInputGroupAccess()
t.Logf("hasInputGroupAccess: %v", result)
}

View File

@@ -0,0 +1,6 @@
package evdev
type State struct {
Available bool `json:"available"`
CapsLock bool `json:"capsLock"`
}

View File

@@ -6,20 +6,17 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
outputs: make(map[uint32]*wlclient.Output), ctx: display.Context(),
outputNames: make(map[uint32]string), cmdq: make(chan cmd, 128),
groups: make(map[uint32]*workspaceGroupState), stopChan: make(chan struct{}),
workspaces: make(map[uint32]*workspaceState),
cmdq: make(chan cmd, 128), dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1),
} }
m.wg.Add(1) m.wg.Add(1)
@@ -62,7 +59,6 @@ func (m *Manager) waylandActor() {
func (m *Manager) setupRegistry() error { func (m *Manager) setupRegistry() error {
log.Info("ExtWorkspace: starting registry setup") log.Info("ExtWorkspace: starting registry setup")
ctx := m.display.Context()
registry, err := m.display.GetRegistry() registry, err := m.display.GetRegistry()
if err != nil { if err != nil {
@@ -72,14 +68,12 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == "wl_output" { if e.Interface == "wl_output" {
output := wlclient.NewOutput(ctx) output := wlclient.NewOutput(m.ctx)
if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil { if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil {
outputID := output.ID() outputID := output.ID()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
m.outputsMutex.Lock() m.outputNames.Store(outputID, ev.Name)
m.outputNames[outputID] = ev.Name
m.outputsMutex.Unlock()
log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name) log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name)
}) })
} }
@@ -88,7 +82,7 @@ func (m *Manager) setupRegistry() error {
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName { if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName) log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName)
manager := ext_workspace.NewExtWorkspaceManagerV1(ctx) manager := ext_workspace.NewExtWorkspaceManagerV1(m.ctx)
version := e.Version version := e.Version
if version > 1 { if version > 1 {
version = 1 version = 1
@@ -139,9 +133,7 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceIDs: make([]uint32, 0), workspaceIDs: make([]uint32, 0),
} }
m.groupsMutex.Lock() m.groups.Store(groupID, group)
m.groups[groupID] = group
m.groupsMutex.Unlock()
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) { handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) {
log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities) log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities)
@@ -151,9 +143,8 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
outputID := e.Output.ID() outputID := e.Output.ID()
log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID) log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID)
group.outputIDs[outputID] = true
m.post(func() { m.post(func() {
group.outputIDs[outputID] = true
m.updateState() m.updateState()
}) })
}) })
@@ -161,8 +152,8 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) { handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) {
outputID := e.Output.ID() outputID := e.Output.ID()
log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID) log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID)
delete(group.outputIDs, outputID)
m.post(func() { m.post(func() {
delete(group.outputIDs, outputID)
m.updateState() m.updateState()
}) })
}) })
@@ -171,14 +162,12 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceID := e.Workspace.ID() workspaceID := e.Workspace.ID()
log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID)
m.workspacesMutex.Lock()
if ws, exists := m.workspaces[workspaceID]; exists {
ws.groupID = groupID
}
m.workspacesMutex.Unlock()
group.workspaceIDs = append(group.workspaceIDs, workspaceID)
m.post(func() { m.post(func() {
if ws, ok := m.workspaces.Load(workspaceID); ok {
ws.groupID = groupID
}
group.workspaceIDs = append(group.workspaceIDs, workspaceID)
m.updateState() m.updateState()
}) })
}) })
@@ -187,32 +176,29 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceID := e.Workspace.ID() workspaceID := e.Workspace.ID()
log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID)
m.workspacesMutex.Lock()
if ws, exists := m.workspaces[workspaceID]; exists {
ws.groupID = 0
}
m.workspacesMutex.Unlock()
for i, id := range group.workspaceIDs {
if id == workspaceID {
group.workspaceIDs = append(group.workspaceIDs[:i], group.workspaceIDs[i+1:]...)
break
}
}
m.post(func() { m.post(func() {
if ws, ok := m.workspaces.Load(workspaceID); ok {
ws.groupID = 0
}
for i, id := range group.workspaceIDs {
if id == workspaceID {
group.workspaceIDs = append(group.workspaceIDs[:i], group.workspaceIDs[i+1:]...)
break
}
}
m.updateState() m.updateState()
}) })
}) })
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) { handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) {
log.Debugf("ExtWorkspace: Group %d removed", groupID) log.Debugf("ExtWorkspace: Group %d removed", groupID)
group.removed = true
m.groupsMutex.Lock()
delete(m.groups, groupID)
m.groupsMutex.Unlock()
m.post(func() { m.post(func() {
group.removed = true
m.groups.Delete(groupID)
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
m.wlMutex.Unlock() m.wlMutex.Unlock()
@@ -234,22 +220,20 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
coordinates: make([]uint32, 0), coordinates: make([]uint32, 0),
} }
m.workspacesMutex.Lock() m.workspaces.Store(workspaceID, ws)
m.workspaces[workspaceID] = ws
m.workspacesMutex.Unlock()
handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) { handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) {
log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id) log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id)
ws.workspaceID = e.Id
m.post(func() { m.post(func() {
ws.workspaceID = e.Id
m.updateState() m.updateState()
}) })
}) })
handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) { handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) {
log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name) log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name)
ws.name = e.Name
m.post(func() { m.post(func() {
ws.name = e.Name
m.updateState() m.updateState()
}) })
}) })
@@ -266,16 +250,16 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
} }
} }
log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords) log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords)
ws.coordinates = coords
m.post(func() { m.post(func() {
ws.coordinates = coords
m.updateState() m.updateState()
}) })
}) })
handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) { handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) {
log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State) log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State)
ws.state = e.State
m.post(func() { m.post(func() {
ws.state = e.State
m.updateState() m.updateState()
}) })
}) })
@@ -286,13 +270,12 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) { handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) {
log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID) log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID)
ws.removed = true
m.workspacesMutex.Lock()
delete(m.workspaces, workspaceID)
m.workspacesMutex.Unlock()
m.post(func() { m.post(func() {
ws.removed = true
m.workspaces.Delete(workspaceID)
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
m.wlMutex.Unlock() m.wlMutex.Unlock()
@@ -303,23 +286,21 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.groupsMutex.RLock()
m.workspacesMutex.RLock()
groups := make([]*WorkspaceGroup, 0) groups := make([]*WorkspaceGroup, 0)
for _, group := range m.groups { m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if group.removed { if group.removed {
continue return true
} }
outputs := make([]string, 0) outputs := make([]string, 0)
for outputID := range group.outputIDs { for outputID := range group.outputIDs {
m.outputsMutex.RLock() if name, ok := m.outputNames.Load(outputID); ok {
name := m.outputNames[outputID] if name != "" {
m.outputsMutex.RUnlock() outputs = append(outputs, name)
if name != "" { } else {
outputs = append(outputs, name) outputs = append(outputs, fmt.Sprintf("output-%d", outputID))
}
} else { } else {
outputs = append(outputs, fmt.Sprintf("output-%d", outputID)) outputs = append(outputs, fmt.Sprintf("output-%d", outputID))
} }
@@ -327,8 +308,11 @@ func (m *Manager) updateState() {
workspaces := make([]*Workspace, 0) workspaces := make([]*Workspace, 0)
for _, wsID := range group.workspaceIDs { for _, wsID := range group.workspaceIDs {
ws, exists := m.workspaces[wsID] ws, exists := m.workspaces.Load(wsID)
if !exists || ws.removed { if !exists {
continue
}
if ws.removed {
continue continue
} }
@@ -350,10 +334,8 @@ func (m *Manager) updateState() {
Workspaces: workspaces, Workspaces: workspaces,
} }
groups = append(groups, groupState) groups = append(groups, groupState)
} return true
})
m.workspacesMutex.RUnlock()
m.groupsMutex.RUnlock()
newState := State{ newState := State{
Groups: groups, Groups: groups,
@@ -388,14 +370,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -404,15 +378,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("ExtWorkspace: subscriber channel full, dropping update") log.Warn("ExtWorkspace: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -422,112 +395,148 @@ func (m *Manager) notifier() {
} }
func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error { func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
m.workspacesMutex.RLock() errChan := make(chan error, 1)
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 m.post(func() {
if groupID != "" { var targetGroupID uint32
var parsedID uint32 if groupID != "" {
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil { var parsedID uint32
targetGroupID = parsedID if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
} targetGroupID = parsedID
}
for _, ws := range m.workspaces {
if targetGroupID != 0 && ws.groupID != targetGroupID {
continue
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Activate()
if err == nil {
err = m.manager.Commit()
} }
m.wlMutex.Unlock()
return err
} }
}
return fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID {
return true
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Activate()
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
} }
func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error { func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
m.workspacesMutex.RLock() errChan := make(chan error, 1)
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 m.post(func() {
if groupID != "" { var targetGroupID uint32
var parsedID uint32 if groupID != "" {
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil { var parsedID uint32
targetGroupID = parsedID if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
} targetGroupID = parsedID
}
for _, ws := range m.workspaces {
if targetGroupID != 0 && ws.groupID != targetGroupID {
continue
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Deactivate()
if err == nil {
err = m.manager.Commit()
} }
m.wlMutex.Unlock()
return err
} }
}
return fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID {
return true
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Deactivate()
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
} }
func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error { func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
m.workspacesMutex.RLock() errChan := make(chan error, 1)
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 m.post(func() {
if groupID != "" { var targetGroupID uint32
var parsedID uint32 if groupID != "" {
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil { var parsedID uint32
targetGroupID = parsedID if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
} targetGroupID = parsedID
}
for _, ws := range m.workspaces {
if targetGroupID != 0 && ws.groupID != targetGroupID {
continue
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Remove()
if err == nil {
err = m.manager.Commit()
} }
m.wlMutex.Unlock()
return err
} }
}
return fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID {
return true
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Remove()
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
} }
func (m *Manager) CreateWorkspace(groupID, workspaceName string) error { func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
m.groupsMutex.RLock() errChan := make(chan error, 1)
defer m.groupsMutex.RUnlock()
for _, group := range m.groups { m.post(func() {
if fmt.Sprintf("group-%d", group.id) == groupID { var found bool
m.wlMutex.Lock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
err := group.handle.CreateWorkspace(workspaceName) if fmt.Sprintf("group-%d", group.id) == groupID {
if err == nil { m.wlMutex.Lock()
err = m.manager.Commit() err := group.handle.CreateWorkspace(workspaceName)
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
} }
m.wlMutex.Unlock() return true
return err })
}
}
return fmt.Errorf("workspace group not found: %s", groupID) if !found {
errChan <- fmt.Errorf("workspace group not found: %s", groupID)
}
})
return <-errChan
} }
func (m *Manager) Close() { func (m *Manager) Close() {
@@ -535,30 +544,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.workspacesMutex.Lock() m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
for _, ws := range m.workspaces {
if ws.handle != nil { if ws.handle != nil {
ws.handle.Destroy() ws.handle.Destroy()
} }
} m.workspaces.Delete(key)
m.workspaces = make(map[uint32]*workspaceState) return true
m.workspacesMutex.Unlock() })
m.groupsMutex.Lock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
for _, group := range m.groups {
if group.handle != nil { if group.handle != nil {
group.handle.Destroy() group.handle.Destroy()
} }
} m.groups.Delete(key)
m.groups = make(map[uint32]*workspaceGroupState) return true
m.groupsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()

View File

@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type Workspace struct { type Workspace struct {
@@ -33,26 +34,22 @@ type cmd struct {
type Manager struct { type Manager struct {
display *wlclient.Display display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry registry *wlclient.Registry
manager *ext_workspace.ExtWorkspaceManagerV1 manager *ext_workspace.ExtWorkspaceManagerV1
outputsMutex sync.RWMutex outputNames syncmap.Map[uint32, string]
outputs map[uint32]*wlclient.Output
outputNames map[uint32]string
groupsMutex sync.RWMutex groups syncmap.Map[uint32, *workspaceGroupState]
groups map[uint32]*workspaceGroupState
workspacesMutex sync.RWMutex workspaces syncmap.Map[uint32, *workspaceState]
workspaces map[uint32]*workspaceState
wlMutex sync.Mutex wlMutex sync.Mutex
cmdq chan cmd cmdq chan cmd
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -94,19 +91,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -29,8 +29,6 @@ func NewManager() (*Manager, error) {
systemConn: systemConn, systemConn: systemConn,
sessionConn: sessionConn, sessionConn: sessionConn,
currentUID: uint64(os.Getuid()), currentUID: uint64(os.Getuid()),
subscribers: make(map[string]chan FreedeskState),
subMutex: sync.RWMutex{},
} }
m.initializeAccounts() m.initializeAccounts()
@@ -206,41 +204,33 @@ func (m *Manager) GetState() FreedeskState {
func (m *Manager) Subscribe(id string) chan FreedeskState { func (m *Manager) Subscribe(id string) chan FreedeskState {
ch := make(chan FreedeskState, 64) ch := make(chan FreedeskState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
m.subMutex.RLock()
defer m.subMutex.RUnlock()
state := m.GetState() state := m.GetState()
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) Close() { func (m *Manager) Close() {
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
for id, ch := range m.subscribers {
close(ch) close(ch)
delete(m.subscribers, id) m.subscribers.Delete(key)
} return true
m.subMutex.Unlock() })
if m.systemConn != nil { if m.systemConn != nil {
m.systemConn.Close() m.systemConn.Close()

View File

@@ -3,6 +3,7 @@ package freedesktop
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -41,6 +42,5 @@ type Manager struct {
accountsObj dbus.BusObject accountsObj dbus.BusObject
settingsObj dbus.BusObject settingsObj dbus.BusObject
currentUID uint64 currentUID uint64
subscribers map[string]chan FreedeskState subscribers syncmap.Map[string, chan FreedeskState]
subMutex sync.RWMutex
} }

View File

@@ -466,9 +466,7 @@ func TestHandleSubscribe(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
conn := newMockNetConn() conn := newMockNetConn()

View File

@@ -25,13 +25,12 @@ func NewManager() (*Manager, error) {
state: &SessionState{ state: &SessionState{
SessionID: sessionID, SessionID: sessionID,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), conn: conn,
conn: conn, dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1), signals: make(chan *dbus.Signal, 256),
signals: make(chan *dbus.Signal, 256),
} }
m.sleepInhibitorEnabled.Store(true) m.sleepInhibitorEnabled.Store(true)
@@ -351,19 +350,14 @@ func (m *Manager) GetState() SessionState {
func (m *Manager) Subscribe(id string) chan SessionState { func (m *Manager) Subscribe(id string) chan SessionState {
ch := make(chan SessionState, 64) ch := make(chan SessionState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifier() { func (m *Manager) notifier() {
@@ -387,28 +381,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan SessionState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -584,12 +571,11 @@ func (m *Manager) Close() {
m.releaseSleepInhibitor() m.releaseSleepInhibitor()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan SessionState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan SessionState) return true
m.subMutex.Unlock() })
if m.conn != nil { if m.conn != nil {
m.conn.Close() m.conn.Close()

View File

@@ -34,26 +34,20 @@ func TestManager_GetState(t *testing.T) {
func TestManager_Subscribe(t *testing.T) { func TestManager_Subscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 64, cap(ch)) assert.Equal(t, 64, cap(ch))
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.True(t, exists) assert.True(t, exists)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
@@ -63,17 +57,13 @@ func TestManager_Unsubscribe(t *testing.T) {
_, ok := <-ch _, ok := <-ch
assert.False(t, ok) assert.False(t, ok)
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.False(t, exists) assert.False(t, exists)
} }
func TestManager_Unsubscribe_NonExistent(t *testing.T) { func TestManager_Unsubscribe_NonExistent(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
// Unsubscribe a non-existent client should not panic // Unsubscribe a non-existent client should not panic
@@ -88,19 +78,15 @@ func TestManager_NotifySubscribers(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan SessionState, 10) ch := make(chan SessionState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
@@ -122,19 +108,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan SessionState, 10) ch := make(chan SessionState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
manager.notifySubscribers() manager.notifySubscribers()
@@ -157,19 +139,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
} }
ch1 := make(chan SessionState, 1) ch1 := make(chan SessionState, 1)
ch2 := make(chan SessionState, 1) ch2 := make(chan SessionState, 1)
manager.subMutex.Lock() manager.subscribers.Store("client1", ch1)
manager.subscribers["client1"] = ch1 manager.subscribers.Store("client2", ch2)
manager.subscribers["client2"] = ch2
manager.subMutex.Unlock()
manager.Close() manager.Close()
@@ -184,7 +162,12 @@ func TestManager_Close(t *testing.T) {
assert.False(t, ok1, "ch1 should be closed") assert.False(t, ok1, "ch1 should be closed")
assert.False(t, ok2, "ch2 should be closed") assert.False(t, ok2, "ch2 should be closed")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan SessionState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_GetState_ThreadSafe(t *testing.T) { func TestManager_GetState_ThreadSafe(t *testing.T) {

View File

@@ -14,10 +14,8 @@ func TestManager_HandleDBusSignal_Lock(t *testing.T) {
Locked: false, Locked: false,
LockedHint: false, LockedHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -38,10 +36,8 @@ func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
Locked: true, Locked: true,
LockedHint: true, LockedHint: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -62,10 +58,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: false, PreparingForSleep: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -85,10 +79,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: true, PreparingForSleep: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -108,10 +100,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: false, PreparingForSleep: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -133,10 +123,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
Active: false, Active: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -161,10 +149,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
IdleHint: false, IdleHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -189,10 +175,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
IdleSinceHint: 0, IdleSinceHint: 0,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -218,10 +202,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
LockedHint: false, LockedHint: false,
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -247,10 +229,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
Active: false, Active: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -272,11 +252,9 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
t.Run("empty body", func(t *testing.T) { t.Run("empty body", func(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -295,10 +273,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
Active: false, Active: false,
IdleHint: false, IdleHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{

View File

@@ -6,6 +6,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -50,8 +51,7 @@ type SessionEvent struct {
type Manager struct { type Manager struct {
state *SessionState state *SessionState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan SessionState subscribers syncmap.Map[string, chan SessionState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
conn *dbus.Conn conn *dbus.Conn
sessionPath dbus.ObjectPath sessionPath dbus.ObjectPath

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
The network manager API provides methods for managing WiFi connections, monitoring network state, and handling credential prompts through NetworkManager. Communication occurs over a message-based protocol (websocket, IPC, etc.) with event subscriptions for state updates. The network manager API provides methods for managing WiFi connections, monitoring network state, and handling credential prompts through NetworkManager or iwd (and systemd-networkd for ethernet only). Communication occurs over a message-based protocol (websocket, IPC, etc.) with event subscriptions for state updates.
## API Methods ## API Methods

View File

@@ -3,14 +3,15 @@ package network
import ( import (
"testing" "testing"
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNetworkManagerBackend_GetWiredConnections_NoDevice(t *testing.T) { func TestNetworkManagerBackend_GetWiredConnections_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.ethernetDevice = nil backend.ethernetDevice = nil
_, err = backend.GetWiredConnections() _, err = backend.GetWiredConnections()
@@ -19,10 +20,10 @@ func TestNetworkManagerBackend_GetWiredConnections_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_GetWiredNetworkDetails_NoDevice(t *testing.T) { func TestNetworkManagerBackend_GetWiredNetworkDetails_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.ethernetDevice = nil backend.ethernetDevice = nil
_, err = backend.GetWiredNetworkDetails("test-uuid") _, err = backend.GetWiredNetworkDetails("test-uuid")
@@ -31,10 +32,10 @@ func TestNetworkManagerBackend_GetWiredNetworkDetails_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_ConnectEthernet_NoDevice(t *testing.T) { func TestNetworkManagerBackend_ConnectEthernet_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.ethernetDevice = nil backend.ethernetDevice = nil
err = backend.ConnectEthernet() err = backend.ConnectEthernet()
@@ -43,10 +44,10 @@ func TestNetworkManagerBackend_ConnectEthernet_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_DisconnectEthernet_NoDevice(t *testing.T) { func TestNetworkManagerBackend_DisconnectEthernet_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.ethernetDevice = nil backend.ethernetDevice = nil
err = backend.DisconnectEthernet() err = backend.DisconnectEthernet()
@@ -55,10 +56,10 @@ func TestNetworkManagerBackend_DisconnectEthernet_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_ActivateWiredConnection_NoDevice(t *testing.T) { func TestNetworkManagerBackend_ActivateWiredConnection_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.ethernetDevice = nil backend.ethernetDevice = nil
err = backend.ActivateWiredConnection("test-uuid") err = backend.ActivateWiredConnection("test-uuid")
@@ -67,25 +68,14 @@ func TestNetworkManagerBackend_ActivateWiredConnection_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_ActivateWiredConnection_NotFound(t *testing.T) { func TestNetworkManagerBackend_ActivateWiredConnection_NotFound(t *testing.T) {
backend, err := NewNetworkManagerBackend() t.Skip("ActivateWiredConnection creates a new Settings instance internally, cannot be fully mocked")
if err != nil {
t.Skipf("NetworkManager not available: %v", err)
}
if backend.ethernetDevice == nil {
t.Skip("No ethernet device available")
}
err = backend.ActivateWiredConnection("non-existent-uuid-12345")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
} }
func TestNetworkManagerBackend_ListEthernetConnections_NoDevice(t *testing.T) { func TestNetworkManagerBackend_ListEthernetConnections_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.ethernetDevice = nil backend.ethernetDevice = nil
_, err = backend.listEthernetConnections() _, err = backend.listEthernetConnections()

View File

@@ -3,15 +3,17 @@ package network
import ( import (
"testing" "testing"
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
"github.com/Wifx/gonetworkmanager/v2"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNetworkManagerBackend_HandleDBusSignal_NewConnection(t *testing.T) { func TestNetworkManagerBackend_HandleDBusSignal_NewConnection(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
sig := &dbus.Signal{ sig := &dbus.Signal{
Name: "org.freedesktop.NetworkManager.Settings.NewConnection", Name: "org.freedesktop.NetworkManager.Settings.NewConnection",
@@ -24,10 +26,10 @@ func TestNetworkManagerBackend_HandleDBusSignal_NewConnection(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleDBusSignal_ConnectionRemoved(t *testing.T) { func TestNetworkManagerBackend_HandleDBusSignal_ConnectionRemoved(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
sig := &dbus.Signal{ sig := &dbus.Signal{
Name: "org.freedesktop.NetworkManager.Settings.ConnectionRemoved", Name: "org.freedesktop.NetworkManager.Settings.ConnectionRemoved",
@@ -40,10 +42,10 @@ func TestNetworkManagerBackend_HandleDBusSignal_ConnectionRemoved(t *testing.T)
} }
func TestNetworkManagerBackend_HandleDBusSignal_InvalidBody(t *testing.T) { func TestNetworkManagerBackend_HandleDBusSignal_InvalidBody(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
sig := &dbus.Signal{ sig := &dbus.Signal{
Name: "org.freedesktop.DBus.Properties.PropertiesChanged", Name: "org.freedesktop.DBus.Properties.PropertiesChanged",
@@ -56,10 +58,10 @@ func TestNetworkManagerBackend_HandleDBusSignal_InvalidBody(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleDBusSignal_InvalidInterface(t *testing.T) { func TestNetworkManagerBackend_HandleDBusSignal_InvalidInterface(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
sig := &dbus.Signal{ sig := &dbus.Signal{
Name: "org.freedesktop.DBus.Properties.PropertiesChanged", Name: "org.freedesktop.DBus.Properties.PropertiesChanged",
@@ -72,10 +74,10 @@ func TestNetworkManagerBackend_HandleDBusSignal_InvalidInterface(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleDBusSignal_InvalidChanges(t *testing.T) { func TestNetworkManagerBackend_HandleDBusSignal_InvalidChanges(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
sig := &dbus.Signal{ sig := &dbus.Signal{
Name: "org.freedesktop.DBus.Properties.PropertiesChanged", Name: "org.freedesktop.DBus.Properties.PropertiesChanged",
@@ -88,10 +90,13 @@ func TestNetworkManagerBackend_HandleDBusSignal_InvalidChanges(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleNetworkManagerChange(t *testing.T) { func TestNetworkManagerBackend_HandleNetworkManagerChange(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil).Maybe()
mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe()
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"PrimaryConnection": dbus.MakeVariant("/"), "PrimaryConnection": dbus.MakeVariant("/"),
@@ -104,10 +109,14 @@ func TestNetworkManagerBackend_HandleNetworkManagerChange(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleNetworkManagerChange_WirelessEnabled(t *testing.T) { func TestNetworkManagerBackend_HandleNetworkManagerChange_WirelessEnabled(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
mockNM.EXPECT().GetPropertyWirelessEnabled().Return(true, nil)
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil).Maybe()
mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe()
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"WirelessEnabled": dbus.MakeVariant(true), "WirelessEnabled": dbus.MakeVariant(true),
@@ -119,10 +128,13 @@ func TestNetworkManagerBackend_HandleNetworkManagerChange_WirelessEnabled(t *tes
} }
func TestNetworkManagerBackend_HandleNetworkManagerChange_ActiveConnections(t *testing.T) { func TestNetworkManagerBackend_HandleNetworkManagerChange_ActiveConnections(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil)
mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe()
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"ActiveConnections": dbus.MakeVariant([]interface{}{}), "ActiveConnections": dbus.MakeVariant([]interface{}{}),
@@ -134,10 +146,13 @@ func TestNetworkManagerBackend_HandleNetworkManagerChange_ActiveConnections(t *t
} }
func TestNetworkManagerBackend_HandleDeviceChange(t *testing.T) { func TestNetworkManagerBackend_HandleDeviceChange(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
mockNM.EXPECT().GetPropertyActiveConnections().Return([]gonetworkmanager.ActiveConnection{}, nil).Maybe()
mockNM.EXPECT().GetPropertyPrimaryConnection().Return(nil, nil).Maybe()
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"State": dbus.MakeVariant(uint32(100)), "State": dbus.MakeVariant(uint32(100)),
@@ -149,10 +164,10 @@ func TestNetworkManagerBackend_HandleDeviceChange(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleDeviceChange_Ip4Config(t *testing.T) { func TestNetworkManagerBackend_HandleDeviceChange_Ip4Config(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"Ip4Config": dbus.MakeVariant("/"), "Ip4Config": dbus.MakeVariant("/"),
@@ -164,10 +179,10 @@ func TestNetworkManagerBackend_HandleDeviceChange_Ip4Config(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleWiFiChange_ActiveAccessPoint(t *testing.T) { func TestNetworkManagerBackend_HandleWiFiChange_ActiveAccessPoint(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"ActiveAccessPoint": dbus.MakeVariant("/"), "ActiveAccessPoint": dbus.MakeVariant("/"),
@@ -179,10 +194,10 @@ func TestNetworkManagerBackend_HandleWiFiChange_ActiveAccessPoint(t *testing.T)
} }
func TestNetworkManagerBackend_HandleWiFiChange_AccessPoints(t *testing.T) { func TestNetworkManagerBackend_HandleWiFiChange_AccessPoints(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"AccessPoints": dbus.MakeVariant([]interface{}{}), "AccessPoints": dbus.MakeVariant([]interface{}{}),
@@ -194,10 +209,10 @@ func TestNetworkManagerBackend_HandleWiFiChange_AccessPoints(t *testing.T) {
} }
func TestNetworkManagerBackend_HandleAccessPointChange_NoStrength(t *testing.T) { func TestNetworkManagerBackend_HandleAccessPointChange_NoStrength(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
changes := map[string]dbus.Variant{ changes := map[string]dbus.Variant{
"SomeOtherProperty": dbus.MakeVariant("value"), "SomeOtherProperty": dbus.MakeVariant("value"),
@@ -209,10 +224,10 @@ func TestNetworkManagerBackend_HandleAccessPointChange_NoStrength(t *testing.T)
} }
func TestNetworkManagerBackend_HandleAccessPointChange_WithStrength(t *testing.T) { func TestNetworkManagerBackend_HandleAccessPointChange_WithStrength(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.stateMutex.Lock() backend.stateMutex.Lock()
backend.state.WiFiSignal = 50 backend.state.WiFiSignal = 50
@@ -228,10 +243,10 @@ func TestNetworkManagerBackend_HandleAccessPointChange_WithStrength(t *testing.T
} }
func TestNetworkManagerBackend_StopSignalPump_NoConnection(t *testing.T) { func TestNetworkManagerBackend_StopSignalPump_NoConnection(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.dbusConn = nil backend.dbusConn = nil
assert.NotPanics(t, func() { assert.NotPanics(t, func() {

View File

@@ -3,15 +3,15 @@ package network
import ( import (
"testing" "testing"
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNetworkManagerBackend_New(t *testing.T) { func TestNetworkManagerBackend_New(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err)
}
backend, err := NewNetworkManagerBackend(mockNM)
assert.NoError(t, err)
assert.NotNil(t, backend) assert.NotNil(t, backend)
assert.Equal(t, "networkmanager", backend.state.Backend) assert.Equal(t, "networkmanager", backend.state.Backend)
assert.NotNil(t, backend.stopChan) assert.NotNil(t, backend.stopChan)
@@ -19,10 +19,10 @@ func TestNetworkManagerBackend_New(t *testing.T) {
} }
func TestNetworkManagerBackend_GetCurrentState(t *testing.T) { func TestNetworkManagerBackend_GetCurrentState(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.state.NetworkStatus = StatusWiFi backend.state.NetworkStatus = StatusWiFi
backend.state.WiFiConnected = true backend.state.WiFiConnected = true
@@ -49,10 +49,10 @@ func TestNetworkManagerBackend_GetCurrentState(t *testing.T) {
} }
func TestNetworkManagerBackend_SetPromptBroker_Nil(t *testing.T) { func TestNetworkManagerBackend_SetPromptBroker_Nil(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
err = backend.SetPromptBroker(nil) err = backend.SetPromptBroker(nil)
assert.Error(t, err) assert.Error(t, err)
@@ -60,10 +60,10 @@ func TestNetworkManagerBackend_SetPromptBroker_Nil(t *testing.T) {
} }
func TestNetworkManagerBackend_SubmitCredentials_NoBroker(t *testing.T) { func TestNetworkManagerBackend_SubmitCredentials_NoBroker(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.promptBroker = nil backend.promptBroker = nil
err = backend.SubmitCredentials("token", map[string]string{"password": "test"}, false) err = backend.SubmitCredentials("token", map[string]string{"password": "test"}, false)
@@ -72,10 +72,10 @@ func TestNetworkManagerBackend_SubmitCredentials_NoBroker(t *testing.T) {
} }
func TestNetworkManagerBackend_CancelCredentials_NoBroker(t *testing.T) { func TestNetworkManagerBackend_CancelCredentials_NoBroker(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.promptBroker = nil backend.promptBroker = nil
err = backend.CancelCredentials("token") err = backend.CancelCredentials("token")
@@ -84,10 +84,10 @@ func TestNetworkManagerBackend_CancelCredentials_NoBroker(t *testing.T) {
} }
func TestNetworkManagerBackend_EnsureWiFiDevice_NoDevice(t *testing.T) { func TestNetworkManagerBackend_EnsureWiFiDevice_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
backend.wifiDev = nil backend.wifiDev = nil
@@ -98,10 +98,10 @@ func TestNetworkManagerBackend_EnsureWiFiDevice_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_EnsureWiFiDevice_AlreadySet(t *testing.T) { func TestNetworkManagerBackend_EnsureWiFiDevice_AlreadySet(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDev = "dummy-device" backend.wifiDev = "dummy-device"
@@ -110,10 +110,10 @@ func TestNetworkManagerBackend_EnsureWiFiDevice_AlreadySet(t *testing.T) {
} }
func TestNetworkManagerBackend_StartSecretAgent_NoBroker(t *testing.T) { func TestNetworkManagerBackend_StartSecretAgent_NoBroker(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.promptBroker = nil backend.promptBroker = nil
err = backend.startSecretAgent() err = backend.startSecretAgent()
@@ -122,10 +122,10 @@ func TestNetworkManagerBackend_StartSecretAgent_NoBroker(t *testing.T) {
} }
func TestNetworkManagerBackend_Close(t *testing.T) { func TestNetworkManagerBackend_Close(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
backend.Close() backend.Close()
@@ -133,20 +133,20 @@ func TestNetworkManagerBackend_Close(t *testing.T) {
} }
func TestNetworkManagerBackend_GetPromptBroker(t *testing.T) { func TestNetworkManagerBackend_GetPromptBroker(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
broker := backend.GetPromptBroker() broker := backend.GetPromptBroker()
assert.Nil(t, broker) assert.Nil(t, broker)
} }
func TestNetworkManagerBackend_StopMonitoring_NoSignals(t *testing.T) { func TestNetworkManagerBackend_StopMonitoring_NoSignals(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
backend.StopMonitoring() backend.StopMonitoring()

View File

@@ -21,33 +21,26 @@ func TestNetworkManagerBackend_GetWiFiEnabled(t *testing.T) {
} }
func TestNetworkManagerBackend_SetWiFiEnabled(t *testing.T) { func TestNetworkManagerBackend_SetWiFiEnabled(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err)
}
originalState, err := backend.GetWiFiEnabled() backend, err := NewNetworkManagerBackend(mockNM)
if err != nil { assert.NoError(t, err)
t.Skipf("Cannot get WiFi state: %v", err)
}
defer func() { mockNM.EXPECT().SetPropertyWirelessEnabled(true).Return(nil)
backend.SetWiFiEnabled(originalState)
}()
err = backend.SetWiFiEnabled(!originalState) err = backend.SetWiFiEnabled(true)
assert.NoError(t, err) assert.NoError(t, err)
backend.stateMutex.RLock() backend.stateMutex.RLock()
assert.Equal(t, !originalState, backend.state.WiFiEnabled) assert.True(t, backend.state.WiFiEnabled)
backend.stateMutex.RUnlock() backend.stateMutex.RUnlock()
} }
func TestNetworkManagerBackend_ScanWiFi_NoDevice(t *testing.T) { func TestNetworkManagerBackend_ScanWiFi_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
err = backend.ScanWiFi() err = backend.ScanWiFi()
@@ -56,14 +49,14 @@ func TestNetworkManagerBackend_ScanWiFi_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_ScanWiFi_Disabled(t *testing.T) { func TestNetworkManagerBackend_ScanWiFi_Disabled(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil { mockDeviceWireless := mock_gonetworkmanager.NewMockDeviceWireless(t)
t.Skipf("NetworkManager not available: %v", err)
}
if backend.wifiDevice == nil { backend, err := NewNetworkManagerBackend(mockNM)
t.Skip("No WiFi device available") assert.NoError(t, err)
}
backend.wifiDevice = mockDeviceWireless
backend.wifiDev = mockDeviceWireless
backend.stateMutex.Lock() backend.stateMutex.Lock()
backend.state.WiFiEnabled = false backend.state.WiFiEnabled = false
@@ -75,10 +68,10 @@ func TestNetworkManagerBackend_ScanWiFi_Disabled(t *testing.T) {
} }
func TestNetworkManagerBackend_GetWiFiNetworkDetails_NoDevice(t *testing.T) { func TestNetworkManagerBackend_GetWiFiNetworkDetails_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
_, err = backend.GetWiFiNetworkDetails("TestNetwork") _, err = backend.GetWiFiNetworkDetails("TestNetwork")
@@ -87,10 +80,10 @@ func TestNetworkManagerBackend_GetWiFiNetworkDetails_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_ConnectWiFi_NoDevice(t *testing.T) { func TestNetworkManagerBackend_ConnectWiFi_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
req := ConnectionRequest{SSID: "TestNetwork", Password: "password"} req := ConnectionRequest{SSID: "TestNetwork", Password: "password"}
@@ -100,14 +93,14 @@ func TestNetworkManagerBackend_ConnectWiFi_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_ConnectWiFi_AlreadyConnected(t *testing.T) { func TestNetworkManagerBackend_ConnectWiFi_AlreadyConnected(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil { mockDeviceWireless := mock_gonetworkmanager.NewMockDeviceWireless(t)
t.Skipf("NetworkManager not available: %v", err)
}
if backend.wifiDevice == nil { backend, err := NewNetworkManagerBackend(mockNM)
t.Skip("No WiFi device available") assert.NoError(t, err)
}
backend.wifiDevice = mockDeviceWireless
backend.wifiDev = mockDeviceWireless
backend.stateMutex.Lock() backend.stateMutex.Lock()
backend.state.WiFiConnected = true backend.state.WiFiConnected = true
@@ -120,10 +113,10 @@ func TestNetworkManagerBackend_ConnectWiFi_AlreadyConnected(t *testing.T) {
} }
func TestNetworkManagerBackend_DisconnectWiFi_NoDevice(t *testing.T) { func TestNetworkManagerBackend_DisconnectWiFi_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
err = backend.DisconnectWiFi() err = backend.DisconnectWiFi()
@@ -132,10 +125,10 @@ func TestNetworkManagerBackend_DisconnectWiFi_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_IsConnectingTo(t *testing.T) { func TestNetworkManagerBackend_IsConnectingTo(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.stateMutex.Lock() backend.stateMutex.Lock()
backend.state.IsConnecting = true backend.state.IsConnecting = true
@@ -147,10 +140,10 @@ func TestNetworkManagerBackend_IsConnectingTo(t *testing.T) {
} }
func TestNetworkManagerBackend_IsConnectingTo_NotConnecting(t *testing.T) { func TestNetworkManagerBackend_IsConnectingTo_NotConnecting(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.stateMutex.Lock() backend.stateMutex.Lock()
backend.state.IsConnecting = false backend.state.IsConnecting = false
@@ -161,10 +154,10 @@ func TestNetworkManagerBackend_IsConnectingTo_NotConnecting(t *testing.T) {
} }
func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) { func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
_, err = backend.updateWiFiNetworks() _, err = backend.updateWiFiNetworks()
@@ -173,10 +166,10 @@ func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) {
} }
func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) { func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.settings = nil backend.settings = nil
_, err = backend.findConnection("NonExistentNetwork") _, err = backend.findConnection("NonExistentNetwork")
@@ -184,10 +177,10 @@ func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) {
} }
func TestNetworkManagerBackend_CreateAndConnectWiFi_NoDevice(t *testing.T) { func TestNetworkManagerBackend_CreateAndConnectWiFi_NoDevice(t *testing.T) {
backend, err := NewNetworkManagerBackend() mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
if err != nil {
t.Skipf("NetworkManager not available: %v", err) backend, err := NewNetworkManagerBackend(mockNM)
} assert.NoError(t, err)
backend.wifiDevice = nil backend.wifiDevice = nil
backend.wifiDev = nil backend.wifiDev = nil

View File

@@ -240,19 +240,25 @@ func TestHandleSubscribe(t *testing.T) {
func TestManager_Subscribe_Unsubscribe(t *testing.T) { func TestManager_Subscribe_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
} }
t.Run("subscribe creates channel", func(t *testing.T) { t.Run("subscribe creates channel", func(t *testing.T) {
ch := manager.Subscribe("client1") ch := manager.Subscribe("client1")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Len(t, manager.subscribers, 1) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool {
count++
return true
})
assert.Equal(t, 1, count)
}) })
t.Run("unsubscribe removes channel", func(t *testing.T) { t.Run("unsubscribe removes channel", func(t *testing.T) {
manager.Unsubscribe("client1") manager.Unsubscribe("client1")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true })
assert.Equal(t, 0, count)
}) })
t.Run("unsubscribe non-existent client is safe", func(t *testing.T) { t.Run("unsubscribe non-existent client is safe", func(t *testing.T) {

View File

@@ -66,13 +66,10 @@ func NewManager() (*Manager, error) {
Preference: PreferenceAuto, Preference: PreferenceAuto,
WiFiNetworks: []WiFiNetwork{}, WiFiNetworks: []WiFiNetwork{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
credentialSubscribers: make(map[string]chan CredentialPrompt),
credSubMutex: sync.RWMutex{},
} }
broker := NewSubscriptionBroker(m.broadcastCredentialPrompt) broker := NewSubscriptionBroker(m.broadcastCredentialPrompt)
@@ -270,48 +267,36 @@ func (m *Manager) GetState() NetworkState {
func (m *Manager) Subscribe(id string) chan NetworkState { func (m *Manager) Subscribe(id string) chan NetworkState {
ch := make(chan NetworkState, 64) ch := make(chan NetworkState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt { func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt {
ch := make(chan CredentialPrompt, 16) ch := make(chan CredentialPrompt, 16)
m.credSubMutex.Lock() m.credentialSubscribers.Store(id, ch)
m.credentialSubscribers[id] = ch
m.credSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeCredentials(id string) { func (m *Manager) UnsubscribeCredentials(id string) {
m.credSubMutex.Lock() if ch, ok := m.credentialSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.credentialSubscribers[id]; ok {
close(ch) close(ch)
delete(m.credentialSubscribers, id)
} }
m.credSubMutex.Unlock()
} }
func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) { func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) {
m.credSubMutex.RLock() m.credentialSubscribers.Range(func(key string, ch chan CredentialPrompt) bool {
defer m.credSubMutex.RUnlock()
for _, ch := range m.credentialSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) notifier() { func (m *Manager) notifier() {
@@ -335,28 +320,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan NetworkState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -396,12 +374,11 @@ func (m *Manager) Close() {
m.backend.Close() m.backend.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan NetworkState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan NetworkState) return true
m.subMutex.Unlock() })
} }
func (m *Manager) ScanWiFi() error { func (m *Manager) ScanWiFi() error {

View File

@@ -31,19 +31,15 @@ func TestManager_NotifySubscribers(t *testing.T) {
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusWiFi, NetworkStatus: StatusWiFi,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan NetworkState, 10) ch := make(chan NetworkState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
@@ -63,19 +59,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusWiFi, NetworkStatus: StatusWiFi,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan NetworkState, 10) ch := make(chan NetworkState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
manager.notifySubscribers() manager.notifySubscribers()
@@ -98,19 +90,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
} }
ch1 := make(chan NetworkState, 1) ch1 := make(chan NetworkState, 1)
ch2 := make(chan NetworkState, 1) ch2 := make(chan NetworkState, 1)
manager.subMutex.Lock() manager.subscribers.Store("client1", ch1)
manager.subscribers["client1"] = ch1 manager.subscribers.Store("client2", ch2)
manager.subscribers["client2"] = ch2
manager.subMutex.Unlock()
manager.Close() manager.Close()
@@ -125,31 +113,27 @@ func TestManager_Close(t *testing.T) {
assert.False(t, ok1, "ch1 should be closed") assert.False(t, ok1, "ch1 should be closed")
assert.False(t, ok2, "ch2 should be closed") assert.False(t, ok2, "ch2 should be closed")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true })
assert.Equal(t, 0, count)
} }
func TestManager_Subscribe(t *testing.T) { func TestManager_Subscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 64, cap(ch)) assert.Equal(t, 64, cap(ch))
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.True(t, exists) assert.True(t, exists)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
@@ -159,9 +143,7 @@ func TestManager_Unsubscribe(t *testing.T) {
_, ok := <-ch _, ok := <-ch
assert.False(t, ok) assert.False(t, ok)
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.False(t, exists) assert.False(t, exists)
} }

View File

@@ -3,37 +3,29 @@ package network
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"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/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest pathSettingToToken syncmap.Map[string, string]
pathSettingToToken map[string]string
broadcastPrompt func(CredentialPrompt) broadcastPrompt func(CredentialPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply), broadcastPrompt: broadcastPrompt,
requests: make(map[string]PromptRequest),
pathSettingToToken: make(map[string]string),
broadcastPrompt: broadcastPrompt,
} }
} }
func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) { func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) {
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
b.mu.Lock() if existingToken, alreadyPending := b.pathSettingToToken.Load(pathSettingKey); alreadyPending {
existingToken, alreadyPending := b.pathSettingToToken[pathSettingKey]
b.mu.Unlock()
if alreadyPending {
log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey) log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey)
return existingToken, nil return existingToken, nil
} }
@@ -44,11 +36,9 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req b.pathSettingToToken.Store(pathSettingKey, token)
b.pathSettingToToken[pathSettingKey] = token
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := CredentialPrompt{ prompt := CredentialPrompt{
@@ -71,10 +61,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -93,10 +80,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token) log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token)
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
@@ -112,25 +96,19 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() if req, exists := b.requests.Load(token); exists {
defer b.mu.Unlock()
if req, exists := b.requests[token]; exists {
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
delete(b.pathSettingToToken, pathSettingKey) b.pathSettingToToken.Delete(pathSettingKey)
} }
delete(b.pending, token) b.pending.Delete(token)
delete(b.requests, token) b.requests.Delete(token)
} }
func (b *SubscriptionBroker) Cancel(path string, setting string) error { func (b *SubscriptionBroker) Cancel(path string, setting string) error {
pathSettingKey := fmt.Sprintf("%s:%s", path, setting) pathSettingKey := fmt.Sprintf("%s:%s", path, setting)
b.mu.Lock() token, exists := b.pathSettingToToken.Load(pathSettingKey)
token, exists := b.pathSettingToToken[pathSettingKey]
b.mu.Unlock()
if !exists { if !exists {
log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey) log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey)
return nil return nil

View File

@@ -6,10 +6,9 @@ func NewTestManager(backend Backend, state *NetworkState) *Manager {
state = &NetworkState{} state = &NetworkState{}
} }
return &Manager{ return &Manager{
backend: backend, backend: backend,
state: state, state: state,
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
} }
} }

View File

@@ -3,6 +3,7 @@ package network
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -108,14 +109,12 @@ type Manager struct {
backend Backend backend Backend
state *NetworkState state *NetworkState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan NetworkState subscribers syncmap.Map[string, chan NetworkState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *NetworkState lastNotifiedState *NetworkState
credentialSubscribers map[string]chan CredentialPrompt credentialSubscribers syncmap.Map[string, chan CredentialPrompt]
credSubMutex sync.RWMutex
} }
type EventType string type EventType string

View File

@@ -9,6 +9,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
@@ -165,6 +166,20 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "evdev.") {
if evdevManager == nil {
models.RespondError(conn, req.ID, "evdev manager not initialized")
return
}
evdevReq := evdev.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
evdev.HandleRequest(conn, evdevReq, evdevManager)
return
}
switch req.Method { switch req.Method {
case "ping": case "ping":
models.Respond(conn, req.ID, "pong") models.Respond(conn, req.ID, "pong")

View File

@@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@@ -18,6 +19,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
@@ -26,9 +28,10 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 17 const APIVersion = 18
type Capabilities struct { type Capabilities struct {
Capabilities []string `json:"capabilities"` Capabilities []string `json:"capabilities"`
@@ -54,13 +57,12 @@ var dwlManager *dwl.Manager
var extWorkspaceManager *extworkspace.Manager var extWorkspaceManager *extworkspace.Manager
var brightnessManager *brightness.Manager var brightnessManager *brightness.Manager
var wlrOutputManager *wlroutput.Manager var wlrOutputManager *wlroutput.Manager
var evdevManager *evdev.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var capabilitySubscribers = make(map[string]chan ServerInfo) var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var capabilityMutex sync.RWMutex var cupsSubscribers syncmap.Map[string, bool]
var cupsSubscriberCount atomic.Int32
var cupsSubscribers = make(map[string]bool)
var cupsSubscribersMutex sync.Mutex
func getSocketDir() string { func getSocketDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
@@ -292,6 +294,19 @@ func InitializeWlrOutputManager() error {
return nil return nil
} }
func InitializeEvdevManager() error {
manager, err := evdev.InitializeManager()
if err != nil {
log.Warnf("Failed to initialize evdev manager: %v", err)
return err
}
evdevManager = manager
log.Info("Evdev manager initialized")
return nil
}
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
@@ -358,6 +373,10 @@ func getCapabilities() Capabilities {
caps = append(caps, "wlroutput") caps = append(caps, "wlroutput")
} }
if evdevManager != nil {
caps = append(caps, "evdev")
}
return Capabilities{Capabilities: caps} return Capabilities{Capabilities: caps}
} }
@@ -404,6 +423,10 @@ func getServerInfo() ServerInfo {
caps = append(caps, "wlroutput") caps = append(caps, "wlroutput")
} }
if evdevManager != nil {
caps = append(caps, "evdev")
}
return ServerInfo{ return ServerInfo{
APIVersion: APIVersion, APIVersion: APIVersion,
Capabilities: caps, Capabilities: caps,
@@ -411,16 +434,14 @@ func getServerInfo() ServerInfo {
} }
func notifyCapabilityChange() { func notifyCapabilityChange() {
capabilityMutex.RLock()
defer capabilityMutex.RUnlock()
info := getServerInfo() info := getServerInfo()
for _, ch := range capabilitySubscribers { capabilitySubscribers.Range(func(key string, ch chan ServerInfo) bool {
select { select {
case ch <- info: case ch <- info:
default: default:
} }
} return true
})
} }
func handleSubscribe(conn net.Conn, req models.Request) { func handleSubscribe(conn net.Conn, req models.Request) {
@@ -452,18 +473,12 @@ func handleSubscribe(conn net.Conn, req models.Request) {
stopChan := make(chan struct{}) stopChan := make(chan struct{})
capChan := make(chan ServerInfo, 64) capChan := make(chan ServerInfo, 64)
capabilityMutex.Lock() capabilitySubscribers.Store(clientID+"-capabilities", capChan)
capabilitySubscribers[clientID+"-capabilities"] = capChan
capabilityMutex.Unlock()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
defer func() { defer capabilitySubscribers.Delete(clientID + "-capabilities")
capabilityMutex.Lock()
delete(capabilitySubscribers, clientID+"-capabilities")
capabilityMutex.Unlock()
}()
for { for {
select { select {
@@ -705,12 +720,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
} }
if shouldSubscribe("cups") { if shouldSubscribe("cups") {
cupsSubscribersMutex.Lock() cupsSubscribers.Store(clientID+"-cups", true)
wasEmpty := len(cupsSubscribers) == 0 count := cupsSubscriberCount.Add(1)
cupsSubscribers[clientID+"-cups"] = true
cupsSubscribersMutex.Unlock()
if wasEmpty { if count == 1 {
if err := InitializeCupsManager(); err != nil { if err := InitializeCupsManager(); err != nil {
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
} else { } else {
@@ -725,13 +738,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
defer wg.Done() defer wg.Done()
defer func() { defer func() {
cupsManager.Unsubscribe(clientID + "-cups") cupsManager.Unsubscribe(clientID + "-cups")
cupsSubscribers.Delete(clientID + "-cups")
count := cupsSubscriberCount.Add(-1)
cupsSubscribersMutex.Lock() if count == 0 {
delete(cupsSubscribers, clientID+"-cups")
isEmpty := len(cupsSubscribers) == 0
cupsSubscribersMutex.Unlock()
if isEmpty {
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager") log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
@@ -799,36 +809,46 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("extworkspace") && extWorkspaceManager != nil { if shouldSubscribe("extworkspace") {
wg.Add(1) if extWorkspaceManager == nil {
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace") if err := InitializeExtWorkspaceManager(); err != nil {
go func() { log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err)
defer wg.Done() } else {
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace") notifyCapabilityChange()
initialState := extWorkspaceManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
case <-stopChan:
return
} }
}
for { if extWorkspaceManager != nil {
wg.Add(1)
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace")
go func() {
defer wg.Done()
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace")
initialState := extWorkspaceManager.GetState()
select { select {
case state, ok := <-extWorkspaceChan: case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
case <-stopChan:
return
}
case <-stopChan: case <-stopChan:
return return
} }
}
}() for {
select {
case state, ok := <-extWorkspaceChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
} }
if shouldSubscribe("brightness") && brightnessManager != nil { if shouldSubscribe("brightness") && brightnessManager != nil {
@@ -918,6 +938,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("evdev") && evdevManager != nil {
wg.Add(1)
evdevChan := evdevManager.Subscribe(clientID + "-evdev")
go func() {
defer wg.Done()
defer evdevManager.Unsubscribe(clientID + "-evdev")
initialState := evdevManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "evdev", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-evdevChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "evdev", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
go func() { go func() {
wg.Wait() wg.Wait()
close(eventChan) close(eventChan)
@@ -974,6 +1026,9 @@ func cleanupManagers() {
if wlrOutputManager != nil { if wlrOutputManager != nil {
wlrOutputManager.Close() wlrOutputManager.Close()
} }
if evdevManager != nil {
evdevManager.Close()
}
if wlContext != nil { if wlContext != nil {
wlContext.Close() wlContext.Close()
} }
@@ -1122,6 +1177,9 @@ func Start(printDocs bool) error {
log.Info(" - transform : Transform value (optional)") log.Info(" - transform : Transform value (optional)")
log.Info(" - scale : Scale value (optional)") log.Info(" - scale : Scale value (optional)")
log.Info(" - adaptiveSync : Adaptive sync state (optional)") log.Info(" - adaptiveSync : Adaptive sync state (optional)")
log.Info("Evdev:")
log.Info(" evdev.getState - Get current evdev state (caps lock)")
log.Info(" evdev.subscribe - Subscribe to evdev state changes (streaming)")
log.Info("") log.Info("")
} }
log.Info("Initializing managers...") log.Info("Initializing managers...")
@@ -1183,10 +1241,6 @@ func Start(printDocs bool) error {
log.Debugf("DWL manager unavailable: %v", err) log.Debugf("DWL manager unavailable: %v", err)
} }
if err := InitializeExtWorkspaceManager(); err != nil {
log.Debugf("ExtWorkspace manager unavailable: %v", err)
}
if err := InitializeWlrOutputManager(); err != nil { if err := InitializeWlrOutputManager(); err != nil {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }
@@ -1194,10 +1248,14 @@ func Start(printDocs bool) error {
fatalErrChan := make(chan error, 1) fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil { if wlrOutputManager != nil {
go func() { go func() {
select { err := <-wlrOutputManager.FatalError()
case err := <-wlrOutputManager.FatalError(): fatalErrChan <- fmt.Errorf("WlrOutput fatal error: %w", err)
fatalErrChan <- fmt.Errorf("WlrOutput fatal error: %w", err) }()
} }
if wlContext != nil {
go func() {
err := <-wlContext.FatalError()
fatalErrChan <- fmt.Errorf("Wayland context fatal error: %w", err)
}() }()
} }
@@ -1209,6 +1267,14 @@ func Start(printDocs bool) error {
} }
}() }()
go func() {
if err := InitializeEvdevManager(); err != nil {
log.Debugf("Evdev manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
}()
if wlContext != nil { if wlContext != nil {
wlContext.Start() wlContext.Start()
log.Info("Wayland event dispatcher started") log.Info("Wayland event dispatcher started")

View File

@@ -8,8 +8,8 @@ import (
"syscall" "syscall"
"time" "time"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
wlclient "github.com/yaslama/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -23,13 +23,13 @@ func NewManager(display *wlclient.Display, config Config) (*Manager, error) {
} }
m := &Manager{ m := &Manager{
config: config, config: config,
display: display, display: display,
outputs: make(map[uint32]*outputState), ctx: display.Context(),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1), updateTrigger: make(chan struct{}, 1),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16), dbusSignal: make(chan *dbus.Signal, 16),
transitionChan: make(chan int, 1), transitionChan: make(chan int, 1),
@@ -113,17 +113,17 @@ func (m *Manager) waylandActor() {
} }
func (m *Manager) allOutputsReady() bool { func (m *Manager) allOutputsReady() bool {
m.outputsMutex.RLock() hasOutputs := false
defer m.outputsMutex.RUnlock() allReady := true
if len(m.outputs) == 0 { m.outputs.Range(func(key uint32, value *outputState) bool {
return false hasOutputs = true
} if value.rampSize == 0 || value.failed {
for _, o := range m.outputs { allReady = false
if o.rampSize == 0 || o.failed {
return false return false
} }
} return true
return true })
return hasOutputs && allReady
} }
func (m *Manager) setupDBusMonitor() error { func (m *Manager) setupDBusMonitor() error {
@@ -148,7 +148,6 @@ func (m *Manager) setupDBusMonitor() error {
func (m *Manager) setupRegistry() error { func (m *Manager) setupRegistry() error {
log.Info("setupRegistry: starting registry setup") log.Info("setupRegistry: starting registry setup")
ctx := m.display.Context()
registry, err := m.display.GetRegistry() registry, err := m.display.GetRegistry()
if err != nil { if err != nil {
@@ -157,7 +156,6 @@ func (m *Manager) setupRegistry() error {
m.registry = registry m.registry = registry
outputs := make([]*wlclient.Output, 0) outputs := make([]*wlclient.Output, 0)
outputRegNames := make(map[uint32]uint32)
outputNames := make(map[uint32]string) outputNames := make(map[uint32]string)
var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1 var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1
@@ -165,7 +163,7 @@ func (m *Manager) setupRegistry() error {
switch e.Interface { switch e.Interface {
case wlr_gamma_control.ZwlrGammaControlManagerV1InterfaceName: case wlr_gamma_control.ZwlrGammaControlManagerV1InterfaceName:
log.Infof("setupRegistry: found %s", wlr_gamma_control.ZwlrGammaControlManagerV1InterfaceName) log.Infof("setupRegistry: found %s", wlr_gamma_control.ZwlrGammaControlManagerV1InterfaceName)
manager := wlr_gamma_control.NewZwlrGammaControlManagerV1(ctx) manager := wlr_gamma_control.NewZwlrGammaControlManagerV1(m.ctx)
version := e.Version version := e.Version
if version > 1 { if version > 1 {
version = 1 version = 1
@@ -178,7 +176,7 @@ func (m *Manager) setupRegistry() error {
} }
case "wl_output": case "wl_output":
log.Debugf("Global event: found wl_output (name=%d)", e.Name) log.Debugf("Global event: found wl_output (name=%d)", e.Name)
output := wlclient.NewOutput(ctx) output := wlclient.NewOutput(m.ctx)
version := e.Version version := e.Version
if version > 4 { if version > 4 {
version = 4 version = 4
@@ -198,14 +196,9 @@ func (m *Manager) setupRegistry() error {
if gammaMgr != nil { if gammaMgr != nil {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name
} }
m.outputsMutex.Lock() m.outputRegNames.Store(outputID, e.Name)
if m.outputRegNames != nil {
m.outputRegNames[outputID] = e.Name
}
m.outputsMutex.Unlock()
m.configMutex.RLock() m.configMutex.RLock()
enabled := m.config.Enabled enabled := m.config.Enabled
@@ -236,23 +229,33 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock() var foundID uint32
defer m.outputsMutex.Unlock() var foundOut *outputState
m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("Output %d (registry name %d) removed, destroying gamma control", id, e.Name) foundID = id
if out.gammaControl != nil { foundOut = out
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) return false
control.Destroy() }
} return true
delete(m.outputs, id) })
if len(m.outputs) == 0 { if foundOut != nil {
m.controlsInitialized = false log.Infof("Output %d (registry name %d) removed, destroying gamma control", foundID, e.Name)
log.Info("All outputs removed, controls no longer initialized") if foundOut.gammaControl != nil {
} control := foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
return control.Destroy()
}
m.outputs.Delete(foundID)
hasOutputs := false
m.outputs.Range(func(key uint32, value *outputState) bool {
hasOutputs = true
return false
})
if !hasOutputs {
m.controlsInitialized = false
log.Info("All outputs removed, controls no longer initialized")
} }
} }
}) })
@@ -292,7 +295,6 @@ func (m *Manager) setupRegistry() error {
m.gammaControl = gammaMgr m.gammaControl = gammaMgr
m.availableOutputs = physicalOutputs m.availableOutputs = physicalOutputs
m.outputRegNames = outputRegNames
log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)") log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)")
return nil return nil
@@ -308,9 +310,12 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
continue continue
} }
outputID := output.ID()
registryName, _ := m.outputRegNames.Load(outputID)
outState := &outputState{ outState := &outputState{
id: output.ID(), id: outputID,
registryName: m.outputRegNames[output.ID()], registryName: registryName,
output: output, output: output,
gammaControl: control, gammaControl: control,
isVirtual: false, isVirtual: false,
@@ -318,14 +323,12 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
func(state *outputState) { func(state *outputState) {
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.rampSize = e.Size outState.rampSize = e.Size
outState.failed = false outState.failed = false
outState.retryCount = 0 outState.retryCount = 0
log.Infof("Output %d gamma_size=%d", state.id, e.Size) log.Infof("Output %d gamma_size=%d", state.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -337,8 +340,7 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.failed = true outState.failed = true
outState.rampSize = 0 outState.rampSize = 0
outState.retryCount++ outState.retryCount++
@@ -357,13 +359,10 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
}(outState) }(outState)
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[output.ID()] = outState
m.outputsMutex.Unlock()
} }
return nil return nil
@@ -375,8 +374,7 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
var outputName string var outputName string
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
outputName = ev.Name outputName = ev.Name
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(outputID); exists {
if outState, exists := m.outputs[outputID]; exists {
outState.name = ev.Name outState.name = ev.Name
if len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" { if len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" {
log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name) log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name)
@@ -384,7 +382,6 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
outState.failed = true outState.failed = true
} }
} }
m.outputsMutex.Unlock()
}) })
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
@@ -394,24 +391,24 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
return fmt.Errorf("failed to get gamma control: %w", err) return fmt.Errorf("failed to get gamma control: %w", err)
} }
registryName, _ := m.outputRegNames.Load(outputID)
outState := &outputState{ outState := &outputState{
id: outputID, id: outputID,
name: outputName, name: outputName,
registryName: m.outputRegNames[outputID], registryName: registryName,
output: output, output: output,
gammaControl: control, gammaControl: control,
isVirtual: false, isVirtual: false,
} }
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outState.id); exists {
if out, exists := m.outputs[outState.id]; exists {
out.rampSize = e.Size out.rampSize = e.Size
out.failed = false out.failed = false
out.retryCount = 0 out.retryCount = 0
log.Infof("Output %d gamma_size=%d", outState.id, e.Size) log.Infof("Output %d gamma_size=%d", outState.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -423,8 +420,7 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outState.id); exists {
if out, exists := m.outputs[outState.id]; exists {
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
out.retryCount++ out.retryCount++
@@ -443,12 +439,9 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[output.ID()] = outState
m.outputsMutex.Unlock()
log.Infof("Added gamma control for output %d", output.ID()) log.Infof("Added gamma control for output %d", output.ID())
return nil return nil
@@ -623,17 +616,19 @@ func (m *Manager) transitionWorker() {
if !enabled && targetTemp == identityTemp && m.controlsInitialized { if !enabled && targetTemp == identityTemp && m.controlsInitialized {
m.post(func() { m.post(func() {
log.Info("Destroying gamma controls after transition to identity") log.Info("Destroying gamma controls after transition to identity")
m.outputsMutex.Lock() m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.gammaControl != nil { if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy() control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id) log.Debugf("Destroyed gamma control for output %d", id)
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false m.controlsInitialized = false
m.outputsMutex.Unlock()
m.transitionMutex.Lock() m.transitionMutex.Lock()
m.currentTemp = identityTemp m.currentTemp = identityTemp
@@ -661,9 +656,7 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
return nil return nil
} }
m.outputsMutex.RLock() _, exists := m.outputs.Load(out.id)
_, exists := m.outputs[out.id]
m.outputsMutex.RUnlock()
if !exists { if !exists {
return nil return nil
@@ -689,14 +682,12 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
state := out state := out
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.rampSize = e.Size outState.rampSize = e.Size
outState.failed = false outState.failed = false
outState.retryCount = 0 outState.retryCount = 0
log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size) log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -708,8 +699,7 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.failed = true outState.failed = true
outState.rampSize = 0 outState.rampSize = 0
outState.retryCount++ outState.retryCount++
@@ -728,7 +718,6 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
out.gammaControl = control out.gammaControl = control
@@ -750,13 +739,11 @@ func (m *Manager) applyNowOnActor(temp int) {
return return
} }
// Lock while snapshotting outputs to prevent races with recreateOutputControl
m.outputsMutex.RLock()
var outs []*outputState var outs []*outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, value *outputState) bool {
outs = append(outs, out) outs = append(outs, value)
} return true
m.outputsMutex.RUnlock() })
if len(outs) == 0 { if len(outs) == 0 {
return return
@@ -796,20 +783,17 @@ func (m *Manager) applyNowOnActor(temp int) {
if err := m.setGammaBytesActor(j.out, j.data); err != nil { if err := m.setGammaBytesActor(j.out, j.data); err != nil {
log.Warnf("Failed to set gamma for output %d: %v", j.out.id, err) log.Warnf("Failed to set gamma for output %d: %v", j.out.id, err)
outID := j.out.id outID := j.out.id
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outID); exists {
if out, exists := m.outputs[outID]; exists {
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
} }
m.outputsMutex.Unlock()
time.AfterFunc(300*time.Millisecond, func() { time.AfterFunc(300*time.Millisecond, func() {
m.post(func() { m.post(func() {
m.outputsMutex.RLock() if out, exists := m.outputs.Load(outID); exists {
out, exists := m.outputs[outID] if out.failed {
m.outputsMutex.RUnlock() m.recreateOutputControl(out)
if exists && out.failed { }
m.recreateOutputControl(out)
} }
}) })
}) })
@@ -935,28 +919,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) { if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan State) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -1296,17 +1273,19 @@ func (m *Manager) SetEnabled(enabled bool) {
if currentTemp == identityTemp { if currentTemp == identityTemp {
m.post(func() { m.post(func() {
log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp) log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp)
m.outputsMutex.Lock() m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.gammaControl != nil { if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy() control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id) log.Debugf("Destroyed gamma control for output %d", id)
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false m.controlsInitialized = false
m.outputsMutex.Unlock()
m.transitionMutex.Lock() m.transitionMutex.Lock()
m.currentTemp = identityTemp m.currentTemp = identityTemp
@@ -1332,21 +1311,22 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if control, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok { if control, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok {
control.Destroy() control.Destroy()
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputsMutex.Unlock() m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok { if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok {
manager.Destroy() manager.Destroy()

View File

@@ -6,8 +6,9 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
wlclient "github.com/yaslama/go-wayland/wayland/client"
) )
type Config struct { type Config struct {
@@ -44,12 +45,12 @@ type Manager struct {
stateMutex sync.RWMutex stateMutex sync.RWMutex
display *wlclient.Display display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry registry *wlclient.Registry
gammaControl interface{} gammaControl interface{}
availableOutputs []*wlclient.Output availableOutputs []*wlclient.Output
outputRegNames map[uint32]uint32 outputRegNames syncmap.Map[uint32, uint32]
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
controlsInitialized bool controlsInitialized bool
cmdq chan cmd cmdq chan cmd
@@ -68,8 +69,7 @@ type Manager struct {
cachedIPLon *float64 cachedIPLon *float64
locationMutex sync.RWMutex locationMutex sync.RWMutex
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -146,19 +146,14 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -6,15 +6,16 @@ import (
"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"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type SharedContext struct { type SharedContext struct {
display *wlclient.Display display *wlclient.Display
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup fatalError chan error
mu sync.Mutex wg sync.WaitGroup
started bool mu sync.Mutex
started bool
} }
func New() (*SharedContext, error) { func New() (*SharedContext, error) {
@@ -24,9 +25,10 @@ func New() (*SharedContext, error) {
} }
sc := &SharedContext{ sc := &SharedContext{
display: display, display: display,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
started: false, fatalError: make(chan error, 1),
started: false,
} }
return sc, nil return sc, nil
@@ -49,8 +51,22 @@ func (sc *SharedContext) Display() *wlclient.Display {
return sc.display return sc.display
} }
func (sc *SharedContext) FatalError() <-chan error {
return sc.fatalError
}
func (sc *SharedContext) eventDispatcher() { func (sc *SharedContext) eventDispatcher() {
defer sc.wg.Done() defer sc.wg.Done()
defer func() {
if r := recover(); r != nil {
err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r)
log.Error(err)
select {
case sc.fatalError <- err:
default:
}
}
}()
ctx := sc.display.Context() ctx := sc.display.Context()
for { for {

View File

@@ -154,14 +154,13 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)") statusChan <- fmt.Errorf("configuration cancelled (outdated serial)")
}) })
m.headsMutex.RLock()
headsByName := make(map[string]*headState) headsByName := make(map[string]*headState)
for _, head := range m.heads { m.heads.Range(func(key uint32, head *headState) bool {
if !head.finished { if !head.finished {
headsByName[head.name] = head headsByName[head.name] = head
} }
} return true
m.headsMutex.RUnlock() })
for _, headCfg := range heads { for _, headCfg := range heads {
head, exists := headsByName[headCfg.Name] head, exists := headsByName[headCfg.Name]
@@ -188,9 +187,7 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
} }
if headCfg.ModeID != nil { if headCfg.ModeID != nil {
m.modesMutex.RLock() mode, exists := m.modes.Load(*headCfg.ModeID)
mode, exists := m.modes[*headCfg.ModeID]
m.modesMutex.RUnlock()
if !exists { if !exists {
config.Destroy() config.Destroy()

View File

@@ -6,19 +6,17 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
heads: make(map[uint32]*headState), ctx: display.Context(),
modes: make(map[uint32]*modeState), cmdq: make(chan cmd, 128),
cmdq: make(chan cmd, 128), stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan State), fatalError: make(chan error, 1),
dirty: make(chan struct{}, 1),
fatalError: make(chan error, 1),
} }
m.wg.Add(1) m.wg.Add(1)
@@ -85,7 +83,6 @@ func (m *Manager) waylandActor() {
func (m *Manager) setupRegistry() error { func (m *Manager) setupRegistry() error {
log.Info("WlrOutput: starting registry setup") log.Info("WlrOutput: starting registry setup")
ctx := m.display.Context()
registry, err := m.display.GetRegistry() registry, err := m.display.GetRegistry()
if err != nil { if err != nil {
@@ -96,7 +93,7 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName { if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
log.Infof("WlrOutput: found %s", wlr_output_management.ZwlrOutputManagerV1InterfaceName) log.Infof("WlrOutput: found %s", wlr_output_management.ZwlrOutputManagerV1InterfaceName)
manager := wlr_output_management.NewZwlrOutputManagerV1(ctx) manager := wlr_output_management.NewZwlrOutputManagerV1(m.ctx)
version := e.Version version := e.Version
if version > 4 { if version > 4 {
version = 4 version = 4
@@ -143,9 +140,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
modeIDs: make([]uint32, 0), modeIDs: make([]uint32, 0),
} }
m.headsMutex.Lock() m.heads.Store(headID, head)
m.heads[headID] = head
m.headsMutex.Unlock()
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name) log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
@@ -254,9 +249,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
log.Debugf("WlrOutput: Head %d finished", headID) log.Debugf("WlrOutput: Head %d finished", headID)
head.finished = true head.finished = true
m.headsMutex.Lock() m.heads.Delete(headID)
delete(m.heads, headID)
m.headsMutex.Unlock()
m.post(func() { m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -279,15 +272,12 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
handle: handle, handle: handle,
} }
m.modesMutex.Lock() m.modes.Store(modeID, mode)
m.modes[modeID] = mode
m.modesMutex.Unlock()
m.headsMutex.Lock() if head, ok := m.heads.Load(headID); ok {
if head, ok := m.heads[headID]; ok {
head.modeIDs = append(head.modeIDs, modeID) head.modeIDs = append(head.modeIDs, modeID)
m.heads.Store(headID, head)
} }
m.headsMutex.Unlock()
handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) { handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height) log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height)
@@ -318,9 +308,7 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
log.Debugf("WlrOutput: Mode %d finished", modeID) log.Debugf("WlrOutput: Mode %d finished", modeID)
mode.finished = true mode.finished = true
m.modesMutex.Lock() m.modes.Delete(modeID)
delete(m.modes, modeID)
m.modesMutex.Unlock()
m.post(func() { m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -333,22 +321,22 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.headsMutex.RLock()
m.modesMutex.RLock()
outputs := make([]Output, 0) outputs := make([]Output, 0)
for _, head := range m.heads { m.heads.Range(func(key uint32, head *headState) bool {
if head.finished { if head.finished {
continue return true
} }
modes := make([]OutputMode, 0) modes := make([]OutputMode, 0)
var currentMode *OutputMode var currentMode *OutputMode
for _, modeID := range head.modeIDs { for _, modeID := range head.modeIDs {
mode, exists := m.modes[modeID] mode, exists := m.modes.Load(modeID)
if !exists || mode.finished { if !exists {
continue
}
if mode.finished {
continue continue
} }
@@ -385,10 +373,8 @@ func (m *Manager) updateState() {
ID: head.id, ID: head.id,
} }
outputs = append(outputs, output) outputs = append(outputs, output)
} return true
})
m.modesMutex.RUnlock()
m.headsMutex.RUnlock()
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -442,14 +428,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -458,15 +436,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("WlrOutput: subscriber channel full, dropping update") log.Warn("WlrOutput: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -480,30 +457,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.modesMutex.Lock() m.modes.Range(func(key uint32, mode *modeState) bool {
for _, mode := range m.modes {
if mode.handle != nil { if mode.handle != nil {
mode.handle.Release() mode.handle.Release()
} }
} m.modes.Delete(key)
m.modes = make(map[uint32]*modeState) return true
m.modesMutex.Unlock() })
m.headsMutex.Lock() m.heads.Range(func(key uint32, head *headState) bool {
for _, head := range m.heads {
if head.handle != nil { if head.handle != nil {
head.handle.Release() head.handle.Release()
} }
} m.heads.Delete(key)
m.heads = make(map[uint32]*headState) return true
m.headsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()

View File

@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type OutputMode struct { type OutputMode struct {
@@ -45,14 +46,12 @@ type cmd struct {
type Manager struct { type Manager struct {
display *wlclient.Display display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry registry *wlclient.Registry
manager *wlr_output_management.ZwlrOutputManagerV1 manager *wlr_output_management.ZwlrOutputManagerV1
headsMutex sync.RWMutex heads syncmap.Map[uint32, *headState]
heads map[uint32]*headState modes syncmap.Map[uint32, *modeState]
modesMutex sync.RWMutex
modes map[uint32]*modeState
serial uint32 serial uint32
@@ -61,8 +60,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -119,19 +117,19 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -0,0 +1,3 @@
// Keep this sorted
rajveermalviya

View File

@@ -0,0 +1,24 @@
Copyright 2021 go-wayland authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,25 @@
# Wayland implementation in Go
[![Go Reference](https://pkg.go.dev/badge/github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland.svg)](https://pkg.go.dev/github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland)
This module contains pure Go implementation of the Wayland protocol.
Currently only wayland-client functionality is supported.
Go code is generated from protocol XML files using
[`go-wayland-scanner`](cmd/go-wayland-scanner/scanner.go).
To load cursor, minimal port of `wayland-cursor` & `xcursor` in pure Go
is located at [`wayland/cursor`](wayland/cursor) & [`wayland/cursor/xcursor`](wayland/cursor/xcursor)
respectively.
To demonstrate the functionality of this module
[`examples/imageviewer`](examples/imageviewer) contains a simple image
viewer. It demos displaying a top-level window, resizing of window,
cursor themes, pointer and keyboard. Because it's in pure Go, it can be
compiled without CGO. You can try it using the following commands:
```sh
CGO_ENABLED=0 go install github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/examples/imageviewer@latest
imageviewer file.jpg
```

4
core/pkg/go-wayland/generate Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
cd ./wayland
go generate -x ./...

9
core/pkg/go-wayland/generatep Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
# Runs go generate for each directory, but in parallel. Any arguments are appended to the
# go generate command.
# Usage: $ ./generatep [go generate arguments]
# Print all generate commands: $ ./generatep -x
cd ./wayland
find . -type f -name '*.go' -exec dirname {} \; | sort -u | parallel -j 0 go generate $1 {}/.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
package client
type Dispatcher interface {
Dispatch(opcode uint32, fd int, data []byte)
}
type Proxy interface {
Context() *Context
SetContext(ctx *Context)
ID() uint32
SetID(id uint32)
}
type BaseProxy struct {
ctx *Context
id uint32
}
func (p *BaseProxy) ID() uint32 {
return p.id
}
func (p *BaseProxy) SetID(id uint32) {
p.id = id
}
func (p *BaseProxy) Context() *Context {
return p.ctx
}
func (p *BaseProxy) SetContext(ctx *Context) {
p.ctx = ctx
}

View File

@@ -0,0 +1,112 @@
package client
import (
"errors"
"fmt"
"net"
"os"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Context struct {
conn *net.UnixConn
objects syncmap.Map[uint32, Proxy] // map[uint32]Proxy - thread-safe concurrent map
currentID uint32
idMu sync.Mutex // protects currentID increment
}
func (ctx *Context) Register(p Proxy) {
ctx.idMu.Lock()
ctx.currentID++
id := ctx.currentID
ctx.idMu.Unlock()
p.SetID(id)
p.SetContext(ctx)
ctx.objects.Store(id, p)
}
func (ctx *Context) Unregister(p Proxy) {
ctx.objects.Delete(p.ID())
}
func (ctx *Context) GetProxy(id uint32) Proxy {
if val, ok := ctx.objects.Load(id); ok {
return val
}
return nil
}
func (ctx *Context) Close() error {
return ctx.conn.Close()
}
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
// respective wayland protocol.
// Dispatch must be called on the same goroutine as other interactions with the Context.
// If a multi goroutine approach is desired, use [Context.GetDispatch] instead.
// Dispatch blocks if there are no incoming messages.
// A Dispatch loop is usually used to handle incoming messages.
func (ctx *Context) Dispatch() error {
return ctx.GetDispatch()()
}
var ErrDispatchSenderNotFound = errors.New("dispatch: unable to find sender")
var ErrDispatchSenderUnsupported = errors.New("dispatch: sender does not implement Dispatch method")
var ErrDispatchUnableToReadMsg = errors.New("dispatch: unable to read msg")
// GetDispatch reads incoming messages and returns the dispatch function which calls
// [client.Dispatcher.Dispatch] on the respective wayland protocol.
// This function is now thread-safe and can be called from multiple goroutines.
// GetDispatch blocks if there are no incoming messages.
func (ctx *Context) GetDispatch() func() error {
senderID, opcode, fd, data, err := ctx.ReadMsg() // Blocks if there are no incoming messages
if err != nil {
return func() error {
return fmt.Errorf("%w: %w", ErrDispatchUnableToReadMsg, err)
}
}
return func() error {
proxy, ok := ctx.objects.Load(senderID)
if !ok {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderNotFound, senderID)
}
sender, ok := proxy.(Dispatcher)
if !ok {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderUnsupported, senderID)
}
sender.Dispatch(opcode, fd, data)
return nil
}
}
func Connect(addr string) (*Display, error) {
if addr == "" {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
return nil, errors.New("env XDG_RUNTIME_DIR not set")
}
if addr == "" {
addr = os.Getenv("WAYLAND_DISPLAY")
}
if addr == "" {
addr = "wayland-0"
}
addr = runtimeDir + "/" + addr
}
ctx := &Context{}
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: addr, Net: "unix"})
if err != nil {
return nil, err
}
ctx.conn = conn
return NewDisplay(ctx), nil
}

View File

@@ -0,0 +1,111 @@
package client_test
import (
"errors"
"fmt"
"log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
// Shows a dispatch loop that will block the goroutine.
// This approach has no risk of data races but the loop blocks the goroutine when no messages are
// received. This can be a valid approach if there are no more changes that need to be made after
// setting up and starting the loop.
// For a multi goroutine approach, use [client.Context.GetDispatch].
func ExampleContext_Dispatch() {
display, err := client.Connect("")
if err != nil {
log.Fatalf("Error connecting to Wayland server: %v", err)
}
registry, err := display.GetRegistry()
if err != nil {
log.Fatalf("Error getting Wayland registry: %v", err)
}
var seat *client.Seat
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.SeatInterfaceName:
seat = client.NewSeat(display.Context())
err := registry.Bind(e.Name, e.Interface, e.Version, seat)
if err != nil {
log.Fatalf("unable to bind %s interface: %v", client.SeatInterfaceName, err)
}
}
})
display.Roundtrip()
display.Roundtrip()
keyboard, err := seat.GetKeyboard()
if err != nil {
log.Printf("Error getting keyboard: %v", err)
}
log.Printf("Got keyboard: %v\n", keyboard)
for {
err := display.Context().Dispatch()
if err != nil {
log.Printf("Dispatch error: %v\n", err)
}
}
}
// Shows how the dispatch loop can be done in another goroutine.
// This prevents the goroutine from being blocked and allows making changes to wayland objects while
// the dispatch loop is blocking another goroutine.
func ExampleContext_GetDispatch() {
display, err := client.Connect("")
if err != nil {
log.Fatalf("Error connecting to Wayland server: %v", err)
}
registry, err := display.GetRegistry()
if err != nil {
log.Fatalf("Error getting Wayland registry: %v", err)
}
var seat *client.Seat
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.SeatInterfaceName:
seat = client.NewSeat(display.Context())
err := registry.Bind(e.Name, e.Interface, e.Version, seat)
if err != nil {
log.Fatalf("unable to bind %s interface: %v", client.SeatInterfaceName, err)
}
}
})
display.Roundtrip()
display.Roundtrip()
dispatchQueue := make(chan func() error)
go func() {
for {
dispatchQueue <- display.Context().GetDispatch()
}
}()
keyboard, err := seat.GetKeyboard()
if err != nil {
log.Printf("Error getting keyboard: %v", err)
}
log.Printf("Got keyboard: %v\n", keyboard)
err = errors.Join(keyboard.Release(), seat.Release(), display.Context().Close())
if err != nil {
fmt.Printf("Error cleaning up: %v\n", err)
}
for {
select {
// Add other cases here to do other things
case dispatchFunc := <-dispatchQueue:
err := dispatchFunc()
if err != nil {
log.Printf("Dispatch error: %v\n", err)
}
}
}
}

View File

@@ -0,0 +1,37 @@
package client
import (
"fmt"
"log"
)
// Roundtrip blocks until all pending request are processed by the server.
// It is the implementation of [wl_display_roundtrip].
//
// [wl_display_roundtrip]: https://wayland.freedesktop.org/docs/html/apb.html#Client-classwl__display_1ab60f38c2f80980ac84f347e932793390
func (i *Display) Roundtrip() error {
callback, err := i.Sync()
if err != nil {
return fmt.Errorf("unable to get sync callback: %w", err)
}
defer func() {
if err2 := callback.Destroy(); err2 != nil {
log.Printf("unable to destroy callback: %v\n", err2)
}
}()
done := false
callback.SetDoneHandler(func(_ CallbackDoneEvent) {
done = true
})
// Wait for callback to return
for !done {
err := i.Context().GetDispatch()()
if err != nil {
return fmt.Errorf("roundtrip: failed to dispatch: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,6 @@
// Package client is Go port of wayland-client library
// for writing pure Go GUI software for wayland supported
// platforms.
package client
//go:generate go run github.com/yaslama/go-wayland/cmd/go-wayland-scanner -pkg client -prefix wl -o client.go -i https://gitlab.freedesktop.org/wayland/wayland/-/raw/1.23.0/protocol/wayland.xml?ref_type=tags

View File

@@ -0,0 +1,120 @@
package client
import (
"bytes"
"fmt"
"unsafe"
"golang.org/x/sys/unix"
_ "unsafe"
)
var oobSpace = unix.CmsgSpace(4)
func (ctx *Context) ReadMsg() (senderID uint32, opcode uint32, fd int, msg []byte, err error) {
fd = -1
oob := make([]byte, oobSpace)
header := make([]byte, 8)
n, oobn, _, _, err := ctx.conn.ReadMsgUnix(header, oob)
if err != nil {
return senderID, opcode, fd, msg, err
}
if n != 8 {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: incorrect number of bytes read for header (n=%d)", n)
}
if oobn > 0 {
fds, err := getFdsFromOob(oob, oobn, "header")
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if len(fds) > 0 {
fd = fds[0]
}
}
senderID = Uint32(header[:4])
opcodeAndSize := Uint32(header[4:8])
opcode = opcodeAndSize & 0xffff
size := opcodeAndSize >> 16
msgSize := int(size) - 8
if msgSize == 0 {
return senderID, opcode, fd, nil, nil
}
msg = make([]byte, msgSize)
if fd == -1 {
// if something was read before, then zero it out
if oobn > 0 {
oob = make([]byte, oobSpace)
}
n, oobn, _, _, err = ctx.conn.ReadMsgUnix(msg, oob)
} else {
n, err = ctx.conn.Read(msg)
}
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if n != msgSize {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: incorrect number of bytes read for msg (n=%d, msgSize=%d)", n, msgSize)
}
if fd == -1 && oobn > 0 {
fds, err := getFdsFromOob(oob, oobn, "msg")
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if len(fds) > 0 {
fd = fds[0]
}
}
return senderID, opcode, fd, msg, nil
}
func getFdsFromOob(oob []byte, oobn int, source string) ([]int, error) {
if oobn > len(oob) {
return nil, fmt.Errorf("getFdsFromOob: incorrect number of bytes read from %s for oob (oobn=%d)", source, oobn)
}
scms, err := unix.ParseSocketControlMessage(oob)
if err != nil {
return nil, fmt.Errorf("getFdsFromOob: unable to parse control message from %s: %w", source, err)
}
var fdsRet []int
for _, scm := range scms {
fds, err := unix.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("getFdsFromOob: unable to parse unix rights from %s: %w", source, err)
}
fdsRet = append(fdsRet, fds...)
}
return fdsRet, nil
}
func Uint32(src []byte) uint32 {
_ = src[3]
return *(*uint32)(unsafe.Pointer(&src[0]))
}
func String(src []byte) string {
idx := bytes.IndexByte(src, 0)
src = src[:idx:idx]
return *(*string)(unsafe.Pointer(&src))
}
func Fixed(src []byte) float64 {
_ = src[3]
fx := *(*int32)(unsafe.Pointer(&src[0]))
return fixedToFloat64(fx)
}

View File

@@ -0,0 +1,44 @@
package client
import (
"fmt"
"unsafe"
)
func (ctx *Context) WriteMsg(b []byte, oob []byte) error {
n, oobn, err := ctx.conn.WriteMsgUnix(b, oob, nil)
if err != nil {
return err
}
if n != len(b) || oobn != len(oob) {
return fmt.Errorf("ctx.WriteMsg: incorrect number of bytes written (n=%d oobn=%d)", n, oobn)
}
return nil
}
func PutUint32(dst []byte, v uint32) {
_ = dst[3]
*(*uint32)(unsafe.Pointer(&dst[0])) = v
}
func PutFixed(dst []byte, f float64) {
fx := fixedFromfloat64(f)
_ = dst[3]
*(*int32)(unsafe.Pointer(&dst[0])) = fx
}
// PutString places a string in Wayland's wire format on the destination buffer.
// It first places the length of the string (plus one for the null terminator) and then the string
// followed by a null byte.
// The length of dst must be equal to, or greater than, len(v) + 5.
func PutString(dst []byte, v string) {
PutUint32(dst[:4], uint32(len(v)+1))
copy(dst[4:], v)
dst[4+len(v)] = '\x00' // To cause panic if dst is not large enough
}
func PutArray(dst []byte, a []byte) {
PutUint32(dst[:4], uint32(len(a)))
copy(dst[4:], a)
}

View File

@@ -0,0 +1,24 @@
package client
import "math"
// From wayland/wayland-util.h
func fixedToFloat64(f int32) float64 {
u_i := (1023+44)<<52 + (1 << 51) + int64(f)
u_d := math.Float64frombits(uint64(u_i))
return u_d - (3 << 43)
}
func fixedFromfloat64(d float64) int32 {
u_d := d + (3 << (51 - 8))
u_i := int64(math.Float64bits(u_d))
return int32(u_i)
}
func PaddedLen(l int) int {
if (l & 0x3) != 0 {
return l + (4 - (l & 0x3))
}
return l
}

28
core/pkg/syncmap/LICENSE Normal file
View File

@@ -0,0 +1,28 @@
Copyright 2009 The Go Authors.
Copyright 2024 Zachary Olstein.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

537
core/pkg/syncmap/syncmap.go Normal file
View File

@@ -0,0 +1,537 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package syncmap
import (
"sync"
"sync/atomic"
"unsafe"
)
// Map is like a Go map[K]V but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate [Mutex] or [RWMutex].
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
//
// In the terminology of [the Go memory model], Map arranges that a write operation
// “synchronizes before” any read operation that observes the effect of the write, where
// read and write operations are defined as follows.
// [Map.Load], [Map.LoadAndDelete], [Map.LoadOrStore], and [Map.Swap] are read operations;
// [Map.Delete], [Map.LoadAndDelete], [Map.Store], and [Map.Swap] are write operations;
// [Map.LoadOrStore] is a write operation when it returns loaded set to false.
//
// [the Go memory model]: https://go.dev/ref/mem
type Map[K comparable, V any] struct {
mu sync.Mutex
// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Pointer[readOnly[K, V]]
// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[K]*entry[V]
// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly[K comparable, V any] struct {
m map[K]*entry[V]
amended bool // true if the dirty map contains some key not in m.
}
// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
// Because the same expunged pointer is used regardless of the Map's value type,
// value pointers read from the map must be compared against expunged BEFORE
// casting the pointer to *V.
var expunged = unsafe.Pointer(new(int))
// An entry is a slot in the map corresponding to a particular key.
type entry[V any] struct {
// p points to the value stored for the entry.
//
// If p == nil, the entry has been deleted, and either m.dirty == nil or
// m.dirty[key] is e.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// If p != expunged, it is always safe to cast it to (*V).
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer
}
func newEntry[V any](i V) *entry[V] {
e := &entry[V]{}
atomic.StorePointer(&e.p, unsafe.Pointer(&i))
return e
}
func (m *Map[K, V]) loadReadOnly() readOnly[K, V] {
if p := m.read.Load(); p != nil {
return *p
}
return readOnly[K, V]{}
}
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map[K, V]) Load(key K) (value V, ok bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return value, false
}
return e.load()
}
func (e *entry[V]) load() (value V, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return value, false
}
return *(*V)(p), true
}
// Store sets the value for a key.
func (m *Map[K, V]) Store(key K, value V) {
_, _ = m.Swap(key, value)
}
// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
// before m.mu is unlocked.
func (e *entry[V]) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// swapLocked unconditionally swaps a value into the entry.
//
// The entry must be known not to be expunged.
func (e *entry[V]) swapLocked(i *V) *V {
return (*V)(atomic.SwapPointer(&e.p, unsafe.Pointer(i)))
}
// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
// Avoid locking if it's a clean hit.
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
}
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
} else if e, ok := m.dirty[key]; ok {
actual, loaded, _ = e.tryLoadOrStore(value)
m.missLocked()
} else {
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
m.read.Store(&readOnly[K, V]{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
actual, loaded = value, false
}
m.mu.Unlock()
return actual, loaded
}
// tryLoadOrStore atomically loads or stores a value if the entry is not
// expunged.
//
// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
// returns with ok==false.
func (e *entry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) {
ptr := atomic.LoadPointer(&e.p)
if ptr == expunged {
return actual, false, false
}
p := (*V)(ptr)
if p != nil {
return *p, true, true
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
ptr = atomic.LoadPointer(&e.p)
if ptr == expunged {
return actual, false, false
}
p = (*V)(ptr)
if p != nil {
return *p, true, true
}
}
}
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
delete(m.dirty, key)
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return value, false
}
// Delete deletes the value for a key.
func (m *Map[K, V]) Delete(key K) {
m.LoadAndDelete(key)
}
func (e *entry[V]) delete() (value V, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return value, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*V)(p), true
}
}
}
// trySwap swaps a value if the entry has not been expunged.
//
// If the entry is expunged, trySwap returns false and leaves the entry
// unchanged.
func (e *entry[V]) trySwap(i *V) (*V, bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return (*V)(p), true
}
}
}
// Swap swaps the value for a key and returns the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map[K, V]) Swap(key K, value V) (previous V, loaded bool) {
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return previous, false
}
return *v, true
}
}
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else if e, ok := m.dirty[key]; ok {
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else {
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
m.read.Store(&readOnly[K, V]{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
return previous, loaded
}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
//
// Range does not necessarily correspond to any consistent snapshot of the Map's
// contents: no key will be visited more than once, but if the value for any key
// is stored or deleted concurrently (including by f), Range may reflect any
// mapping for that key from any point during the Range call. Range does not
// block other methods on the receiver; even f itself may call any method on m.
//
// Range may be O(N) with the number of elements in the map even if f returns
// false after a constant number of calls.
func (m *Map[K, V]) Range(f func(key K, value V) bool) {
// We need to be able to iterate over all of the keys that were already
// present at the start of the call to Range.
// If read.amended is false, then read.m satisfies that property without
// requiring us to hold m.mu for a long time.
read := m.loadReadOnly()
if read.amended {
// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
// (assuming the caller does not break out early), so a call to Range
// amortizes an entire copy of the map: we can promote the dirty copy
// immediately!
m.mu.Lock()
read = m.loadReadOnly()
if read.amended {
read = readOnly[K, V]{m: m.dirty}
copyRead := read
m.read.Store(&copyRead)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
// CompareAndSwap swaps the old and new values for key
// if the value stored in the map is equal to old.
// The old value must be of a comparable type.
func CompareAndSwap[K comparable, V comparable](m *Map[K, V], key K, old, new V) (swapped bool) {
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
return tryCompareAndSwap(e, old, new)
} else if !read.amended {
return false // No existing value for key.
}
m.mu.Lock()
defer m.mu.Unlock()
read = m.loadReadOnly()
swapped = false
if e, ok := read.m[key]; ok {
swapped = tryCompareAndSwap(e, old, new)
} else if e, ok := m.dirty[key]; ok {
swapped = tryCompareAndSwap(e, old, new)
// We needed to lock mu in order to load the entry for key,
// and the operation didn't change the set of keys in the map
// (so it would be made more efficient by promoting the dirty
// map to read-only).
// Count it as a miss so that we will eventually switch to the
// more efficient steady state.
m.missLocked()
}
return swapped
}
// CompareAndDelete deletes the entry for key if its value is equal to old.
// The old value must be of a comparable type.
//
// If there is no current value for key in the map, CompareAndDelete
// returns false (even if the old value is the zero value of V).
func CompareAndDelete[K comparable, V comparable](m *Map[K, V], key K, old V) (deleted bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Don't delete key from m.dirty: we still need to do the “compare” part
// of the operation. The entry will eventually be expunged when the
// dirty map is promoted to the read map.
//
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
for ok {
ptr := atomic.LoadPointer(&e.p)
if ptr == nil || ptr == expunged {
return false
}
p := (*V)(ptr)
if *p != old {
return false
}
if atomic.CompareAndSwapPointer(&e.p, ptr, nil) {
return true
}
}
return false
}
// tryCompareAndSwap compare the entry with the given old value and swaps
// it with a new value if the entry is equal to the old value, and the entry
// has not been expunged.
//
// If the entry is expunged, tryCompareAndSwap returns false and leaves
// the entry unchanged.
func tryCompareAndSwap[V comparable](e *entry[V], old, new V) bool {
ptr := atomic.LoadPointer(&e.p)
if ptr == nil || ptr == expunged {
return false
}
p := (*V)(ptr)
if *p != old {
return false
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if the comparison fails from the start, we shouldn't
// bother heap-allocating an interface value to store.
nc := new
for {
if atomic.CompareAndSwapPointer(&e.p, ptr, unsafe.Pointer(&nc)) {
return true
}
ptr = atomic.LoadPointer(&e.p)
if ptr == nil || ptr == expunged {
return false
}
p = (*V)(ptr)
if *p != old {
return false
}
}
}
func (m *Map[K, V]) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(&readOnly[K, V]{m: m.dirty})
m.dirty = nil
m.misses = 0
}
func (m *Map[K, V]) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
m.dirty = make(map[K]*entry[V], len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry[V]) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}

View File

@@ -14,7 +14,7 @@
export PATH=$PATH:${lib.makeBinPath [ cfg.quickshell.package config.programs.${cfg.compositor.name}.package ]} export PATH=$PATH:${lib.makeBinPath [ cfg.quickshell.package config.programs.${cfg.compositor.name}.package ]}
${lib.escapeShellArgs ([ ${lib.escapeShellArgs ([
"sh" "sh"
"${../quickshell/Modules/Greetd/assets/dms-greeter}" "${../../quickshell/Modules/Greetd/assets/dms-greeter}"
"--cache-dir" "--cache-dir"
"/var/lib/dmsgreeter" "/var/lib/dmsgreeter"
"--command" "--command"

32
dms-bin.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Build script wrapper for dms-cli core binaries
# Forwards make commands to core/Makefile
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CORE_DIR="$SCRIPT_DIR/core"
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Check if core directory exists
if [ ! -d "$CORE_DIR" ]; then
echo "Error: core directory not found at $CORE_DIR"
exit 1
fi
# If no arguments provided, build and install
if [ $# -eq 0 ]; then
echo -e "${BLUE}Building and installing DMS CLI binary...${NC}"
cd "$CORE_DIR"
make && sudo make install
exit 0
fi
# Forward all arguments to make in core directory
echo -e "${GREEN}Building in core directory...${NC}"
cd "$CORE_DIR"
make "$@"

View File

@@ -0,0 +1,43 @@
{
"dark": {
"name": "Everforest dark, hard",
"primary": "#7fbbb3",
"primaryText": "#2b3339",
"primaryContainer": "#83c092",
"secondary": "#d699b6",
"surface": "#323c41",
"surfaceText": "#9da9a0",
"surfaceVariant": "#503946",
"surfaceVariantText": "#edeada",
"surfaceTint": "#7fbbb3",
"background": "#2b3339",
"backgroundText": "#fffbef",
"outline": "#9da9a0",
"surfaceContainer": "#503946",
"surfaceContainerHigh": "#859289",
"error": "#e67e80",
"warning": "#e69875",
"info": "#83c092"
},
"light": {
"name": "Everforest light, hard",
"primary": "#3a94c5",
"primaryText": "#d3c6aa",
"primaryContainer": "#35a77c",
"secondary": "#df69ba",
"surface": "#5c6a72",
"surfaceText": "#939f91",
"surfaceVariant": "#fffbef",
"surfaceVariantText": "#5c6a72",
"surfaceTint": "#3a94c5",
"background": "#fffbef",
"backgroundText": "#2b3339",
"outline": "#939f91",
"surfaceContainer": "#fffbef",
"surfaceContainerHigh": "#939f91",
"error": "#f85552",
"warning": "#f57d26",
"info": "#35a77c"
}
}

42
docs/theme_nord.json Normal file
View File

@@ -0,0 +1,42 @@
{
"dark": {
"name": "Nord dark",
"primary": "#81a1c1",
"primaryText": "#2e3440",
"primaryContainer": "#88c0d0",
"secondary": "#b48ead",
"surface": "#3b4252",
"surfaceText": "#5e81ac",
"surfaceVariant": "#434c5e",
"surfaceVariantText": "#eceff4",
"surfaceTint": "#81a1c1",
"background": "#2e3440",
"backgroundText": "#8fbcbb",
"outline": "#d8dee9",
"surfaceContainer": "#434c5e",
"surfaceContainerHigh": "#4c566a",
"error": "#bf616a",
"warning": "#d08770",
"info": "#88c0d0"
},
"light": {
"name": "Nord light",
"primary": "#3b6ea8",
"primaryText": "#e5e9f0",
"primaryContainer": "#398eac",
"secondary": "#97365b",
"surface": "#c2d0e7",
"surfaceText": "#5272af",
"surfaceVariant": "#b8c5db",
"surfaceVariantText": "#3b4252",
"surfaceTint": "#3b6ea8",
"background": "#e5e9f0",
"backgroundText": "#29838d",
"outline": "#60728c",
"surfaceContainer": "#b8c5db",
"surfaceContainerHigh": "#aebacf",
"error": "#99324b",
"warning": "#ac4426",
"info": "#398eac"
}
}

42
docs/theme_rose-pine.json Normal file
View File

@@ -0,0 +1,42 @@
{
"dark": {
"name": "Rosé Pine",
"primary": "#c4a7e7",
"primaryText": "#191724",
"primaryContainer": "#9ccfd8",
"secondary": "#f6c177",
"surface": "#1f1d2e",
"surfaceText": "#524f67",
"surfaceVariant": "#26233a",
"surfaceVariantText": "#e0def4",
"surfaceTint": "#c4a7e7",
"background": "#191724",
"backgroundText": "#524f67",
"outline": "#908caa",
"surfaceContainer": "#26233a",
"surfaceContainerHigh": "#6e6a86",
"error": "#eb6f92",
"warning": "#f6c177",
"info": "#9ccfd8"
},
"light": {
"name": "Rosé Pine dawn",
"primary": "#907aa9",
"primaryText": "#faf4ed",
"primaryContainer": "#56949f",
"secondary": "#ea9d34",
"surface": "#fffaf3",
"surfaceText": "#cecacd",
"surfaceVariant": "#f2e9de",
"surfaceVariantText": "#575279",
"surfaceTint": "#907aa9",
"background": "#faf4ed",
"backgroundText": "#cecacd",
"outline": "#797593",
"surfaceContainer": "#f2e9de",
"surfaceContainerHigh": "#9893a5",
"error": "#b4637a",
"warning": "#ea9d34",
"info": "#56949f"
}
}

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