mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 07:22:50 -05:00
Compare commits
68 Commits
v0.5.1
...
wip/bar-ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a68ce35a3 | ||
|
|
eb4655fcbc | ||
|
|
6eb349c9d4 | ||
|
|
0a8a7895b3 | ||
|
|
73c82a4dd9 | ||
|
|
ccf28fc4e7 | ||
|
|
64ec5be919 | ||
|
|
3916512d66 | ||
|
|
e2f426a1bd | ||
|
|
aa1df8dfcf | ||
|
|
67557555f2 | ||
|
|
4cb652abd9 | ||
|
|
d11868b99f | ||
|
|
1798417e6a | ||
|
|
43dc3e5bb1 | ||
|
|
91891a14ed | ||
|
|
20f7d60147 | ||
|
|
7e17e7d37a | ||
|
|
cbb244f785 | ||
|
|
1c264d858b | ||
|
|
217037c2ae | ||
|
|
b4dbd0b69c | ||
|
|
89a2b5c00b | ||
|
|
929b6dae1a | ||
|
|
52fe493da9 | ||
|
|
3e6be3e762 | ||
|
|
7a8cc449b9 | ||
|
|
8f5a9d6e9f | ||
|
|
1c5e31fea9 | ||
|
|
fd08ae18ab | ||
|
|
a7eb3de06e | ||
|
|
8902dd7c44 | ||
|
|
6387d8400c | ||
|
|
597cacb9cc | ||
|
|
3e285ad9ff | ||
|
|
cc1fa89790 | ||
|
|
b0ed007751 | ||
|
|
e1e2650d2b | ||
|
|
b23f17b633 | ||
|
|
818e40b2df | ||
|
|
5685e39631 | ||
|
|
72534b7674 | ||
|
|
328490d23d | ||
|
|
97a0696930 | ||
|
|
cb4e0660e0 | ||
|
|
67c642de4c | ||
|
|
0d7c2e1024 | ||
|
|
16a779a41b | ||
|
|
c4ca3c8644 | ||
|
|
aabcbe34f3 | ||
|
|
f06626e441 | ||
|
|
c4e1a71776 | ||
|
|
77e6c16bd2 | ||
|
|
9d1fac3570 | ||
|
|
b7aeaa7fc5 | ||
|
|
f6d8c9ff61 | ||
|
|
0490794d6c | ||
|
|
335c83dd3c | ||
|
|
91da720c26 | ||
|
|
b6ac744a68 | ||
|
|
526c4092fd | ||
|
|
ed06dda384 | ||
|
|
6465b11e9b | ||
|
|
b2879878a1 | ||
|
|
3e17b086fb | ||
|
|
0545e6bcda | ||
|
|
27a907433f | ||
|
|
69616800e3 |
25
.github/workflows/copr-release.yml
vendored
25
.github/workflows/copr-release.yml
vendored
@@ -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
|
||||||
|
|||||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
48
core/.mockery.yml
Normal 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:
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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...")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
43
core/go.mod
43
core/go.mod
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
94
core/go.sum
94
core/go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package logger
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
295
core/internal/mocks/evdev/mock_EvdevDevice.go
Normal file
295
core/internal/mocks/evdev/mock_EvdevDevice.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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].
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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].
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) {
|
||||||
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()
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
27
core/internal/server/evdev/handlers.go
Normal file
27
core/internal/server/evdev/handlers.go
Normal 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)
|
||||||
|
}
|
||||||
130
core/internal/server/evdev/handlers_test.go
Normal file
130
core/internal/server/evdev/handlers_test.go
Normal 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)
|
||||||
|
}
|
||||||
404
core/internal/server/evdev/manager.go
Normal file
404
core/internal/server/evdev/manager.go
Normal 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
|
||||||
|
}
|
||||||
344
core/internal/server/evdev/manager_test.go
Normal file
344
core/internal/server/evdev/manager_test.go
Normal 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)
|
||||||
|
}
|
||||||
6
core/internal/server/evdev/models.go
Normal file
6
core/internal/server/evdev/models.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package evdev
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
CapsLock bool `json:"capsLock"`
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) {
|
||||||
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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) {
|
||||||
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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
3
core/pkg/go-wayland/AUTHORS
Normal file
3
core/pkg/go-wayland/AUTHORS
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Keep this sorted
|
||||||
|
|
||||||
|
rajveermalviya
|
||||||
24
core/pkg/go-wayland/LICENSE
Normal file
24
core/pkg/go-wayland/LICENSE
Normal 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.
|
||||||
25
core/pkg/go-wayland/README.md
Normal file
25
core/pkg/go-wayland/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Wayland implementation in Go
|
||||||
|
|
||||||
|
[](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
4
core/pkg/go-wayland/generate
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd ./wayland
|
||||||
|
go generate -x ./...
|
||||||
9
core/pkg/go-wayland/generatep
Executable file
9
core/pkg/go-wayland/generatep
Executable 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 {}/.
|
||||||
7544
core/pkg/go-wayland/wayland/client/client.go
Normal file
7544
core/pkg/go-wayland/wayland/client/client.go
Normal file
File diff suppressed because it is too large
Load Diff
33
core/pkg/go-wayland/wayland/client/common.go
Normal file
33
core/pkg/go-wayland/wayland/client/common.go
Normal 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
|
||||||
|
}
|
||||||
112
core/pkg/go-wayland/wayland/client/context.go
Normal file
112
core/pkg/go-wayland/wayland/client/context.go
Normal 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
|
||||||
|
}
|
||||||
111
core/pkg/go-wayland/wayland/client/context_test.go
Normal file
111
core/pkg/go-wayland/wayland/client/context_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
core/pkg/go-wayland/wayland/client/display.go
Normal file
37
core/pkg/go-wayland/wayland/client/display.go
Normal 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
|
||||||
|
}
|
||||||
6
core/pkg/go-wayland/wayland/client/doc.go
Normal file
6
core/pkg/go-wayland/wayland/client/doc.go
Normal 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
|
||||||
120
core/pkg/go-wayland/wayland/client/event.go
Normal file
120
core/pkg/go-wayland/wayland/client/event.go
Normal 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)
|
||||||
|
}
|
||||||
44
core/pkg/go-wayland/wayland/client/request.go
Normal file
44
core/pkg/go-wayland/wayland/client/request.go
Normal 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)
|
||||||
|
}
|
||||||
24
core/pkg/go-wayland/wayland/client/util.go
Normal file
24
core/pkg/go-wayland/wayland/client/util.go
Normal 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
28
core/pkg/syncmap/LICENSE
Normal 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
537
core/pkg/syncmap/syncmap.go
Normal 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(©Read)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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
32
dms-bin.sh
Executable 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 "$@"
|
||||||
43
docs/theme_everforest.json
Normal file
43
docs/theme_everforest.json
Normal 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
42
docs/theme_nord.json
Normal 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
42
docs/theme_rose-pine.json
Normal 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
Reference in New Issue
Block a user