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

Compare commits

...

53 Commits

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

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

* style: fmt
2025-12-08 11:16:43 -05:00
Marcus Ramberg ddb74b598d ci: add flake check (#951) 2025-12-08 11:15:35 -05:00
bbedward 29571fc3aa screenshot: use wlr-output-management on DWL for x/y offsets 2025-12-08 10:53:08 -05:00
125 changed files with 4512 additions and 3533 deletions
+23
View File
@@ -0,0 +1,23 @@
name: Check nix flake
on:
pull_request:
branches: [master, main]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Check the flake
run: nix flake check
+3 -28
View File
@@ -511,7 +511,7 @@ jobs:
Requires: (quickshell or quickshell-git) Requires: (quickshell or quickshell-git)
Requires: accountsservice Requires: accountsservice
Requires: dms-cli Requires: dms-cli = %{version}-%{release}
Requires: dgop Requires: dgop
Recommends: cava Recommends: cava
@@ -541,17 +541,6 @@ jobs:
Command-line interface for DankMaterialShell configuration and management. Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities. Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep %prep
%setup -q -c -n dms-qml %setup -q -c -n dms-qml
@@ -576,18 +565,10 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli chmod +x %{_builddir}/dms-cli
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build %build
%install %install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
install -d %{buildroot}%{_datadir}/bash-completion/completions install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions install -d %{buildroot}%{_datadir}/zsh/site-functions
@@ -617,10 +598,8 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi fi
# Signal running DMS instances to reload
if [ "$1" -ge 2 ]; then pkill -USR1 -x dms >/dev/null 2>&1 || :
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
@@ -636,14 +615,10 @@ jobs:
%{_datadir}/zsh/site-functions/_dms %{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish %{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1 * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER - Stable release VERSION_PLACEHOLDER
- Built from GitHub release - Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
+4 -31
View File
@@ -62,7 +62,7 @@ jobs:
} }
echo "✅ Source downloaded" echo "✅ Source downloaded"
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture" echo "Note: dms-cli binary will be downloaded during build based on target architecture"
ls -lh ls -lh
- name: Generate stable spec file - name: Generate stable spec file
@@ -94,7 +94,7 @@ jobs:
Requires: (quickshell or quickshell-git) Requires: (quickshell or quickshell-git)
Requires: accountsservice Requires: accountsservice
Requires: dms-cli Requires: dms-cli = %{version}-%{release}
Requires: dgop Requires: dgop
Recommends: cava Recommends: cava
@@ -125,17 +125,6 @@ jobs:
Command-line interface for DankMaterialShell configuration and management. Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities. Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep %prep
%setup -q -c -n dms-qml %setup -q -c -n dms-qml
@@ -162,19 +151,10 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli chmod +x %{_builddir}/dms-cli
# Download dgop for target architecture
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build %build
%install %install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Shell completions # Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions install -d %{buildroot}%{_datadir}/bash-completion/completions
@@ -202,11 +182,8 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi fi
# Signal running DMS instances to reload (harmless if none running)
# Restart DMS for active users after upgrade pkill -USR1 -x dms >/dev/null 2>&1 || :
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
@@ -220,14 +197,10 @@ jobs:
%{_datadir}/zsh/site-functions/_dms %{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish %{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER - Stable release VERSION_PLACEHOLDER
- Built from GitHub release - Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
+1 -1
View File
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
## Documentation ## Documentation
- **Website:** [danklinux.com](https://danklinux.com) - **Website:** [danklinux.com](https://danklinux.com)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs) - **Docs:** [danklinux.com/docs](https://danklinux.com/docs/)
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes) - **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview) - **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc) - **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
+2
View File
@@ -22,6 +22,8 @@ linters:
- (*os.Process).Signal - (*os.Process).Signal
- (*os.Process).Kill - (*os.Process).Kill
- syscall.Kill - syscall.Kill
# Seek on memfd (reset position before passing fd)
- syscall.Seek
# DBus cleanup # DBus cleanup
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal - (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal - (*github.com/godbus/dbus/v5.Conn).RemoveSignal
+5
View File
@@ -12,6 +12,11 @@ import (
var Version = "dev" var Version = "dev"
func main() { func main() {
if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1)
}
fileLogger, err := log.NewFileLogger() fileLogger, err := log.NewFileLogger()
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err) fmt.Printf("Warning: Failed to create log file: %v\n", err)
+51 -13
View File
@@ -295,7 +295,14 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data() data := buf.Data()
rgb := make([]byte, dstW*dstH*3) rgb := make([]byte, dstW*dstH*3)
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < dstH; y++ { for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale) srcY := int(float64(y) / scale)
@@ -309,16 +316,17 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
} }
si := srcY*buf.Stride + srcX*4 si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3 di := (y*dstW + x) * 3
if si+2 < len(data) { if si+3 >= len(data) {
if swapRB { continue
rgb[di+0] = data[si+2] }
rgb[di+1] = data[si+1] if swapRB {
rgb[di+2] = data[si+0] rgb[di+0] = data[si+2]
} else { rgb[di+1] = data[si+1]
rgb[di+0] = data[si+0] rgb[di+2] = data[si+0]
rgb[di+1] = data[si+1] } else {
rgb[di+2] = data[si+2] rgb[di+0] = data[si+0]
} rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
} }
} }
} }
@@ -370,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
} }
for _, o := range outputs { for _, o := range outputs {
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n", scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale) if o.FractionalScale == float64(int(o.FractionalScale)) {
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
}
transformStr := transformName(o.Transform)
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
}
}
func transformName(t int32) string {
switch t {
case 0:
return "normal"
case 1:
return "90"
case 2:
return "180"
case 3:
return "270"
case 4:
return "flipped"
case 5:
return "flipped-90"
case 6:
return "flipped-180"
case 7:
return "flipped-270"
default:
return fmt.Sprintf("%d", t)
} }
} }
+17 -3
View File
@@ -29,6 +29,7 @@ func runSetup() error {
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected { if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.") fmt.Println("No configurations selected. Exiting.")
@@ -67,14 +68,14 @@ func runSetup() error {
var err error var err error
if wmSelected && terminalSelected { if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal) results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
} else if wmSelected { } else if wmSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty) results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
if len(results) > 1 { if len(results) > 1 {
results = results[:1] results = results[:1]
} }
} else if terminalSelected { } else if terminalSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal) results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
if len(results) > 0 && results[0].ConfigType == "Niri" { if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:] results = results[1:]
} }
@@ -144,6 +145,19 @@ func promptTerminal() (deps.Terminal, bool) {
} }
} }
func promptSystemd() bool {
fmt.Println("\nUse systemd for session management?")
fmt.Println("1) Yes (recommended for most distros)")
fmt.Println("2) No (standalone, no systemd integration)")
var response string
fmt.Print("\nChoice (1-2): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
return response != "2"
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool { func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME") homeDir := os.Getenv("HOME")
willBackup := false willBackup := false
+14 -1
View File
@@ -30,6 +30,7 @@ type Output struct {
height int32 height int32
scale int32 scale int32
fractionalScale float64 fractionalScale float64
transform int32
} }
type LayerSurface struct { type LayerSurface struct {
@@ -276,6 +277,7 @@ func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
if o, ok := p.outputs[name]; ok { if o, ok := p.outputs[name]; ok {
o.x = e.X o.x = e.X
o.y = e.Y o.y = e.Y
o.transform = int32(e.Transform)
} }
p.outputsMu.Unlock() p.outputsMu.Unlock()
}) })
@@ -485,8 +487,19 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady() ls.state.OnScreencopyReady()
logicalW, _ := ls.state.LogicalSize()
screenBuf := ls.state.ScreenBuffer() screenBuf := ls.state.ScreenBuffer()
if screenBuf != nil && ls.output.transform != TransformNormal {
invTransform := InverseTransform(ls.output.transform)
transformed, err := screenBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != screenBuf {
ls.state.ReplaceScreenBuffer(transformed)
}
}
logicalW, _ := ls.state.LogicalSize()
screenBuf = ls.state.ScreenBuffer()
if logicalW > 0 && screenBuf != nil { if logicalW > 0 && screenBuf != nil {
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW) ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
} }
+15
View File
@@ -4,10 +4,25 @@ import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
)
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color { func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888) return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
} }
+78 -21
View File
@@ -1,6 +1,7 @@
package colorpicker package colorpicker
import ( import (
"fmt"
"math" "math"
"strings" "strings"
"sync" "sync"
@@ -15,6 +16,8 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888 FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888 FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
) )
type SurfaceState struct { type SurfaceState struct {
@@ -79,6 +82,11 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
bpp := format.BytesPerPixel()
if stride < width*bpp {
return fmt.Errorf("invalid stride %d for width %d (bpp=%d)", stride, width, bpp)
}
if s.screenBuf != nil { if s.screenBuf != nil {
s.screenBuf.Close() s.screenBuf.Close()
s.screenBuf = nil s.screenBuf = nil
@@ -90,6 +98,7 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
} }
s.screenBuf = buf s.screenBuf = buf
s.screenBuf.Format = format
s.screenFormat = format s.screenFormat = format
return nil return nil
} }
@@ -106,6 +115,20 @@ func (s *SurfaceState) ScreenFormat() PixelFormat {
return s.screenFormat return s.screenFormat
} }
func (s *SurfaceState) ReplaceScreenBuffer(newBuf *ShmBuffer) {
s.mu.Lock()
defer s.mu.Unlock()
if s.screenBuf != nil {
s.screenBuf.Close()
}
s.screenBuf = newBuf
s.screenFormat = newBuf.Format
s.recomputeScale()
s.ensureRenderBuffers()
}
func (s *SurfaceState) OnScreencopyFlags(flags uint32) { func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
s.mu.Lock() s.mu.Lock()
s.yInverted = (flags & 1) != 0 s.yInverted = (flags & 1) != 0
@@ -120,6 +143,15 @@ func (s *SurfaceState) OnScreencopyReady() {
return return
} }
if s.screenFormat.Is24Bit() {
converted, newFormat, err := s.screenBuf.ConvertTo32Bit(s.screenFormat)
if err == nil && converted != s.screenBuf {
s.screenBuf.Close()
s.screenBuf = converted
s.screenFormat = newFormat
}
}
s.recomputeScale() s.recomputeScale()
s.ensureRenderBuffers() s.ensureRenderBuffers()
s.readyForDisplay = true s.readyForDisplay = true
@@ -279,10 +311,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawMagnifierWithInversion( drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.Height, dst.Data(), dst.Stride, dst.Width, dst.Height,
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height, s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
px, py, picked, s.yInverted, px, py, picked, s.yInverted, s.screenFormat,
) )
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase) drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
return dst return dst
} }
@@ -390,6 +422,7 @@ func drawMagnifierWithInversion(
cx, cy int, cx, cy int,
borderColor Color, borderColor Color,
yInverted bool, yInverted bool,
format PixelFormat,
) { ) {
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 { if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
return return
@@ -407,6 +440,14 @@ func drawMagnifierWithInversion(
innerRadius := float64(outerRadius - borderThickness) innerRadius := float64(outerRadius - borderThickness)
outerRadiusF := float64(outerRadius) outerRadiusF := float64(outerRadius)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ { for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
y := cy + dy y := cy + dy
if y < 0 || y >= dstH { if y < 0 || y >= dstH {
@@ -431,9 +472,9 @@ func drawMagnifierWithInversion(
} }
bgColor := Color{ bgColor := Color{
B: dst[dstOff+0], R: dst[dstOff+rOff],
G: dst[dstOff+1], G: dst[dstOff+1],
R: dst[dstOff+2], B: dst[dstOff+bOff],
A: dst[dstOff+3], A: dst[dstOff+3],
} }
@@ -462,7 +503,7 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255} magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
finalColor = blendColors(magColor, borderColor, alpha) finalColor = blendColors(magColor, borderColor, alpha)
} else { } else {
finalColor = borderColor finalColor = borderColor
@@ -483,24 +524,25 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255} finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
} else { } else {
continue continue
} }
} }
dst[dstOff+0] = finalColor.B dst[dstOff+rOff] = finalColor.R
dst[dstOff+1] = finalColor.G dst[dstOff+1] = finalColor.G
dst[dstOff+2] = finalColor.R dst[dstOff+bOff] = finalColor.B
dst[dstOff+3] = 255 dst[dstOff+3] = 255
} }
} }
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius) drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
} }
func drawMagnifierCrosshair( func drawMagnifierCrosshair(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int, data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) { ) {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return return
@@ -998,7 +1040,7 @@ var fontGlyphs = map[rune][fontH]uint8{
}, },
} }
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) { func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
text := formatColorForPreview(c, format, lowercase) text := formatColorForPreview(c, format, lowercase)
if len(text) == 0 { if len(text) == 0 {
return return
@@ -1033,9 +1075,8 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
y = height - boxH y = height - boxH
} }
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c) drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
// Use contrasting text color based on luminance
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B) lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
var fg Color var fg Color
if lum > 128 { if lum > 128 {
@@ -1043,7 +1084,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else { } else {
fg = Color{R: 255, G: 255, B: 255, A: 255} fg = Color{R: 255, G: 255, B: 255, A: 255}
} }
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg) drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
} }
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string { func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
@@ -1064,7 +1105,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
} }
} }
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) { func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) {
if w <= 0 || h <= 0 { if w <= 0 || h <= 0 {
return return
} }
@@ -1073,6 +1114,14 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
x = clamp(x, 0, width) x = clamp(x, 0, width)
y = clamp(y, 0, height) y = clamp(y, 0, height)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for yy := y; yy < yEnd; yy++ { for yy := y; yy < yEnd; yy++ {
rowOff := yy * stride rowOff := yy * stride
for xx := x; xx < xEnd; xx++ { for xx := x; xx < xEnd; xx++ {
@@ -1080,26 +1129,34 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
if off+4 > len(data) { if off+4 > len(data) {
continue continue
} }
data[off+0] = col.B data[off+rOff] = col.R
data[off+1] = col.G data[off+1] = col.G
data[off+2] = col.R data[off+bOff] = col.B
data[off+3] = 255 data[off+3] = 255
} }
} }
} }
func drawText(data []byte, stride, width, height, x, y int, text string, col Color) { func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) {
for i, r := range text { for i, r := range text {
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col) drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format)
} }
} }
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) { func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) {
g, ok := fontGlyphs[r] g, ok := fontGlyphs[r]
if !ok { if !ok {
return return
} }
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for row := 0; row < fontH; row++ { for row := 0; row < fontH; row++ {
yy := y + row yy := y + row
if yy < 0 || yy >= height { if yy < 0 || yy >= height {
@@ -1123,9 +1180,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
continue continue
} }
data[off+0] = col.B data[off+rOff] = col.R
data[off+1] = col.G data[off+1] = col.G
data[off+2] = col.R data[off+bOff] = col.B
data[off+3] = 255 data[off+3] = 255
} }
} }
+91 -69
View File
@@ -46,11 +46,20 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil) return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
} }
// DeployConfigurationsWithSystemd deploys configurations with systemd option
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
}
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil) return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
shouldReplaceConfig := func(configType string) bool { shouldReplaceConfig := func(configType string) bool {
@@ -64,7 +73,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") { if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal) result, err := cd.deployNiriConfig(terminal, useSystemd)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err) return results, fmt.Errorf("failed to deploy Niri config: %w", err)
@@ -72,7 +81,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
} }
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") { if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal) result, err := cd.deployHyprlandConfig(terminal, useSystemd)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err) return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
@@ -110,7 +119,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return results, nil return results, nil
} }
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) { func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Niri", ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"), Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
@@ -148,12 +157,6 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
}
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -166,8 +169,11 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
terminalCommand = "ghostty" terminalCommand = "ghostty"
} }
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
@@ -404,41 +410,6 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil return results, nil
} }
// detectPolkitAgent tries to find the polkit authentication agent on the system
// Prioritizes mate-polkit paths since that's what we install
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
// Prioritize mate-polkit paths first
matePaths := []string{
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
}
for _, path := range matePaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
return path, nil
}
}
// Fallback to other polkit agents if mate-polkit is not found
fallbackPaths := []string{
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"/usr/libexec/polkit-gnome-authentication-agent-1",
}
for _, path := range fallbackPaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
return path, nil
}
}
return "", fmt.Errorf("no polkit agent found in common locations")
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config // mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones) // Regular expression to match output sections (including commented ones)
@@ -482,7 +453,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
} }
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging // deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) { func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Hyprland", ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"), Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
@@ -514,14 +485,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -531,13 +494,15 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
case deps.TerminalAlacritty: case deps.TerminalAlacritty:
terminalCommand = "alacritty" terminalCommand = "alacritty"
default: default:
terminalCommand = "ghostty" // fallback to ghostty terminalCommand = "ghostty"
} }
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the monitor sections
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
if err != nil { if err != nil {
@@ -560,24 +525,16 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config // mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match monitor lines (including commented ones)
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
// Also matches commented versions: # monitor = ...
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`) monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
// Find all monitor lines in the existing config
existingMonitors := monitorRegex.FindAllString(existingConfig, -1) existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 { if len(existingMonitors) == 0 {
// No monitor sections to merge
return newConfig, nil return newConfig, nil
} }
// Remove the example monitor line from the new config
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`) exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "") mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`) monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig) headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
@@ -585,8 +542,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return "", fmt.Errorf("could not find MONITOR CONFIG section") return "", fmt.Errorf("could not find MONITOR CONFIG section")
} }
// Insert after the header insertPos := headerMatch[1] + 1
insertPos := headerMatch[1] + 1 // +1 for the newline
var builder strings.Builder var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos]) builder.WriteString(mergedConfig[:insertPos])
@@ -601,3 +557,69 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return builder.String(), nil return builder.String(), nil
} }
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
lines := strings.Split(config, "\n")
var result []string
startupSectionFound := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
continue
}
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
continue
}
result = append(result, line)
}
if !startupSectionFound {
for i, line := range result {
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
}
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
break
}
}
}
return strings.Join(result, "\n")
}
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "%s"
}`, terminalCommand)
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
config = strings.Replace(config,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
1)
}
return config
}
+6 -38
View File
@@ -3,7 +3,6 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -11,23 +10,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDetectPolkitAgent(t *testing.T) {
cd := &ConfigDeployer{}
// This test depends on the system having a polkit agent installed
// We'll just test that the function doesn't crash and returns some path or error
path, err := cd.detectPolkitAgent()
if err != nil {
// If no polkit agent is found, that's okay for testing
assert.Contains(t, err.Error(), "no polkit agent found")
} else {
// If found, it should be a valid path
assert.NotEmpty(t, path)
assert.True(t, strings.Contains(path, "polkit"))
}
}
func TestMergeNiriOutputSections(t *testing.T) { func TestMergeNiriOutputSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -272,17 +254,6 @@ func getGhosttyPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config") return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
} }
func TestPolkitPathInjection(t *testing.T) {
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
other content`
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
}
func TestMergeHyprlandMonitorSections(t *testing.T) { func TestMergeHyprlandMonitorSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -424,7 +395,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
cd := NewConfigDeployer(logChan) cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) { t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty) result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -435,7 +406,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path) content, err := os.ReadFile(result.Path)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG") assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, $TERMINAL") assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
assert.Contains(t, string(content), "exec-once = ") assert.Contains(t, string(content), "exec-once = ")
}) })
@@ -454,7 +425,7 @@ general {
err = os.WriteFile(hyprPath, []byte(existingContent), 0644) err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err) require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty) result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -471,7 +442,7 @@ general {
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144") assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60") assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, $TERMINAL") assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
assert.NotContains(t, string(newContent), "monitor = eDP-2") assert.NotContains(t, string(newContent), "monitor = eDP-2")
}) })
} }
@@ -479,7 +450,6 @@ general {
func TestNiriConfigStructure(t *testing.T) { func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {") assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {") assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriBindsConfig, "binds {") assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`) assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
@@ -490,11 +460,9 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# STARTUP APPS") assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG") assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS") assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}") assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle") assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrule {") assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
} }
func TestGhosttyConfigStructure(t *testing.T) { func TestGhosttyConfigStructure(t *testing.T) {
+26 -121
View File
@@ -10,8 +10,9 @@ monitor = , preferred,auto,auto
# ================== # ==================
# STARTUP APPS # STARTUP APPS
# ================== # ==================
exec-once = dbus-update-activation-environment --systemd --all
exec-once = systemctl --user start hyprland-session.target
exec-once = bash -c "wl-paste --watch cliphist store &" exec-once = bash -c "wl-paste --watch cliphist store &"
exec-once = {{POLKIT_AGENT_PATH}}
# ================== # ==================
# INPUT CONFIG # INPUT CONFIG
@@ -90,132 +91,36 @@ misc {
# ================== # ==================
# WINDOW RULES # WINDOW RULES
# ================== # ==================
windowrule { windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
name = windowrule-1
tile = on
match:class = ^(org\.wezfurlong\.wezterm)$
border_size = 0
}
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrule { windowrulev2 = tile, class:^(gnome-control-center)$
name = windowrule-2 windowrulev2 = tile, class:^(pavucontrol)$
rounding = 12 windowrulev2 = tile, class:^(nm-connection-editor)$
match:class = ^(org\.gnome\.)
border_size = 0
}
windowrulev2 = float, class:^(gnome-calculator)$
windowrulev2 = float, class:^(galculator)$
windowrulev2 = float, class:^(blueman-manager)$
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
windowrulev2 = float, class:^(steam)$
windowrulev2 = float, class:^(xdg-desktop-portal)$
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = noborder, class:^(Alacritty)$
windowrulev2 = noborder, class:^(zen)$
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
windowrulev2 = noborder, class:^(kitty)$
windowrule { windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
name = windowrule-3 windowrulev2 = float, class:^(zoom)$
tile = on
match:class = ^(gnome-control-center)$
}
windowrule { # DMS windows floating by default
name = windowrule-4 windowrulev2 = float, class:^(org.quickshell)$
tile = on windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
match:class = ^(pavucontrol)$
}
windowrule { layerrule = noanim, ^(quickshell)$
name = windowrule-5
tile = on
match:class = ^(nm-connection-editor)$
}
windowrule {
name = windowrule-6
float = on
match:class = ^(gnome-calculator)$
}
windowrule {
name = windowrule-7
float = on
match:class = ^(galculator)$
}
windowrule {
name = windowrule-8
float = on
match:class = ^(blueman-manager)$
}
windowrule {
name = windowrule-9
float = on
match:class = ^(org\.gnome\.Nautilus)$
}
windowrule {
name = windowrule-10
float = on
match:class = ^(steam)$
}
windowrule {
name = windowrule-11
float = on
match:class = ^(xdg-desktop-portal)$
}
windowrule {
name = windowrule-12
border_size = 0
match:class = ^(Alacritty)$
}
windowrule {
name = windowrule-13
border_size = 0
match:class = ^(zen)$
}
windowrule {
name = windowrule-14
border_size = 0
match:class = ^(com\.mitchellh\.ghostty)$
}
windowrule {
name = windowrule-15
border_size = 0
match:class = ^(kitty)$
}
windowrule {
name = windowrule-16
float = on
match:class = ^(firefox)$
match:title = ^(Picture-in-Picture)$
}
windowrule {
name = windowrule-17
float = on
match:class = ^(zoom)$
}
windowrule {
name = windowrule-18
opacity = 0.9 0.9
match:float = 0
match:focus = 0
}
layerrule {
name = layerrule-1
no_anim = on
match:namespace = ^(quickshell)$
}
# ================== # ==================
# KEYBINDINGS # KEYBINDINGS
@@ -223,7 +128,7 @@ layerrule {
$mod = SUPER $mod = SUPER
# === Application Launchers === # === Application Launchers ===
bind = $mod, T, exec, $TERMINAL bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle bind = $mod, M, exec, dms ipc call processlist focusOrToggle
-7
View File
@@ -44,7 +44,6 @@ input {
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout // https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout { layout {
// Set gaps around windows in logical pixels. // Set gaps around windows in logical pixels.
gaps 5
background-color "transparent" background-color "transparent"
// When to center a column when changing focus, options are: // When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left // - "never", default behavior, focusing an off-screen column will keep at the left
@@ -87,11 +86,6 @@ layout {
inactive-color "#d0d0d0" // Light gray inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red urgent-color "#cc4444" // Softer red
} }
focus-ring {
width 2
active-color "#808080" // Medium gray
inactive-color "#505050" // Dark gray
}
shadow { shadow {
softness 30 softness 30
spread 5 spread 5
@@ -116,7 +110,6 @@ overview {
// See the binds section below for more spawn examples. // See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors. // This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &" spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment { environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
} }
+7 -5
View File
@@ -182,13 +182,11 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
if forceQuickshellGit || variant == deps.VariantGit { if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem} // ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
} }
@@ -362,6 +360,10 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := a.WriteWindowManagerConfig(wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := a.EnableDMSService(ctx); err != nil { if err := a.EnableDMSService(ctx); err != nil {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+35
View File
@@ -611,6 +611,41 @@ func (b *BaseDistribution) EnableDMSService(ctx context.Context) error {
return nil return nil
} }
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
if wm == deps.WindowManagerHyprland {
if err := b.WriteHyprlandSessionTarget(); err != nil {
return fmt.Errorf("failed to write hyprland session target: %w", err)
}
}
return nil
}
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
targetPath := filepath.Join(targetDir, "hyprland-session.target")
content := `[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
return nil
}
// installDMSBinary installs the DMS binary from GitHub releases // installDMSBinary installs the DMS binary from GitHub releases
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
b.log("Installing/updating DMS binary...") b.log("Installing/updating DMS binary...")
+11 -7
View File
@@ -208,7 +208,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil { if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential") cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err) return fmt.Errorf("failed to install build-essential: %w", err)
} }
@@ -225,7 +225,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -338,6 +338,10 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := d.WriteWindowManagerConfig(wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := d.EnableDMSService(ctx); err != nil { if err := d.EnableDMSService(ctx); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
@@ -449,7 +453,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL), CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
} }
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath) keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd) cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil { if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
@@ -467,7 +471,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
addRepoCmd := ExecSudoCommand(ctx, sudoPassword, addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile)) fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
} }
@@ -502,7 +506,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"apt-get", "install", "-y"} args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -612,7 +616,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup", CommandInfo: "sudo apt-get install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup") rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
@@ -651,7 +655,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go", CommandInfo: "sudo apt-get install golang-go",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go") installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
} }
+6 -5
View File
@@ -166,10 +166,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"} return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
} }
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"} return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
} }
@@ -177,7 +174,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
if variant == deps.VariantGit { if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
} }
return PackageMapping{Name: "niri", Repository: RepoTypeSystem} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
} }
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -362,6 +359,10 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := f.WriteWindowManagerConfig(wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := f.EnableDMSService(ctx); err != nil { if err := f.EnableDMSService(ctx); err != nil {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+6 -22
View File
@@ -95,7 +95,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectWindowManager(wm)) dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell()) dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal()) dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectPolkitAgent())
dependencies = append(dependencies, g.detectAccountsService()) dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland { if wm == deps.WindowManagerHyprland {
@@ -127,20 +126,6 @@ func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
} }
} }
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("mate-extra/mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if g.packageInstalled("gui-apps/xwayland-satellite") { if g.packageInstalled("gui-apps/xwayland-satellite") {
@@ -187,7 +172,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, "alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"}, "xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"}, "qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
@@ -223,12 +207,8 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"} return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
} }
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
archKeyword := g.getArchKeyword() return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
if variant == deps.VariantGit {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
}
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
} }
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
@@ -456,6 +436,10 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := g.WriteWindowManagerConfig(wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := g.EnableDMSService(ctx); err != nil { if err := g.EnableDMSService(ctx); err != nil {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+5 -1
View File
@@ -377,6 +377,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := o.WriteWindowManagerConfig(wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := o.EnableDMSService(ctx); err != nil { if err := o.EnableDMSService(ctx); err != nil {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
@@ -472,7 +476,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
cmd := ExecSudoCommand(ctx, sudoPassword, cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL)) fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err) o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
} }
enabledRepos[pkg.RepoURL] = true enabledRepos[pkg.RepoURL] = true
+4
View File
@@ -357,6 +357,10 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := u.WriteWindowManagerConfig(wm); err != nil {
u.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := u.EnableDMSService(ctx); err != nil { if err := u.EnableDMSService(ctx); err != nil {
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+6 -5
View File
@@ -154,11 +154,12 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
} }
bind := keybinds.Keybind{ bind := keybinds.Keybind{
Key: keyStr, Key: keyStr,
Description: kb.Description, Description: kb.Description,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source, Source: source,
HideOnOverlay: kb.HideOnOverlay,
} }
if source == "dms" && conflicts != nil { if source == "dms" && conflicts != nil {
+21 -13
View File
@@ -11,12 +11,13 @@ import (
) )
type NiriKeyBinding struct { type NiriKeyBinding struct {
Mods []string Mods []string
Key string Key string
Action string Action string
Args []string Args []string
Description string Description string
Source string HideOnOverlay bool
Source string
} }
type NiriSection struct { type NiriSection struct {
@@ -273,19 +274,26 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
} }
var description string var description string
var hideOnOverlay bool
if node.Properties != nil { if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok { if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
description = val.ValueString() switch val.ValueString() {
case "null", "":
hideOnOverlay = true
default:
description = val.ValueString()
}
} }
} }
return &NiriKeyBinding{ return &NiriKeyBinding{
Mods: mods, Mods: mods,
Key: key, Key: key,
Action: action, Action: action,
Args: args, Args: args,
Description: description, Description: description,
Source: p.currentSource, HideOnOverlay: hideOnOverlay,
Source: p.currentSource,
} }
} }
+7 -6
View File
@@ -1,12 +1,13 @@
package keybinds package keybinds
type Keybind struct { type Keybind struct {
Key string `json:"key"` Key string `json:"key"`
Description string `json:"desc"` Description string `json:"desc"`
Action string `json:"action,omitempty"` Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"` Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"` HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
} }
type DMSBindsStatus struct { type DMSBindsStatus struct {
@@ -238,9 +238,17 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy != nil { if proxy == nil {
e.Head = proxy.(*ZwlrOutputHeadV1) head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context())
head.SetID(objectID)
registerServerProxy(i.Context(), head, objectID)
e.Head = head
} else if head, ok := proxy.(*ZwlrOutputHeadV1); ok {
e.Head = head
} else { } else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
head := &ZwlrOutputHeadV1{} head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context()) head.SetContext(i.Context())
head.SetID(objectID) head.SetID(objectID)
@@ -715,9 +723,17 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy != nil { if proxy == nil {
e.Mode = proxy.(*ZwlrOutputModeV1) mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
registerServerProxy(i.Context(), mode, objectID)
e.Mode = mode
} else if mode, ok := proxy.(*ZwlrOutputModeV1); ok {
e.Mode = mode
} else { } else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
mode := &ZwlrOutputModeV1{} mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context()) mode.SetContext(i.Context())
mode.SetID(objectID) mode.SetID(objectID)
@@ -743,7 +759,26 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
} }
var e ZwlrOutputHeadV1CurrentModeEvent var e ZwlrOutputHeadV1CurrentModeEvent
l := 0 l := 0
e.Mode = i.Context().GetProxy(client.Uint32(data[l : l+4])).(*ZwlrOutputModeV1) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
// Mode not yet registered, create it
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
registerServerProxy(i.Context(), mode, objectID)
e.Mode = mode
} else if mode, ok := proxy.(*ZwlrOutputModeV1); ok {
e.Mode = mode
} else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
registerServerProxy(i.Context(), mode, objectID)
e.Mode = mode
}
l += 4 l += 4
i.currentModeHandler(e) i.currentModeHandler(e)
+107 -8
View File
@@ -7,6 +7,7 @@ import (
"os/exec" "os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
@@ -89,12 +90,15 @@ func SetCompositorDWL() {
} }
type WindowGeometry struct { type WindowGeometry struct {
X int32 X int32
Y int32 Y int32
Width int32 Width int32
Height int32 Height int32
Output string Output string
Scale float64 Scale float64
OutputX int32
OutputY int32
OutputTransform int32
} }
func GetActiveWindow() (*WindowGeometry, error) { func GetActiveWindow() (*WindowGeometry, error) {
@@ -382,6 +386,92 @@ func GetFocusedMonitor() string {
return "" return ""
} }
type outputInfo struct {
x, y int32
transform int32
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
display, err := client.Connect("")
if err != nil {
return nil, false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, false
}
var outputManager *wlr_output_management.ZwlrOutputManagerV1
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
outputManager = mgr
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false
}
if outputManager == nil {
return nil, false
}
type headState struct {
name string
x, y int32
transform int32
}
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
done := false
outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
state := &headState{}
heads[e.Head] = state
e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) {
state.name = ne.Name
})
e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
state.x = pe.X
state.y = pe.Y
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform
})
})
outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
done = true
})
for !done {
if err := ctx.Dispatch(); err != nil {
return nil, false
}
}
for _, state := range heads {
if state.name == outputName {
return &outputInfo{
x: state.x,
y: state.y,
transform: state.transform,
}, true
}
}
return nil, false
}
func getDWLActiveWindow() (*WindowGeometry, error) { func getDWLActiveWindow() (*WindowGeometry, error) {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
@@ -509,14 +599,23 @@ func getDWLActiveWindow() (*WindowGeometry, error) {
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
return &WindowGeometry{
geom := &WindowGeometry{
X: state.x, X: state.x,
Y: state.y, Y: state.y,
Width: state.w, Width: state.w,
Height: state.h, Height: state.h,
Output: state.name, Output: state.name,
Scale: scale, Scale: scale,
}, nil }
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
} }
return nil, fmt.Errorf("no active output found") return nil, fmt.Errorf("no active output found")
+7 -1
View File
@@ -20,7 +20,13 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height)) img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
data := buf.Data() data := buf.Data()
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0 var swapRB bool
switch format {
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < buf.Height; y++ { for y := 0; y < buf.Height; y++ {
srcOff := y * buf.Stride srcOff := y * buf.Stride
+52 -6
View File
@@ -380,19 +380,24 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
return return
} }
var capturedBuf *ShmBuffer
var capturedFormat PixelFormat
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
capturedFormat = PixelFormat(e.Format)
bpp := capturedFormat.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
return
}
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("create screen buffer failed", "err", err) log.Error("create screen buffer failed", "err", err)
return return
} }
if withCursor { capturedBuf = buf
pc.screenBuf = buf buf.Format = capturedFormat
pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil { if err != nil {
@@ -421,6 +426,47 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
frame.Destroy() frame.Destroy()
if capturedBuf == nil {
onReady()
return
}
if capturedFormat.Is24Bit() {
converted, newFormat, err := capturedBuf.ConvertTo32Bit(capturedFormat)
if err != nil {
log.Error("convert 24-bit to 32-bit failed", "err", err)
} else if converted != capturedBuf {
capturedBuf.Close()
capturedBuf = converted
capturedFormat = newFormat
}
}
pc.format = uint32(capturedFormat)
if pc.yInverted {
capturedBuf.FlipVertical()
pc.yInverted = false
}
if output.transform != TransformNormal {
invTransform := InverseTransform(output.transform)
transformed, err := capturedBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != capturedBuf {
capturedBuf.Close()
capturedBuf = transformed
}
}
if withCursor {
pc.screenBuf = capturedBuf
} else {
pc.screenBufNoCursor = capturedBuf
}
onReady() onReady()
}) })
+162 -38
View File
@@ -150,51 +150,33 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
case CompositorHyprland: case CompositorHyprland:
return s.captureAndCrop(output, region) return s.captureAndCrop(output, region)
case CompositorDWL: case CompositorDWL:
return s.captureDWLWindow(output, region, geom.Scale) return s.captureDWLWindow(output, region, geom)
default: default:
return s.captureRegionOnOutput(output, region) return s.captureRegionOnOutput(output, region)
} }
} }
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, dwlScale float64) (*CaptureResult, error) { func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output) result, err := s.captureWholeOutput(output)
if err != nil { if err != nil {
return nil, err return nil, err
} }
scale := dwlScale scale := geom.Scale
if scale <= 0 { if scale <= 0 || scale == 1.0 {
scale = float64(result.Buffer.Width) / float64(output.width) if output.fractionalScale > 1.0 {
scale = output.fractionalScale
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
localX := int(float64(region.X) * scale) localX := int(float64(region.X-geom.OutputX) * scale)
localY := int(float64(region.Y) * scale) localY := int(float64(region.Y-geom.OutputY) * scale)
if localX >= result.Buffer.Width {
localX = localX % result.Buffer.Width
}
if localY >= result.Buffer.Height {
localY = localY % result.Buffer.Height
}
w := int(float64(region.Width) * scale) w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale) h := int(float64(region.Height) * scale)
if localY+h > result.Buffer.Height && h <= result.Buffer.Height {
localY = result.Buffer.Height - h
if localY < 0 {
localY = 0
}
}
if localX+w > result.Buffer.Width && w <= result.Buffer.Width {
localX = result.Buffer.Width - w
if localX < 0 {
localX = 0
}
}
if localX < 0 { if localX < 0 {
w += localX w += localX
localX = 0 localX = 0
@@ -342,13 +324,18 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
outX, outY := output.x, output.y outX, outY := output.x, output.y
scale := float64(output.scale) scale := float64(output.scale)
if DetectCompositor() == CompositorHyprland { switch DetectCompositor() {
case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy outX, outY = hx, hy
} }
if s := GetHyprlandMonitorScale(output.name); s > 0 { if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s scale = s
} }
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
@@ -476,13 +463,42 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
return nil, fmt.Errorf("capture output: %w", err) return nil, fmt.Errorf("capture output: %w", err)
} }
return s.processFrame(frame, Region{ result, err := s.processFrame(frame, Region{
X: output.x, X: output.x,
Y: output.y, Y: output.y,
Width: output.width, Width: output.width,
Height: output.height, Height: output.height,
Output: output.name, Output: output.name,
}) })
if err != nil {
return nil, err
}
if result.YInverted {
result.Buffer.FlipVertical()
result.YInverted = false
}
if output.transform == TransformNormal {
return result, nil
}
invTransform := InverseTransform(output.transform)
transformed, err := result.Buffer.ApplyTransform(invTransform)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("apply transform: %w", err)
}
if transformed != result.Buffer {
result.Buffer.Close()
result.Buffer = transformed
}
result.Region.Width = int32(transformed.Width)
result.Region.Height = int32(transformed.Height)
return result, nil
} }
func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
@@ -563,6 +579,10 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
} }
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
if output.transform != TransformNormal {
return s.captureRegionOnTransformedOutput(output, region)
}
scale := output.fractionalScale scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland { if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name) scale = GetHyprlandMonitorScale(output.name)
@@ -617,6 +637,76 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return s.processFrame(frame, region) return s.processFrame(frame, region)
} }
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
}
scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-output.x) * scale)
localY := int(float64(region.Y-output.y) * scale)
w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale)
if localX < 0 {
w += localX
localX = 0
}
if localY < 0 {
h += localY
localY = 0
}
if localX+w > result.Buffer.Width {
w = result.Buffer.Width - localX
}
if localY+h > result.Buffer.Height {
h = result.Buffer.Height - localY
}
if w <= 0 || h <= 0 {
result.Buffer.Close()
return nil, fmt.Errorf("region not visible on output")
}
cropped, err := CreateShmBuffer(w, h, w*4)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("create crop buffer: %w", err)
}
srcData := result.Buffer.Data()
dstData := cropped.Data()
for y := 0; y < h; y++ {
srcOff := (localY+y)*result.Buffer.Stride + localX*4
dstOff := y * cropped.Stride
if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) {
copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4])
}
}
result.Buffer.Close()
cropped.Format = PixelFormat(result.Format)
return &CaptureResult{
Buffer: cropped,
Region: region,
YInverted: false,
Format: result.Format,
}, nil
}
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) { func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
var buf *ShmBuffer var buf *ShmBuffer
var pool *client.ShmPool var pool *client.ShmPool
@@ -627,13 +717,18 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
failed := false failed := false
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
format = PixelFormat(e.Format)
bpp := format.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
return
}
var err error var err error
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("failed to create buffer", "err", err) log.Error("failed to create buffer", "err", err)
return return
} }
format = PixelFormat(e.Format)
buf.Format = format buf.Format = format
}) })
@@ -696,6 +791,19 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
return nil, fmt.Errorf("frame capture failed") return nil, fmt.Errorf("frame capture failed")
} }
if format.Is24Bit() {
converted, newFormat, err := buf.ConvertTo32Bit(format)
if err != nil {
buf.Close()
return nil, fmt.Errorf("convert 24-bit to 32-bit: %w", err)
}
if converted != buf {
buf.Close()
buf = converted
}
format = newFormat
}
return &CaptureResult{ return &CaptureResult{
Buffer: buf, Buffer: buf,
Region: region, Region: region,
@@ -924,16 +1032,32 @@ func ListOutputs() ([]Output, error) {
sc.outputsMu.Lock() sc.outputsMu.Lock()
defer sc.outputsMu.Unlock() defer sc.outputsMu.Unlock()
compositor := DetectCompositor()
result := make([]Output, 0, len(sc.outputs)) result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs { for _, o := range sc.outputs {
result = append(result, Output{ out := Output{
Name: o.name, Name: o.name,
X: o.x, X: o.x,
Y: o.y, Y: o.y,
Width: o.width, Width: o.width,
Height: o.height, Height: o.height,
Scale: o.scale, Scale: o.scale,
}) FractionalScale: o.fractionalScale,
Transform: o.transform,
}
switch compositor {
case CompositorHyprland:
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
out.X, out.Y = hx, hy
out.Width, out.Height = hw, hh
}
if s := GetHyprlandMonitorScale(o.name); s > 0 {
out.FractionalScale = s
}
}
result = append(result, out)
} }
return result, nil return result, nil
} }
+17
View File
@@ -9,6 +9,19 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888 FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888 FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
)
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
) )
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
@@ -16,3 +29,7 @@ type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
+7 -5
View File
@@ -32,11 +32,13 @@ func (r Region) IsEmpty() bool {
} }
type Output struct { type Output struct {
Name string Name string
X, Y int32 X, Y int32
Width int32 Width int32
Height int32 Height int32
Scale int32 Scale int32
FractionalScale float64
Transform int32
} }
type Config struct { type Config struct {
+2 -8
View File
@@ -7,13 +7,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "apppicker.open", "browser.open": case "apppicker.open", "browser.open":
handleOpen(conn, req, manager) handleOpen(conn, req, manager)
@@ -22,7 +16,7 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleOpen(conn net.Conn, req Request, manager *Manager) { func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params) log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := req.Params["target"].(string) target, ok := req.Params["target"].(string)
+56 -79
View File
@@ -6,25 +6,15 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type BluetoothEvent struct { type BluetoothEvent struct {
Type string `json:"type"` Type string `json:"type"`
Data BluetoothState `json:"data"` Data BluetoothState `json:"data"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method { switch req.Method {
case "bluetooth.getState": case "bluetooth.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -57,31 +47,30 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleStartDiscovery(conn net.Conn, req Request, manager *Manager) { func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.StartDiscovery(); err != nil { if err := manager.StartDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery started"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"})
} }
func handleStopDiscovery(conn net.Conn, req Request, manager *Manager) { func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.StopDiscovery(); err != nil { if err := manager.StopDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery stopped"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"})
} }
func handleSetPowered(conn net.Conn, req Request, manager *Manager) { func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
powered, ok := req.Params["powered"].(bool) powered, err := params.Bool(req.Params, "powered")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'powered' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -90,13 +79,13 @@ func handleSetPowered(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "powered state updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"})
} }
func handlePairDevice(conn net.Conn, req Request, manager *Manager) { func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -105,13 +94,13 @@ func handlePairDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing initiated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"})
} }
func handleConnectDevice(conn net.Conn, req Request, manager *Manager) { func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -120,13 +109,13 @@ func handleConnectDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleDisconnectDevice(conn net.Conn, req Request, manager *Manager) { func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -135,13 +124,13 @@ func handleDisconnectDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
} }
func handleRemoveDevice(conn net.Conn, req Request, manager *Manager) { func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -150,13 +139,13 @@ func handleRemoveDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device removed"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"})
} }
func handleTrustDevice(conn net.Conn, req Request, manager *Manager) { func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -165,13 +154,13 @@ func handleTrustDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device trusted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"})
} }
func handleUntrustDevice(conn net.Conn, req Request, manager *Manager) { func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string) devicePath, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -180,43 +169,31 @@ func handleUntrustDevice(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device untrusted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"})
} }
func handlePairingSubmit(conn net.Conn, req Request, manager *Manager) { func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
secretsRaw, ok := req.Params["secrets"].(map[string]any) secrets := params.StringMapOpt(req.Params, "secrets")
secrets := make(map[string]string) accept := params.BoolOpt(req.Params, "accept", false)
if ok {
for k, v := range secretsRaw {
if str, ok := v.(string); ok {
secrets[k] = str
}
}
}
accept := false
if acceptParam, ok := req.Params["accept"].(bool); ok {
accept = acceptParam
}
if err := manager.SubmitPairing(token, secrets, accept); err != nil { if err := manager.SubmitPairing(token, secrets, accept); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing response submitted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"})
} }
func handlePairingCancel(conn net.Conn, req Request, manager *Manager) { func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -225,10 +202,10 @@ func handlePairingCancel(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing cancelled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+49 -88
View File
@@ -2,12 +2,14 @@ package brightness
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
func HandleRequest(conn net.Conn, req Request, m *Manager) { func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method { switch req.Method {
case "brightness.getState": case "brightness.getState":
handleGetState(conn, req, m) handleGetState(conn, req, m)
@@ -22,131 +24,90 @@ func HandleRequest(conn net.Conn, req Request, m *Manager) {
case "brightness.subscribe": case "brightness.subscribe":
handleSubscribe(conn, req, m) handleSubscribe(conn, req, m)
default: default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method) models.RespondError(conn, req.ID, "unknown method: "+req.Method)
} }
} }
func handleGetState(conn net.Conn, req Request, m *Manager) { func handleGetState(conn net.Conn, req models.Request, m *Manager) {
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleSetBrightness(conn net.Conn, req Request, m *Manager) { func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
var params SetBrightnessParams device, err := params.String(req.Params, "device")
if err != nil {
device, ok := req.Params["device"].(string) models.RespondError(conn, req.ID, err.Error())
if !ok {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter")
return
}
params.Device = device
percentFloat, ok := req.Params["percent"].(float64)
if !ok {
models.RespondError(conn, req.ID.(int), "missing or invalid percent parameter")
return
}
params.Percent = int(percentFloat)
if exponential, ok := req.Params["exponential"].(bool); ok {
params.Exponential = exponential
}
exponent := 1.2
if exponentFloat, ok := req.Params["exponent"].(float64); ok {
params.Exponent = exponentFloat
exponent = exponentFloat
}
if err := m.SetBrightnessWithExponent(params.Device, params.Percent, params.Exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error())
return return
} }
state := m.GetState() percent, err := params.Int(req.Params, "percent")
models.Respond(conn, req.ID.(int), state) if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
exponential := params.BoolOpt(req.Params, "exponential", false)
exponent := params.FloatOpt(req.Params, "exponent", 1.2)
if err := m.SetBrightnessWithExponent(device, percent, exponential, exponent); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, m.GetState())
} }
func handleIncrement(conn net.Conn, req Request, m *Manager) { func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
device, ok := req.Params["device"].(string) device, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
step := 10 step := params.IntOpt(req.Params, "step", 10)
if stepFloat, ok := req.Params["step"].(float64); ok { exponential := params.BoolOpt(req.Params, "exponential", false)
step = int(stepFloat) exponent := params.FloatOpt(req.Params, "exponent", 1.2)
}
exponential := false
if expBool, ok := req.Params["exponential"].(bool); ok {
exponential = expBool
}
exponent := 1.2
if exponentFloat, ok := req.Params["exponent"].(float64); ok {
exponent = exponentFloat
}
if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil { if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleDecrement(conn net.Conn, req Request, m *Manager) { func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
device, ok := req.Params["device"].(string) device, err := params.String(req.Params, "device")
if !ok { if err != nil {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
step := 10 step := params.IntOpt(req.Params, "step", 10)
if stepFloat, ok := req.Params["step"].(float64); ok { exponential := params.BoolOpt(req.Params, "exponential", false)
step = int(stepFloat) exponent := params.FloatOpt(req.Params, "exponent", 1.2)
}
exponential := false
if expBool, ok := req.Params["exponential"].(bool); ok {
exponential = expBool
}
exponent := 1.2
if exponentFloat, ok := req.Params["exponent"].(float64); ok {
exponent = exponentFloat
}
if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil { if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleRescan(conn net.Conn, req Request, m *Manager) { func handleRescan(conn net.Conn, req models.Request, m *Manager) {
m.Rescan() m.Rescan()
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
func handleSubscribe(conn net.Conn, req Request, m *Manager) { func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := "brightness-subscriber" clientID := fmt.Sprintf("brightness-%d", req.ID)
if idStr, ok := req.ID.(string); ok && idStr != "" {
clientID = idStr
}
ch := m.Subscribe(clientID) ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID) defer m.Unsubscribe(clientID)
initialState := m.GetState() initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID.(int), ID: req.ID,
Result: &initialState, Result: &initialState,
}); err != nil { }); err != nil {
return return
@@ -154,7 +115,7 @@ func handleSubscribe(conn net.Conn, req Request, m *Manager) {
for state := range ch { for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID.(int), ID: req.ID,
Result: &state, Result: &state,
}); err != nil { }); err != nil {
return return
-13
View File
@@ -33,12 +33,6 @@ type DeviceUpdate struct {
Device Device `json:"device"` Device Device `json:"device"`
} }
type Request struct {
ID any `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
type Manager struct { type Manager struct {
logindBackend *LogindBackend logindBackend *LogindBackend
sysfsBackend *SysfsBackend sysfsBackend *SysfsBackend
@@ -112,13 +106,6 @@ type ddcCapability struct {
current int current int
} }
type SetBrightnessParams struct {
Device string `json:"device"`
Percent int `json:"percent"`
Exponential bool `json:"exponential,omitempty"`
Exponent float64 `json:"exponent,omitempty"`
}
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16) ch := make(chan State, 16)
+1 -7
View File
@@ -6,13 +6,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "browser.open": case "browser.open":
url, ok := req.Params["url"].(string) url, ok := req.Params["url"].(string)
+152 -168
View File
@@ -6,25 +6,21 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type CUPSEvent struct { type CUPSEvent struct {
Type string `json:"type"` Type string `json:"type"`
Data CUPSState `json:"data"` Data CUPSState `json:"data"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { type TestPageResult struct {
Success bool `json:"success"`
JobID int `json:"jobId"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method { switch req.Method {
case "cups.subscribe": case "cups.subscribe":
handleSubscribe(conn, req, manager) handleSubscribe(conn, req, manager)
@@ -79,20 +75,19 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetPrinters(conn net.Conn, req Request, manager *Manager) { func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
printers, err := manager.GetPrinters() printers, err := manager.GetPrinters()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, printers) models.Respond(conn, req.ID, printers)
} }
func handleGetJobs(conn net.Conn, req Request, manager *Manager) { func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -101,14 +96,13 @@ func handleGetJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, jobs) models.Respond(conn, req.ID, jobs)
} }
func handlePausePrinter(conn net.Conn, req Request, manager *Manager) { func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -116,13 +110,13 @@ func handlePausePrinter(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "paused"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
} }
func handleResumePrinter(conn net.Conn, req Request, manager *Manager) { func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -130,28 +124,27 @@ func handleResumePrinter(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "resumed"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
} }
func handleCancelJob(conn net.Conn, req Request, manager *Manager) { func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64) jobID, err := params.Int(req.Params, "jobID")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'jobid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
jobID := int(jobIDFloat)
if err := manager.CancelJob(jobID); err != nil { if err := manager.CancelJob(jobID); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job canceled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
} }
func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) { func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.String(req.Params, "printerName")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -159,10 +152,10 @@ func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "jobs canceled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
@@ -193,7 +186,7 @@ func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetDevices(conn net.Conn, req Request, manager *Manager) { func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
devices, err := manager.GetDevices() devices, err := manager.GetDevices()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -202,7 +195,7 @@ func handleGetDevices(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, devices) models.Respond(conn, req.ID, devices)
} }
func handleGetPPDs(conn net.Conn, req Request, manager *Manager) { func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
ppds, err := manager.GetPPDs() ppds, err := manager.GetPPDs()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -211,7 +204,7 @@ func handleGetPPDs(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, ppds) models.Respond(conn, req.ID, ppds)
} }
func handleGetClasses(conn net.Conn, req Request, manager *Manager) { func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
classes, err := manager.GetClasses() classes, err := manager.GetClasses()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -220,41 +213,41 @@ func handleGetClasses(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, classes) models.Respond(conn, req.ID, classes)
} }
func handleCreatePrinter(conn net.Conn, req Request, manager *Manager) { func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
name, ok := req.Params["name"].(string) name, err := params.StringNonEmpty(req.Params, "name")
if !ok || name == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
deviceURI, ok := req.Params["deviceURI"].(string) deviceURI, err := params.StringNonEmpty(req.Params, "deviceURI")
if !ok || deviceURI == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'deviceURI' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
ppd, ok := req.Params["ppd"].(string) ppd, err := params.StringNonEmpty(req.Params, "ppd")
if !ok || ppd == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ppd' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
shared, _ := req.Params["shared"].(bool) shared := params.BoolOpt(req.Params, "shared", false)
errorPolicy, _ := req.Params["errorPolicy"].(string) errorPolicy := params.StringOpt(req.Params, "errorPolicy", "")
information, _ := req.Params["information"].(string) information := params.StringOpt(req.Params, "information", "")
location, _ := req.Params["location"].(string) location := params.StringOpt(req.Params, "location", "")
if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil { if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer created"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
} }
func handleDeletePrinter(conn net.Conn, req Request, manager *Manager) { func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -262,13 +255,13 @@ func handleDeletePrinter(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer deleted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
} }
func handleAcceptJobs(conn net.Conn, req Request, manager *Manager) { func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -276,13 +269,13 @@ func handleAcceptJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "accepting jobs"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
} }
func handleRejectJobs(conn net.Conn, req Request, manager *Manager) { func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -290,19 +283,19 @@ func handleRejectJobs(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "rejecting jobs"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
} }
func handleSetPrinterShared(conn net.Conn, req Request, manager *Manager) { func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
shared, ok := req.Params["shared"].(bool) shared, err := params.Bool(req.Params, "shared")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'shared' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -310,19 +303,19 @@ func handleSetPrinterShared(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sharing updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
} }
func handleSetPrinterLocation(conn net.Conn, req Request, manager *Manager) { func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
location, ok := req.Params["location"].(string) location, err := params.String(req.Params, "location")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -330,19 +323,19 @@ func handleSetPrinterLocation(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
} }
func handleSetPrinterInfo(conn net.Conn, req Request, manager *Manager) { func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
info, ok := req.Params["info"].(string) info, err := params.String(req.Params, "info")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'info' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -350,39 +343,33 @@ func handleSetPrinterInfo(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "info updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
} }
func handleMoveJob(conn net.Conn, req Request, manager *Manager) { func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64) jobID, err := params.Int(req.Params, "jobID")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
destPrinter, ok := req.Params["destPrinter"].(string)
if !ok || destPrinter == "" {
models.RespondError(conn, req.ID, "missing or invalid 'destPrinter' parameter")
return
}
if err := manager.MoveJob(int(jobIDFloat), destPrinter); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job moved"})
destPrinter, err := params.StringNonEmpty(req.Params, "destPrinter")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.MoveJob(jobID, destPrinter); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
} }
type TestPageResult struct { func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
Success bool `json:"success"` printerName, err := params.StringNonEmpty(req.Params, "printerName")
JobID int `json:"jobId"` if err != nil {
Message string `json:"message"` models.RespondError(conn, req.ID, err.Error())
}
func handlePrintTestPage(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return return
} }
@@ -394,16 +381,16 @@ func handlePrintTestPage(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"}) models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
} }
func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) { func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
className, ok := req.Params["className"].(string) className, err := params.StringNonEmpty(req.Params, "className")
if !ok || className == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -411,19 +398,19 @@ func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer added to class"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
} }
func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager) { func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
className, ok := req.Params["className"].(string) className, err := params.StringNonEmpty(req.Params, "className")
if !ok || className == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
printerName, ok := req.Params["printerName"].(string) printerName, err := params.StringNonEmpty(req.Params, "printerName")
if !ok || printerName == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -431,13 +418,13 @@ func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager)
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer removed from class"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
} }
func handleDeleteClass(conn net.Conn, req Request, manager *Manager) { func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
className, ok := req.Params["className"].(string) className, err := params.StringNonEmpty(req.Params, "className")
if !ok || className == "" { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -445,38 +432,35 @@ func handleDeleteClass(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "class deleted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
} }
func handleRestartJob(conn net.Conn, req Request, manager *Manager) { func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64) jobID, err := params.Int(req.Params, "jobID")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
if err := manager.RestartJob(int(jobIDFloat)); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn net.Conn, req Request, manager *Manager) { if err := manager.RestartJob(jobID); err != nil {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
holdUntil, _ := req.Params["holdUntil"].(string)
if holdUntil == "" {
holdUntil = "indefinite"
}
if err := manager.HoldJob(int(jobIDFloat), holdUntil); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job held"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
holdUntil := params.StringOpt(req.Params, "holdUntil", "indefinite")
if err := manager.HoldJob(jobID, holdUntil); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
} }
+46 -46
View File
@@ -43,7 +43,7 @@ func TestHandleGetPrinters(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getPrinters", Method: "cups.getPrinters",
} }
@@ -68,7 +68,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getPrinters", Method: "cups.getPrinters",
} }
@@ -100,7 +100,7 @@ func TestHandleGetJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getJobs", Method: "cups.getJobs",
Params: map[string]any{ Params: map[string]any{
@@ -127,7 +127,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.getJobs", Method: "cups.getJobs",
Params: map[string]any{}, Params: map[string]any{},
@@ -152,7 +152,7 @@ func TestHandlePausePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.pausePrinter", Method: "cups.pausePrinter",
Params: map[string]any{ Params: map[string]any{
@@ -162,7 +162,7 @@ func TestHandlePausePrinter(t *testing.T) {
handlePausePrinter(conn, req, m) handlePausePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -179,7 +179,7 @@ func TestHandleResumePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.resumePrinter", Method: "cups.resumePrinter",
Params: map[string]any{ Params: map[string]any{
@@ -189,7 +189,7 @@ func TestHandleResumePrinter(t *testing.T) {
handleResumePrinter(conn, req, m) handleResumePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -206,7 +206,7 @@ func TestHandleCancelJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.cancelJob", Method: "cups.cancelJob",
Params: map[string]any{ Params: map[string]any{
@@ -216,7 +216,7 @@ func TestHandleCancelJob(t *testing.T) {
handleCancelJob(conn, req, m) handleCancelJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -233,7 +233,7 @@ func TestHandlePurgeJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.purgeJobs", Method: "cups.purgeJobs",
Params: map[string]any{ Params: map[string]any{
@@ -243,7 +243,7 @@ func TestHandlePurgeJobs(t *testing.T) {
handlePurgeJobs(conn, req, m) handlePurgeJobs(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -260,7 +260,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.unknownMethod", Method: "cups.unknownMethod",
} }
@@ -287,7 +287,7 @@ func TestHandleGetDevices(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getDevices"} req := models.Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m) handleGetDevices(conn, req, m)
var resp models.Response[[]Device] var resp models.Response[[]Device]
@@ -309,7 +309,7 @@ func TestHandleGetPPDs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getPPDs"} req := models.Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m) handleGetPPDs(conn, req, m)
var resp models.Response[[]PPD] var resp models.Response[[]PPD]
@@ -332,7 +332,7 @@ func TestHandleGetClasses(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getClasses"} req := models.Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m) handleGetClasses(conn, req, m)
var resp models.Response[[]PrinterClass] var resp models.Response[[]PrinterClass]
@@ -353,7 +353,7 @@ func TestHandleCreatePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.createPrinter", Method: "cups.createPrinter",
Params: map[string]any{ Params: map[string]any{
@@ -364,7 +364,7 @@ func TestHandleCreatePrinter(t *testing.T) {
} }
handleCreatePrinter(conn, req, m) handleCreatePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -377,7 +377,7 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}} req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m) handleCreatePrinter(conn, req, m)
var resp models.Response[any] var resp models.Response[any]
@@ -396,14 +396,14 @@ func TestHandleDeletePrinter(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.deletePrinter", Method: "cups.deletePrinter",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
} }
handleDeletePrinter(conn, req, m) handleDeletePrinter(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -419,14 +419,14 @@ func TestHandleAcceptJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.acceptJobs", Method: "cups.acceptJobs",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
} }
handleAcceptJobs(conn, req, m) handleAcceptJobs(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -442,14 +442,14 @@ func TestHandleRejectJobs(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.rejectJobs", Method: "cups.rejectJobs",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
} }
handleRejectJobs(conn, req, m) handleRejectJobs(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -465,14 +465,14 @@ func TestHandleSetPrinterShared(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.setPrinterShared", Method: "cups.setPrinterShared",
Params: map[string]any{"printerName": "printer1", "shared": true}, Params: map[string]any{"printerName": "printer1", "shared": true},
} }
handleSetPrinterShared(conn, req, m) handleSetPrinterShared(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -488,14 +488,14 @@ func TestHandleSetPrinterLocation(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.setPrinterLocation", Method: "cups.setPrinterLocation",
Params: map[string]any{"printerName": "printer1", "location": "Office"}, Params: map[string]any{"printerName": "printer1", "location": "Office"},
} }
handleSetPrinterLocation(conn, req, m) handleSetPrinterLocation(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -511,14 +511,14 @@ func TestHandleSetPrinterInfo(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.setPrinterInfo", Method: "cups.setPrinterInfo",
Params: map[string]any{"printerName": "printer1", "info": "Main Printer"}, Params: map[string]any{"printerName": "printer1", "info": "Main Printer"},
} }
handleSetPrinterInfo(conn, req, m) handleSetPrinterInfo(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -534,14 +534,14 @@ func TestHandleMoveJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.moveJob", Method: "cups.moveJob",
Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"}, Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"},
} }
handleMoveJob(conn, req, m) handleMoveJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -557,7 +557,7 @@ func TestHandlePrintTestPage(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.printTestPage", Method: "cups.printTestPage",
Params: map[string]any{"printerName": "printer1"}, Params: map[string]any{"printerName": "printer1"},
@@ -581,14 +581,14 @@ func TestHandleAddPrinterToClass(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.addPrinterToClass", Method: "cups.addPrinterToClass",
Params: map[string]any{"className": "office", "printerName": "printer1"}, Params: map[string]any{"className": "office", "printerName": "printer1"},
} }
handleAddPrinterToClass(conn, req, m) handleAddPrinterToClass(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -604,14 +604,14 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.removePrinterFromClass", Method: "cups.removePrinterFromClass",
Params: map[string]any{"className": "office", "printerName": "printer1"}, Params: map[string]any{"className": "office", "printerName": "printer1"},
} }
handleRemovePrinterFromClass(conn, req, m) handleRemovePrinterFromClass(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -627,14 +627,14 @@ func TestHandleDeleteClass(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.deleteClass", Method: "cups.deleteClass",
Params: map[string]any{"className": "office"}, Params: map[string]any{"className": "office"},
} }
handleDeleteClass(conn, req, m) handleDeleteClass(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -650,14 +650,14 @@ func TestHandleRestartJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.restartJob", Method: "cups.restartJob",
Params: map[string]any{"jobID": float64(1)}, Params: map[string]any{"jobID": float64(1)},
} }
handleRestartJob(conn, req, m) handleRestartJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -673,14 +673,14 @@ func TestHandleHoldJob(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.holdJob", Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1)}, Params: map[string]any{"jobID": float64(1)},
} }
handleHoldJob(conn, req, m) handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
@@ -696,14 +696,14 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
req := Request{ req := models.Request{
ID: 1, ID: 1,
Method: "cups.holdJob", Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"}, Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"},
} }
handleHoldJob(conn, req, m) handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp) err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, resp.Result) assert.NotNil(t, resp.Result)
+6 -12
View File
@@ -8,18 +8,12 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct { type SuccessResult struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized") models.RespondError(conn, req.ID, "dwl manager not initialized")
return return
@@ -41,12 +35,12 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() state := manager.GetState()
models.Respond(conn, req.ID, state) models.Respond(conn, req.ID, state)
} }
func handleSetTags(conn net.Conn, req Request, manager *Manager) { func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string) output, ok := req.Params["output"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -73,7 +67,7 @@ func handleSetTags(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"})
} }
func handleSetClientTags(conn net.Conn, req Request, manager *Manager) { func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string) output, ok := req.Params["output"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -100,7 +94,7 @@ func handleSetClientTags(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"})
} }
func handleSetLayout(conn net.Conn, req Request, manager *Manager) { func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
output, ok := req.Params["output"].(string) output, ok := req.Params["output"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter") models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -121,7 +115,7 @@ func handleSetLayout(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+4 -11
View File
@@ -6,22 +6,15 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
ID any `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, m *Manager) {
switch req.Method { switch req.Method {
case "evdev.getState": case "evdev.getState":
handleGetState(conn, req, m) handleGetState(conn, req, m)
default: default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method) models.RespondError(conn, req.ID, "unknown method: "+req.Method)
} }
} }
func handleGetState(conn net.Conn, req Request, m *Manager) { func handleGetState(conn net.Conn, req models.Request, m *Manager) {
state := m.GetState() models.Respond(conn, req.ID, m.GetState())
models.Respond(conn, req.ID.(int), state)
} }
+3 -3
View File
@@ -53,7 +53,7 @@ func TestHandleRequest(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "evdev.getState", Method: "evdev.getState",
Params: map[string]any{}, Params: map[string]any{},
@@ -82,7 +82,7 @@ func TestHandleRequest(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 456, ID: 456,
Method: "evdev.unknownMethod", Method: "evdev.unknownMethod",
Params: map[string]any{}, Params: map[string]any{},
@@ -111,7 +111,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 789, ID: 789,
Method: "evdev.getState", Method: "evdev.getState",
Params: map[string]any{}, Params: map[string]any{},
+9
View File
@@ -306,6 +306,15 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey] capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }
+7 -13
View File
@@ -8,18 +8,12 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct { type SuccessResult struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "extworkspace manager not initialized") models.RespondError(conn, req.ID, "extworkspace manager not initialized")
return return
@@ -43,12 +37,12 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() state := manager.GetState()
models.Respond(conn, req.ID, state) models.Respond(conn, req.ID, state)
} }
func handleActivateWorkspace(conn net.Conn, req Request, manager *Manager) { func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
groupID = "" groupID = ""
@@ -68,7 +62,7 @@ func handleActivateWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"})
} }
func handleDeactivateWorkspace(conn net.Conn, req Request, manager *Manager) { func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
groupID = "" groupID = ""
@@ -88,7 +82,7 @@ func handleDeactivateWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"})
} }
func handleRemoveWorkspace(conn net.Conn, req Request, manager *Manager) { func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
groupID = "" groupID = ""
@@ -108,7 +102,7 @@ func handleRemoveWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"})
} }
func handleCreateWorkspace(conn net.Conn, req Request, manager *Manager) { func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string) groupID, ok := req.Params["groupID"].(string)
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter") models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
@@ -129,7 +123,7 @@ func handleCreateWorkspace(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"}) models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+40 -52
View File
@@ -5,21 +5,10 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Value string `json:"value,omitempty"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "freedesktop.getState": case "freedesktop.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -44,15 +33,14 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleSetIconFile(conn net.Conn, req Request, manager *Manager) { func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
iconPath, ok := req.Params["path"].(string) iconPath, err := params.String(req.Params, "path")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'path' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -61,13 +49,13 @@ func handleSetIconFile(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon file set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
} }
func handleSetRealName(conn net.Conn, req Request, manager *Manager) { func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
name, ok := req.Params["name"].(string) name, err := params.String(req.Params, "name")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -76,13 +64,13 @@ func handleSetRealName(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "real name set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
} }
func handleSetEmail(conn net.Conn, req Request, manager *Manager) { func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
email, ok := req.Params["email"].(string) email, err := params.String(req.Params, "email")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'email' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -91,13 +79,13 @@ func handleSetEmail(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "email set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
} }
func handleSetLanguage(conn net.Conn, req Request, manager *Manager) { func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
language, ok := req.Params["language"].(string) language, err := params.String(req.Params, "language")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'language' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -106,13 +94,13 @@ func handleSetLanguage(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "language set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
} }
func handleSetLocation(conn net.Conn, req Request, manager *Manager) { func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
location, ok := req.Params["location"].(string) location, err := params.String(req.Params, "location")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -121,13 +109,13 @@ func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
} }
func handleGetUserIconFile(conn net.Conn, req Request, manager *Manager) { func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
username, ok := req.Params["username"].(string) username, err := params.String(req.Params, "username")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'username' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -137,10 +125,10 @@ func handleGetUserIconFile(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Value: iconFile}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
} }
func handleGetColorScheme(conn net.Conn, req Request, manager *Manager) { func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.updateSettingsState(); err != nil { if err := manager.updateSettingsState(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -150,10 +138,10 @@ func handleGetColorScheme(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme}) models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
} }
func handleSetIconTheme(conn net.Conn, req Request, manager *Manager) { func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
iconTheme, ok := req.Params["iconTheme"].(string) iconTheme, err := params.String(req.Params, "iconTheme")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'iconTheme' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -162,5 +150,5 @@ func handleSetIconTheme(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon theme set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon theme set"})
} }
@@ -74,10 +74,10 @@ func TestRespondError_Freedesktop(t *testing.T) {
func TestRespond_Freedesktop(t *testing.T) { func TestRespond_Freedesktop(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
result := SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -106,7 +106,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "freedesktop.getState"} req := models.Request{ID: 123, Method: "freedesktop.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -131,7 +131,7 @@ func TestHandleSetIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{}, Params: map[string]any{},
@@ -164,7 +164,7 @@ func TestHandleSetIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{ Params: map[string]any{
@@ -174,7 +174,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -196,7 +196,7 @@ func TestHandleSetIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{ Params: map[string]any{
@@ -206,7 +206,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -223,7 +223,7 @@ func TestHandleSetRealName(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setRealName", Method: "freedesktop.accounts.setRealName",
Params: map[string]any{}, Params: map[string]any{},
@@ -256,7 +256,7 @@ func TestHandleSetRealName(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setRealName", Method: "freedesktop.accounts.setRealName",
Params: map[string]any{ Params: map[string]any{
@@ -266,7 +266,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager) handleSetRealName(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -286,7 +286,7 @@ func TestHandleSetEmail(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setEmail", Method: "freedesktop.accounts.setEmail",
Params: map[string]any{}, Params: map[string]any{},
@@ -319,7 +319,7 @@ func TestHandleSetEmail(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setEmail", Method: "freedesktop.accounts.setEmail",
Params: map[string]any{ Params: map[string]any{
@@ -329,7 +329,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager) handleSetEmail(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -349,7 +349,7 @@ func TestHandleSetLanguage(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setLanguage", Method: "freedesktop.accounts.setLanguage",
Params: map[string]any{}, Params: map[string]any{},
@@ -374,7 +374,7 @@ func TestHandleSetLocation(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setLocation", Method: "freedesktop.accounts.setLocation",
Params: map[string]any{}, Params: map[string]any{},
@@ -399,7 +399,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.getUserIconFile", Method: "freedesktop.accounts.getUserIconFile",
Params: map[string]any{}, Params: map[string]any{},
@@ -426,7 +426,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.getUserIconFile", Method: "freedesktop.accounts.getUserIconFile",
Params: map[string]any{ Params: map[string]any{
@@ -436,7 +436,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager) handleGetUserIconFile(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -457,7 +457,7 @@ func TestHandleGetColorScheme(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager) handleGetColorScheme(conn, req, manager)
@@ -488,7 +488,7 @@ func TestHandleGetColorScheme(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager) handleGetColorScheme(conn, req, manager)
@@ -516,7 +516,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.unknown", Method: "freedesktop.unknown",
} }
@@ -533,7 +533,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.getState", Method: "freedesktop.getState",
} }
@@ -561,7 +561,7 @@ func TestHandleRequest(t *testing.T) {
for _, method := range tests { for _, method := range tests {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: method, Method: method,
Params: map[string]any{}, Params: map[string]any{},
+30 -41
View File
@@ -6,20 +6,10 @@ import (
"net" "net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "loginctl.getState": case "loginctl.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -46,39 +36,38 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleLock(conn net.Conn, req Request, manager *Manager) { func handleLock(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Lock(); err != nil { if err := manager.Lock(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "locked"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked"})
} }
func handleUnlock(conn net.Conn, req Request, manager *Manager) { func handleUnlock(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Unlock(); err != nil { if err := manager.Unlock(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "unlocked"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "unlocked"})
} }
func handleActivate(conn net.Conn, req Request, manager *Manager) { func handleActivate(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Activate(); err != nil { if err := manager.Activate(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "activated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "activated"})
} }
func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) { func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
idle, ok := req.Params["idle"].(bool) idle, err := params.Bool(req.Params, "idle")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'idle' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -86,32 +75,32 @@ func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "idle hint set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
} }
func handleSetLockBeforeSuspend(conn net.Conn, req Request, manager *Manager) { func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool) enabled, err := params.Bool(req.Params, "enabled")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetLockBeforeSuspend(enabled) manager.SetLockBeforeSuspend(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "lock before suspend set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"})
} }
func handleSetSleepInhibitorEnabled(conn net.Conn, req Request, manager *Manager) { func handleSetSleepInhibitorEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool) enabled, err := params.Bool(req.Params, "enabled")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetSleepInhibitorEnabled(enabled) manager.SetSleepInhibitorEnabled(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sleep inhibitor setting updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
} }
func handleLockerReady(conn net.Conn, req Request, manager *Manager) { func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) {
manager.lockTimerMu.Lock() manager.lockTimerMu.Lock()
if manager.lockTimer != nil { if manager.lockTimer != nil {
manager.lockTimer.Stop() manager.lockTimer.Stop()
@@ -125,18 +114,18 @@ func handleLockerReady(conn net.Conn, req Request, manager *Manager) {
if manager.inSleepCycle.Load() { if manager.inSleepCycle.Load() {
manager.signalLockerReady() manager.signalLockerReady()
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "ok"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "ok"})
} }
func handleTerminate(conn net.Conn, req Request, manager *Manager) { func handleTerminate(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Terminate(); err != nil { if err := manager.Terminate(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "terminated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "terminated"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+28 -28
View File
@@ -58,10 +58,10 @@ func TestRespondError_Loginctl(t *testing.T) {
func TestRespond_Loginctl(t *testing.T) { func TestRespond_Loginctl(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
result := SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -86,7 +86,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.getState"} req := models.Request{ID: 123, Method: "loginctl.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -115,10 +115,10 @@ func TestHandleLock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.lock"} req := models.Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager) handleLock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -141,10 +141,10 @@ func TestHandleLock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.lock"} req := models.Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager) handleLock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -166,10 +166,10 @@ func TestHandleUnlock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.unlock"} req := models.Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager) handleUnlock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -192,10 +192,10 @@ func TestHandleUnlock(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.unlock"} req := models.Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager) handleUnlock(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -217,10 +217,10 @@ func TestHandleActivate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.activate"} req := models.Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager) handleActivate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -243,10 +243,10 @@ func TestHandleActivate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.activate"} req := models.Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager) handleActivate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -263,7 +263,7 @@ func TestHandleSetIdleHint(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.setIdleHint", Method: "loginctl.setIdleHint",
Params: map[string]any{}, Params: map[string]any{},
@@ -291,7 +291,7 @@ func TestHandleSetIdleHint(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.setIdleHint", Method: "loginctl.setIdleHint",
Params: map[string]any{ Params: map[string]any{
@@ -301,7 +301,7 @@ func TestHandleSetIdleHint(t *testing.T) {
handleSetIdleHint(conn, req, manager) handleSetIdleHint(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -324,7 +324,7 @@ func TestHandleSetIdleHint(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.setIdleHint", Method: "loginctl.setIdleHint",
Params: map[string]any{ Params: map[string]any{
@@ -334,7 +334,7 @@ func TestHandleSetIdleHint(t *testing.T) {
handleSetIdleHint(conn, req, manager) handleSetIdleHint(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -356,10 +356,10 @@ func TestHandleTerminate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.terminate"} req := models.Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager) handleTerminate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -382,10 +382,10 @@ func TestHandleTerminate(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.terminate"} req := models.Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager) handleTerminate(conn, req, manager)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -405,7 +405,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.unknown", Method: "loginctl.unknown",
} }
@@ -422,7 +422,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.getState", Method: "loginctl.getState",
} }
@@ -445,7 +445,7 @@ func TestHandleRequest(t *testing.T) {
manager.sessionObj = mockSessionObj manager.sessionObj = mockSessionObj
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "loginctl.lock", Method: "loginctl.lock",
} }
@@ -470,7 +470,7 @@ func TestHandleSubscribe(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.subscribe"} req := models.Request{ID: 123, Method: "loginctl.subscribe"}
done := make(chan bool) done := make(chan bool)
go func() { go func() {
+6
View File
@@ -29,3 +29,9 @@ func Respond[T any](conn net.Conn, id int, result T) {
resp := Response[T]{ID: id, Result: &result} resp := Response[T]{ID: id, Result: &result}
json.NewEncoder(conn).Encode(resp) json.NewEncoder(conn).Encode(resp)
} }
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Value string `json:"value,omitempty"`
}
+123 -210
View File
@@ -7,20 +7,10 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method { switch req.Method {
case "network.getState": case "network.getState":
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -89,32 +79,22 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleCredentialsSubmit(conn net.Conn, req Request, manager *Manager) { func handleCredentialsSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
log.Warnf("handleCredentialsSubmit: missing or invalid token parameter") log.Warnf("handleCredentialsSubmit: missing or invalid token parameter")
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
secretsRaw, ok := req.Params["secrets"].(map[string]any) secrets, err := params.StringMap(req.Params, "secrets")
if !ok { if err != nil {
log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter") log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter")
models.RespondError(conn, req.ID, "missing or invalid 'secrets' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
secrets := make(map[string]string) save := params.BoolOpt(req.Params, "save", true)
for k, v := range secretsRaw {
if str, ok := v.(string); ok {
secrets[k] = str
}
}
save := true
if saveParam, ok := req.Params["save"].(bool); ok {
save = saveParam
}
if err := manager.SubmitCredentials(token, secrets, save); err != nil { if err := manager.SubmitCredentials(token, secrets, save); err != nil {
log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err) log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err)
@@ -123,13 +103,13 @@ func handleCredentialsSubmit(conn net.Conn, req Request, manager *Manager) {
} }
log.Infof("handleCredentialsSubmit: credentials submitted successfully") log.Infof("handleCredentialsSubmit: credentials submitted successfully")
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials submitted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials submitted"})
} }
func handleCredentialsCancel(conn net.Conn, req Request, manager *Manager) { func handleCredentialsCancel(conn net.Conn, req models.Request, manager *Manager) {
token, ok := req.Params["token"].(string) token, err := params.String(req.Params, "token")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -138,16 +118,15 @@ func handleCredentialsCancel(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials cancelled"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials cancelled"})
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleScanWiFi(conn net.Conn, req Request, manager *Manager) { func handleScanWiFi(conn net.Conn, req models.Request, manager *Manager) {
device, _ := req.Params["device"].(string) device := params.StringOpt(req.Params, "device", "")
var err error var err error
if device != "" { if device != "" {
err = manager.ScanWiFiDevice(device) err = manager.ScanWiFiDevice(device)
@@ -158,33 +137,25 @@ func handleScanWiFi(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "scanning"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "scanning"})
} }
func handleGetWiFiNetworks(conn net.Conn, req Request, manager *Manager) { func handleGetWiFiNetworks(conn net.Conn, req models.Request, manager *Manager) {
networks := manager.GetWiFiNetworks() models.Respond(conn, req.ID, manager.GetWiFiNetworks())
models.Respond(conn, req.ID, networks)
} }
func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) { func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
var connReq ConnectionRequest var connReq ConnectionRequest
connReq.SSID = ssid connReq.SSID = ssid
connReq.Password = params.StringOpt(req.Params, "password", "")
if password, ok := req.Params["password"].(string); ok { connReq.Username = params.StringOpt(req.Params, "username", "")
connReq.Password = password connReq.Device = params.StringOpt(req.Params, "device", "")
}
if username, ok := req.Params["username"].(string); ok {
connReq.Username = username
}
if device, ok := req.Params["device"].(string); ok {
connReq.Device = device
}
if interactive, ok := req.Params["interactive"].(bool); ok { if interactive, ok := req.Params["interactive"].(bool); ok {
connReq.Interactive = interactive connReq.Interactive = interactive
@@ -206,27 +177,14 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
} }
} }
if anonymousIdentity, ok := req.Params["anonymousIdentity"].(string); ok { connReq.AnonymousIdentity = params.StringOpt(req.Params, "anonymousIdentity", "")
connReq.AnonymousIdentity = anonymousIdentity connReq.DomainSuffixMatch = params.StringOpt(req.Params, "domainSuffixMatch", "")
} connReq.EAPMethod = params.StringOpt(req.Params, "eapMethod", "")
if domainSuffixMatch, ok := req.Params["domainSuffixMatch"].(string); ok { connReq.Phase2Auth = params.StringOpt(req.Params, "phase2Auth", "")
connReq.DomainSuffixMatch = domainSuffixMatch connReq.CACertPath = params.StringOpt(req.Params, "caCertPath", "")
} connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "")
if eapMethod, ok := req.Params["eapMethod"].(string); ok { connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "")
connReq.EAPMethod = eapMethod
}
if phase2Auth, ok := req.Params["phase2Auth"].(string); ok {
connReq.Phase2Auth = phase2Auth
}
if caCertPath, ok := req.Params["caCertPath"].(string); ok {
connReq.CACertPath = caCertPath
}
if clientCertPath, ok := req.Params["clientCertPath"].(string); ok {
connReq.ClientCertPath = clientCertPath
}
if privateKeyPath, ok := req.Params["privateKeyPath"].(string); ok {
connReq.PrivateKeyPath = privateKeyPath
}
if useSystemCACerts, ok := req.Params["useSystemCACerts"].(bool); ok { if useSystemCACerts, ok := req.Params["useSystemCACerts"].(bool); ok {
connReq.UseSystemCACerts = &useSystemCACerts connReq.UseSystemCACerts = &useSystemCACerts
} }
@@ -236,11 +194,11 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) { func handleDisconnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
device, _ := req.Params["device"].(string) device := params.StringOpt(req.Params, "device", "")
var err error var err error
if device != "" { if device != "" {
err = manager.DisconnectWiFiDevice(device) err = manager.DisconnectWiFiDevice(device)
@@ -251,13 +209,13 @@ func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
} }
func handleForgetWiFi(conn net.Conn, req Request, manager *Manager) { func handleForgetWiFi(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -266,10 +224,10 @@ func handleForgetWiFi(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "forgotten"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "forgotten"})
} }
func handleToggleWiFi(conn net.Conn, req Request, manager *Manager) { func handleToggleWiFi(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.ToggleWiFi(); err != nil { if err := manager.ToggleWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -279,7 +237,7 @@ func handleToggleWiFi(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled}) models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled})
} }
func handleEnableWiFi(conn net.Conn, req Request, manager *Manager) { func handleEnableWiFi(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.EnableWiFi(); err != nil { if err := manager.EnableWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -287,7 +245,7 @@ func handleEnableWiFi(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": true}) models.Respond(conn, req.ID, map[string]bool{"enabled": true})
} }
func handleDisableWiFi(conn net.Conn, req Request, manager *Manager) { func handleDisableWiFi(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.DisableWiFi(); err != nil { if err := manager.DisableWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
@@ -295,29 +253,29 @@ func handleDisableWiFi(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": false}) models.Respond(conn, req.ID, map[string]bool{"enabled": false})
} }
func handleConnectEthernetSpecificConfig(conn net.Conn, req Request, manager *Manager) { func handleConnectEthernetSpecificConfig(conn net.Conn, req models.Request, manager *Manager) {
uuid, ok := req.Params["uuid"].(string) uuid, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
if err := manager.activateConnection(uuid); err != nil { if err := manager.activateConnection(uuid); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleConnectEthernet(conn net.Conn, req Request, manager *Manager) { func handleConnectEthernet(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.ConnectEthernet(); err != nil { if err := manager.ConnectEthernet(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
} }
func handleDisconnectEthernet(conn net.Conn, req Request, manager *Manager) { func handleDisconnectEthernet(conn net.Conn, req models.Request, manager *Manager) {
device, _ := req.Params["device"].(string) device := params.StringOpt(req.Params, "device", "")
var err error var err error
if device != "" { if device != "" {
err = manager.DisconnectEthernetDevice(device) err = manager.DisconnectEthernetDevice(device)
@@ -328,13 +286,13 @@ func handleDisconnectEthernet(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
} }
func handleSetPreference(conn net.Conn, req Request, manager *Manager) { func handleSetPreference(conn net.Conn, req models.Request, manager *Manager) {
preference, ok := req.Params["preference"].(string) preference, err := params.String(req.Params, "preference")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'preference' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -346,10 +304,10 @@ func handleSetPreference(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]string{"preference": preference}) models.Respond(conn, req.ID, map[string]string{"preference": preference})
} }
func handleGetNetworkInfo(conn net.Conn, req Request, manager *Manager) { func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -362,10 +320,10 @@ func handleGetNetworkInfo(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, network) models.Respond(conn, req.ID, network)
} }
func handleGetWiredNetworkInfo(conn net.Conn, req Request, manager *Manager) { func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
uuid, ok := req.Params["uuid"].(string) uuid, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -378,7 +336,7 @@ func handleGetWiredNetworkInfo(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, network) models.Respond(conn, req.ID, network)
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
@@ -408,7 +366,7 @@ func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleListVPNProfiles(conn net.Conn, req Request, manager *Manager) { func handleListVPNProfiles(conn net.Conn, req models.Request, manager *Manager) {
profiles, err := manager.ListVPNProfiles() profiles, err := manager.ListVPNProfiles()
if err != nil { if err != nil {
log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err) log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err)
@@ -419,7 +377,7 @@ func handleListVPNProfiles(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, profiles) models.Respond(conn, req.ID, profiles)
} }
func handleListActiveVPN(conn net.Conn, req Request, manager *Manager) { func handleListActiveVPN(conn net.Conn, req models.Request, manager *Manager) {
active, err := manager.ListActiveVPN() active, err := manager.ListActiveVPN()
if err != nil { if err != nil {
log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err) log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err)
@@ -430,27 +388,15 @@ func handleListActiveVPN(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, active) models.Respond(conn, req.ID, active)
} }
func handleConnectVPN(conn net.Conn, req Request, manager *Manager) { func handleConnectVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuidOrName"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid")
if !ok { if !ok {
name, nameOk := req.Params["name"].(string) log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter")
uuid, uuidOk := req.Params["uuid"].(string) models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
if nameOk { return
uuidOrName = name
} else if uuidOk {
uuidOrName = uuid
} else {
log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
return
}
} }
// Default to true - only allow one VPN connection at a time singleActive := params.BoolOpt(req.Params, "singleActive", true)
singleActive := true
if sa, ok := req.Params["singleActive"].(bool); ok {
singleActive = sa
}
if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil { if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil {
log.Warnf("handleConnectVPN: failed to connect: %v", err) log.Warnf("handleConnectVPN: failed to connect: %v", err)
@@ -458,23 +404,15 @@ func handleConnectVPN(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN connection initiated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN connection initiated"})
} }
func handleDisconnectVPN(conn net.Conn, req Request, manager *Manager) { func handleDisconnectVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuidOrName"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid")
if !ok { if !ok {
name, nameOk := req.Params["name"].(string) log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter")
uuid, uuidOk := req.Params["uuid"].(string) models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
if nameOk { return
uuidOrName = name
} else if uuidOk {
uuidOrName = uuid
} else {
log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
return
}
} }
if err := manager.DisconnectVPN(uuidOrName); err != nil { if err := manager.DisconnectVPN(uuidOrName); err != nil {
@@ -483,27 +421,21 @@ func handleDisconnectVPN(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN disconnected"})
} }
func handleDisconnectAllVPN(conn net.Conn, req Request, manager *Manager) { func handleDisconnectAllVPN(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.DisconnectAllVPN(); err != nil { if err := manager.DisconnectAllVPN(); err != nil {
log.Warnf("handleDisconnectAllVPN: failed: %v", err) log.Warnf("handleDisconnectAllVPN: failed: %v", err)
models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err))
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "All VPNs disconnected"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "All VPNs disconnected"})
} }
func handleClearVPNCredentials(conn net.Conn, req Request, manager *Manager) { func handleClearVPNCredentials(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok { if !ok {
log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter") log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter") models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter")
@@ -516,19 +448,19 @@ func handleClearVPNCredentials(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials cleared"})
} }
func handleSetWiFiAutoconnect(conn net.Conn, req Request, manager *Manager) { func handleSetWiFiAutoconnect(conn net.Conn, req models.Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string) ssid, err := params.String(req.Params, "ssid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
autoconnect, ok := req.Params["autoconnect"].(bool) autoconnect, err := params.Bool(req.Params, "autoconnect")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'autoconnect' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -537,10 +469,10 @@ func handleSetWiFiAutoconnect(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "autoconnect updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "autoconnect updated"})
} }
func handleListVPNPlugins(conn net.Conn, req Request, manager *Manager) { func handleListVPNPlugins(conn net.Conn, req models.Request, manager *Manager) {
plugins, err := manager.ListVPNPlugins() plugins, err := manager.ListVPNPlugins()
if err != nil { if err != nil {
log.Warnf("handleListVPNPlugins: failed to list plugins: %v", err) log.Warnf("handleListVPNPlugins: failed to list plugins: %v", err)
@@ -551,17 +483,14 @@ func handleListVPNPlugins(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, plugins) models.Respond(conn, req.ID, plugins)
} }
func handleImportVPN(conn net.Conn, req Request, manager *Manager) { func handleImportVPN(conn net.Conn, req models.Request, manager *Manager) {
filePath, ok := req.Params["file"].(string) filePath, ok := params.StringAlt(req.Params, "file", "path")
if !ok {
filePath, ok = req.Params["path"].(string)
}
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'file' or 'path' parameter") models.RespondError(conn, req.ID, "missing 'file' or 'path' parameter")
return return
} }
name, _ := req.Params["name"].(string) name := params.StringOpt(req.Params, "name", "")
result, err := manager.ImportVPN(filePath, name) result, err := manager.ImportVPN(filePath, name)
if err != nil { if err != nil {
@@ -573,14 +502,8 @@ func handleImportVPN(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, result) models.Respond(conn, req.ID, result)
} }
func handleGetVPNConfig(conn net.Conn, req Request, manager *Manager) { func handleGetVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter") models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter")
return return
@@ -596,10 +519,10 @@ func handleGetVPNConfig(conn net.Conn, req Request, manager *Manager) {
models.Respond(conn, req.ID, config) models.Respond(conn, req.ID, config)
} }
func handleUpdateVPNConfig(conn net.Conn, req Request, manager *Manager) { func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
connUUID, ok := req.Params["uuid"].(string) connUUID, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -626,17 +549,11 @@ func handleUpdateVPNConfig(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN config updated"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN config updated"})
} }
func handleDeleteVPN(conn net.Conn, req Request, manager *Manager) { func handleDeleteVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string) uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter") models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter")
return return
@@ -648,23 +565,19 @@ func handleDeleteVPN(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN deleted"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN deleted"})
} }
func handleSetVPNCredentials(conn net.Conn, req Request, manager *Manager) { func handleSetVPNCredentials(conn net.Conn, req models.Request, manager *Manager) {
connUUID, ok := req.Params["uuid"].(string) connUUID, err := params.String(req.Params, "uuid")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing 'uuid' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
username, _ := req.Params["username"].(string) username := params.StringOpt(req.Params, "username", "")
password, _ := req.Params["password"].(string) password := params.StringOpt(req.Params, "password", "")
save := params.BoolOpt(req.Params, "save", true)
save := true
if saveParam, ok := req.Params["save"].(bool); ok {
save = saveParam
}
if err := manager.SetVPNCredentials(connUUID, username, password, save); err != nil { if err := manager.SetVPNCredentials(connUUID, username, password, save); err != nil {
log.Warnf("handleSetVPNCredentials: failed to set credentials: %v", err) log.Warnf("handleSetVPNCredentials: failed to set credentials: %v", err)
@@ -672,5 +585,5 @@ func handleSetVPNCredentials(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials set"})
} }
@@ -53,10 +53,10 @@ func TestRespondError_Network(t *testing.T) {
func TestRespond_Network(t *testing.T) { func TestRespond_Network(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
result := SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
@@ -77,7 +77,7 @@ func TestHandleGetState(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "network.getState"} req := models.Request{ID: 123, Method: "network.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
@@ -103,7 +103,7 @@ func TestHandleGetWiFiNetworks(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ID: 123, Method: "network.wifi.networks"} req := models.Request{ID: 123, Method: "network.wifi.networks"}
handleGetWiFiNetworks(conn, req, manager) handleGetWiFiNetworks(conn, req, manager)
@@ -125,7 +125,7 @@ func TestHandleConnectWiFi(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.wifi.connect", Method: "network.wifi.connect",
Params: map[string]any{}, Params: map[string]any{},
@@ -149,7 +149,7 @@ func TestHandleSetPreference(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.preference.set", Method: "network.preference.set",
Params: map[string]any{}, Params: map[string]any{},
@@ -173,7 +173,7 @@ func TestHandleGetNetworkInfo(t *testing.T) {
} }
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.info", Method: "network.info",
Params: map[string]any{}, Params: map[string]any{},
@@ -199,7 +199,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.unknown", Method: "network.unknown",
} }
@@ -216,7 +216,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn() conn := newMockNetConn()
req := Request{ req := models.Request{
ID: 123, ID: 123,
Method: "network.getState", Method: "network.getState",
} }
+113
View File
@@ -0,0 +1,113 @@
package params
import "fmt"
func Get[T any](params map[string]any, key string) (T, error) {
val, ok := params[key].(T)
if !ok {
var zero T
return zero, fmt.Errorf("missing or invalid '%s' parameter", key)
}
return val, nil
}
func GetOpt[T any](params map[string]any, key string, def T) T {
if val, ok := params[key].(T); ok {
return val
}
return def
}
func String(params map[string]any, key string) (string, error) {
return Get[string](params, key)
}
func StringNonEmpty(params map[string]any, key string) (string, error) {
val, err := Get[string](params, key)
if err != nil || val == "" {
return "", fmt.Errorf("missing or invalid '%s' parameter", key)
}
return val, nil
}
func StringOpt(params map[string]any, key string, def string) string {
return GetOpt(params, key, def)
}
func Int(params map[string]any, key string) (int, error) {
val, err := Get[float64](params, key)
if err != nil {
return 0, err
}
return int(val), nil
}
func IntOpt(params map[string]any, key string, def int) int {
if val, ok := params[key].(float64); ok {
return int(val)
}
return def
}
func Float(params map[string]any, key string) (float64, error) {
return Get[float64](params, key)
}
func FloatOpt(params map[string]any, key string, def float64) float64 {
return GetOpt(params, key, def)
}
func Bool(params map[string]any, key string) (bool, error) {
return Get[bool](params, key)
}
func BoolOpt(params map[string]any, key string, def bool) bool {
return GetOpt(params, key, def)
}
func StringMap(params map[string]any, key string) (map[string]string, error) {
rawMap, err := Get[map[string]any](params, key)
if err != nil {
return nil, err
}
result := make(map[string]string)
for k, v := range rawMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result, nil
}
func StringMapOpt(params map[string]any, key string) map[string]string {
rawMap, ok := params[key].(map[string]any)
if !ok {
return nil
}
result := make(map[string]string)
for k, v := range rawMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result
}
func Any(params map[string]any, key string) (any, bool) {
val, ok := params[key]
return val, ok
}
func AnyMap(params map[string]any, key string) (map[string]any, bool) {
val, ok := params[key].(map[string]any)
return val, ok
}
func StringAlt(params map[string]any, keys ...string) (string, bool) {
for _, key := range keys {
if val, ok := params[key].(string); ok {
return val, true
}
}
return "", false
}
+154
View File
@@ -0,0 +1,154 @@
package params
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGet(t *testing.T) {
p := map[string]any{"key": "value"}
val, err := Get[string](p, "key")
assert.NoError(t, err)
assert.Equal(t, "value", val)
_, err = Get[string](p, "missing")
assert.Error(t, err)
_, err = Get[int](p, "key")
assert.Error(t, err)
}
func TestGetOpt(t *testing.T) {
p := map[string]any{"key": "value"}
assert.Equal(t, "value", GetOpt(p, "key", "default"))
assert.Equal(t, "default", GetOpt(p, "missing", "default"))
}
func TestString(t *testing.T) {
p := map[string]any{"s": "hello", "n": 123}
val, err := String(p, "s")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
_, err = String(p, "n")
assert.Error(t, err)
}
func TestStringNonEmpty(t *testing.T) {
p := map[string]any{"s": "hello", "empty": ""}
val, err := StringNonEmpty(p, "s")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
_, err = StringNonEmpty(p, "empty")
assert.Error(t, err)
_, err = StringNonEmpty(p, "missing")
assert.Error(t, err)
}
func TestStringOpt(t *testing.T) {
p := map[string]any{"s": "hello"}
assert.Equal(t, "hello", StringOpt(p, "s", "default"))
assert.Equal(t, "default", StringOpt(p, "missing", "default"))
}
func TestInt(t *testing.T) {
p := map[string]any{"n": float64(42), "s": "str"}
val, err := Int(p, "n")
assert.NoError(t, err)
assert.Equal(t, 42, val)
_, err = Int(p, "s")
assert.Error(t, err)
}
func TestIntOpt(t *testing.T) {
p := map[string]any{"n": float64(42)}
assert.Equal(t, 42, IntOpt(p, "n", 0))
assert.Equal(t, 99, IntOpt(p, "missing", 99))
}
func TestFloat(t *testing.T) {
p := map[string]any{"f": 3.14, "s": "str"}
val, err := Float(p, "f")
assert.NoError(t, err)
assert.Equal(t, 3.14, val)
_, err = Float(p, "s")
assert.Error(t, err)
}
func TestFloatOpt(t *testing.T) {
p := map[string]any{"f": 3.14}
assert.Equal(t, 3.14, FloatOpt(p, "f", 0))
assert.Equal(t, 1.0, FloatOpt(p, "missing", 1.0))
}
func TestBool(t *testing.T) {
p := map[string]any{"b": true, "s": "str"}
val, err := Bool(p, "b")
assert.NoError(t, err)
assert.True(t, val)
_, err = Bool(p, "s")
assert.Error(t, err)
}
func TestBoolOpt(t *testing.T) {
p := map[string]any{"b": true}
assert.True(t, BoolOpt(p, "b", false))
assert.True(t, BoolOpt(p, "missing", true))
}
func TestStringMap(t *testing.T) {
p := map[string]any{
"m": map[string]any{"a": "1", "b": "2", "c": 3},
}
val, err := StringMap(p, "m")
assert.NoError(t, err)
assert.Equal(t, map[string]string{"a": "1", "b": "2"}, val)
_, err = StringMap(p, "missing")
assert.Error(t, err)
}
func TestStringMapOpt(t *testing.T) {
p := map[string]any{
"m": map[string]any{"a": "1"},
}
assert.Equal(t, map[string]string{"a": "1"}, StringMapOpt(p, "m"))
assert.Nil(t, StringMapOpt(p, "missing"))
}
func TestAny(t *testing.T) {
p := map[string]any{"k": 123}
val, ok := Any(p, "k")
assert.True(t, ok)
assert.Equal(t, 123, val)
_, ok = Any(p, "missing")
assert.False(t, ok)
}
func TestAnyMap(t *testing.T) {
inner := map[string]any{"nested": true}
p := map[string]any{"m": inner}
val, ok := AnyMap(p, "m")
assert.True(t, ok)
assert.Equal(t, inner, val)
_, ok = AnyMap(p, "missing")
assert.False(t, ok)
}
func TestStringAlt(t *testing.T) {
p := map[string]any{"b": "found"}
val, ok := StringAlt(p, "a", "b", "c")
assert.True(t, ok)
assert.Equal(t, "found", val)
_, ok = StringAlt(p, "x", "y")
assert.False(t, ok)
}
+12 -72
View File
@@ -27,12 +27,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "network manager not initialized") models.RespondError(conn, req.ID, "network manager not initialized")
return return
} }
netReq := network.Request{ network.HandleRequest(conn, req, networkManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
network.HandleRequest(conn, netReq, networkManager)
return return
} }
@@ -46,12 +41,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "loginctl manager not initialized") models.RespondError(conn, req.ID, "loginctl manager not initialized")
return return
} }
loginReq := loginctl.Request{ loginctl.HandleRequest(conn, req, loginctlManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
loginctl.HandleRequest(conn, loginReq, loginctlManager)
return return
} }
@@ -60,12 +50,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "freedesktop manager not initialized") models.RespondError(conn, req.ID, "freedesktop manager not initialized")
return return
} }
freedeskReq := freedesktop.Request{ freedesktop.HandleRequest(conn, req, freedesktopManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
freedesktop.HandleRequest(conn, freedeskReq, freedesktopManager)
return return
} }
@@ -74,12 +59,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "wayland manager not initialized") models.RespondError(conn, req.ID, "wayland manager not initialized")
return return
} }
waylandReq := wayland.Request{ wayland.HandleRequest(conn, req, waylandManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
wayland.HandleRequest(conn, waylandReq, waylandManager)
return return
} }
@@ -88,12 +68,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "bluetooth manager not initialized") models.RespondError(conn, req.ID, "bluetooth manager not initialized")
return return
} }
bluezReq := bluez.Request{ bluez.HandleRequest(conn, req, bluezManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
bluez.HandleRequest(conn, bluezReq, bluezManager)
return return
} }
@@ -102,12 +77,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "apppicker manager not initialized") models.RespondError(conn, req.ID, "apppicker manager not initialized")
return return
} }
appPickerReq := apppicker.Request{ apppicker.HandleRequest(conn, req, appPickerManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
apppicker.HandleRequest(conn, appPickerReq, appPickerManager)
return return
} }
@@ -116,12 +86,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "CUPS manager not initialized") models.RespondError(conn, req.ID, "CUPS manager not initialized")
return return
} }
cupsReq := cups.Request{ cups.HandleRequest(conn, req, cupsManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
cups.HandleRequest(conn, cupsReq, cupsManager)
return return
} }
@@ -130,12 +95,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "dwl manager not initialized") models.RespondError(conn, req.ID, "dwl manager not initialized")
return return
} }
dwlReq := dwl.Request{ dwl.HandleRequest(conn, req, dwlManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
dwl.HandleRequest(conn, dwlReq, dwlManager)
return return
} }
@@ -144,12 +104,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "brightness manager not initialized") models.RespondError(conn, req.ID, "brightness manager not initialized")
return return
} }
brightnessReq := brightness.Request{ brightness.HandleRequest(conn, req, brightnessManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
brightness.HandleRequest(conn, brightnessReq, brightnessManager)
return return
} }
@@ -170,12 +125,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
} }
extWorkspaceReq := extworkspace.Request{ extworkspace.HandleRequest(conn, req, extWorkspaceManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
extworkspace.HandleRequest(conn, extWorkspaceReq, extWorkspaceManager)
return return
} }
@@ -184,12 +134,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "wlroutput manager not initialized") models.RespondError(conn, req.ID, "wlroutput manager not initialized")
return return
} }
wlrOutputReq := wlroutput.Request{ wlroutput.HandleRequest(conn, req, wlrOutputManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
wlroutput.HandleRequest(conn, wlrOutputReq, wlrOutputManager)
return return
} }
@@ -198,12 +143,7 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "evdev manager not initialized") models.RespondError(conn, req.ID, "evdev manager not initialized")
return return
} }
evdevReq := evdev.Request{ evdev.HandleRequest(conn, req, evdevManager)
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
evdev.HandleRequest(conn, evdevReq, evdevManager)
return return
} }
+125 -46
View File
@@ -2,8 +2,6 @@ package wayland
import ( import (
"math" "math"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type GammaRamp struct { type GammaRamp struct {
@@ -12,6 +10,126 @@ type GammaRamp struct {
Blue []uint16 Blue []uint16
} }
type rgb struct {
r, g, b float64
}
type xyz struct {
x, y, z float64
}
func illuminantD(temp int) (float64, float64, bool) {
var x float64
switch {
case temp >= 2500 && temp <= 7000:
t := float64(temp)
x = 0.244063 + 0.09911e3/t + 2.9678e6/(t*t) - 4.6070e9/(t*t*t)
case temp > 7000 && temp <= 25000:
t := float64(temp)
x = 0.237040 + 0.24748e3/t + 1.9018e6/(t*t) - 2.0064e9/(t*t*t)
default:
return 0, 0, false
}
y := -3*(x*x) + 2.870*x - 0.275
return x, y, true
}
func planckianLocus(temp int) (float64, float64, bool) {
var x, y float64
switch {
case temp >= 1667 && temp <= 4000:
t := float64(temp)
x = -0.2661239e9/(t*t*t) - 0.2343589e6/(t*t) + 0.8776956e3/t + 0.179910
if temp <= 2222 {
y = -1.1064814*(x*x*x) - 1.34811020*(x*x) + 2.18555832*x - 0.20219683
} else {
y = -0.9549476*(x*x*x) - 1.37418593*(x*x) + 2.09137015*x - 0.16748867
}
case temp > 4000 && temp < 25000:
t := float64(temp)
x = -3.0258469e9/(t*t*t) + 2.1070379e6/(t*t) + 0.2226347e3/t + 0.240390
y = 3.0817580*(x*x*x) - 5.87338670*(x*x) + 3.75112997*x - 0.37001483
default:
return 0, 0, false
}
return x, y, true
}
func srgbGamma(value, gamma float64) float64 {
if value <= 0.0031308 {
return 12.92 * value
}
return math.Pow(1.055*value, 1.0/gamma) - 0.055
}
func clamp01(v float64) float64 {
switch {
case v > 1.0:
return 1.0
case v < 0.0:
return 0.0
default:
return v
}
}
func xyzToSRGB(c xyz) rgb {
return rgb{
r: srgbGamma(clamp01(3.2404542*c.x-1.5371385*c.y-0.4985314*c.z), 2.2),
g: srgbGamma(clamp01(-0.9692660*c.x+1.8760108*c.y+0.0415560*c.z), 2.2),
b: srgbGamma(clamp01(0.0556434*c.x-0.2040259*c.y+1.0572252*c.z), 2.2),
}
}
func normalizeRGB(c *rgb) {
maxw := math.Max(c.r, math.Max(c.g, c.b))
if maxw > 0 {
c.r /= maxw
c.g /= maxw
c.b /= maxw
}
}
func calcWhitepoint(temp int) rgb {
if temp == 6500 {
return rgb{r: 1.0, g: 1.0, b: 1.0}
}
var wp xyz
switch {
case temp >= 25000:
x, y, _ := illuminantD(25000)
wp.x = x
wp.y = y
case temp >= 4000:
x, y, _ := illuminantD(temp)
wp.x = x
wp.y = y
case temp >= 2500:
x1, y1, _ := illuminantD(temp)
x2, y2, _ := planckianLocus(temp)
factor := float64(4000-temp) / 1500.0
sineFactor := (math.Cos(math.Pi*factor) + 1.0) / 2.0
wp.x = x1*sineFactor + x2*(1.0-sineFactor)
wp.y = y1*sineFactor + y2*(1.0-sineFactor)
default:
t := temp
if t < 1667 {
t = 1667
}
x, y, _ := planckianLocus(t)
wp.x = x
wp.y = y
}
wp.z = 1.0 - wp.x - wp.y
wpRGB := xyzToSRGB(wp)
normalizeRGB(&wpRGB)
return wpRGB
}
func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp { func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp {
ramp := GammaRamp{ ramp := GammaRamp{
Red: make([]uint16, size), Red: make([]uint16, size),
@@ -19,16 +137,13 @@ func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp {
Blue: make([]uint16, size), Blue: make([]uint16, size),
} }
wp := calcWhitepoint(temp)
for i := uint32(0); i < size; i++ { for i := uint32(0); i < size; i++ {
val := float64(i) / float64(size-1) val := float64(i) / float64(size-1)
ramp.Red[i] = uint16(clamp01(math.Pow(val*wp.r, 1.0/gamma)) * 65535.0)
valGamma := math.Pow(val, 1.0/gamma) ramp.Green[i] = uint16(clamp01(math.Pow(val*wp.g, 1.0/gamma)) * 65535.0)
ramp.Blue[i] = uint16(clamp01(math.Pow(val*wp.b, 1.0/gamma)) * 65535.0)
r, g, b := temperatureToRGB(temp)
ramp.Red[i] = uint16(utils.Clamp(valGamma*r*65535.0, 0, 65535))
ramp.Green[i] = uint16(utils.Clamp(valGamma*g*65535.0, 0, 65535))
ramp.Blue[i] = uint16(utils.Clamp(valGamma*b*65535.0, 0, 65535))
} }
return ramp return ramp
@@ -50,39 +165,3 @@ func GenerateIdentityRamp(size uint32) GammaRamp {
return ramp return ramp
} }
func temperatureToRGB(temp int) (float64, float64, float64) {
tempK := float64(temp) / 100.0
var r, g, b float64
if tempK <= 66 {
r = 1.0
} else {
r = tempK - 60
r = 329.698727446 * math.Pow(r, -0.1332047592)
r = utils.Clamp(r, 0, 255) / 255.0
}
if tempK <= 66 {
g = tempK
g = 99.4708025861*math.Log(g) - 161.1195681661
g = utils.Clamp(g, 0, 255) / 255.0
} else {
g = tempK - 60
g = 288.1221695283 * math.Pow(g, -0.0755148492)
g = utils.Clamp(g, 0, 255) / 255.0
}
if tempK >= 66 {
b = 1.0
} else if tempK <= 19 {
b = 0.0
} else {
b = tempK - 10
b = 138.5177312231*math.Log(b) - 305.0447927307
b = utils.Clamp(b, 0, 255) / 255.0
}
return r, g, b
}
+13 -13
View File
@@ -54,7 +54,7 @@ func TestGenerateGammaRamp(t *testing.T) {
} }
} }
func TestTemperatureToRGB(t *testing.T) { func TestCalcWhitepoint(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
temp int temp int
@@ -67,32 +67,32 @@ func TestTemperatureToRGB(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r, g, b := temperatureToRGB(tt.temp) wp := calcWhitepoint(tt.temp)
if r < 0 || r > 1 { if wp.r < 0 || wp.r > 1 {
t.Errorf("red out of range: %f", r) t.Errorf("red out of range: %f", wp.r)
} }
if g < 0 || g > 1 { if wp.g < 0 || wp.g > 1 {
t.Errorf("green out of range: %f", g) t.Errorf("green out of range: %f", wp.g)
} }
if b < 0 || b > 1 { if wp.b < 0 || wp.b > 1 {
t.Errorf("blue out of range: %f", b) t.Errorf("blue out of range: %f", wp.b)
} }
}) })
} }
} }
func TestTemperatureProgression(t *testing.T) { func TestWhitepointProgression(t *testing.T) {
temps := []int{3000, 4000, 5000, 6000, 6500} temps := []int{3000, 4000, 5000, 6000, 6500}
var prevBlue float64 var prevBlue float64
for i, temp := range temps { for i, temp := range temps {
_, _, b := temperatureToRGB(temp) wp := calcWhitepoint(temp)
if i > 0 && b < prevBlue { if i > 0 && wp.b < prevBlue {
t.Errorf("blue should increase with temperature, %d->%d: %f->%f", t.Errorf("blue should increase with temperature, %d->%d: %f->%f",
temps[i-1], temp, prevBlue, b) temps[i-1], temp, prevBlue, wp.b)
} }
prevBlue = b prevBlue = wp.b
} }
} }
+42 -51
View File
@@ -7,20 +7,10 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
type Request struct { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "wayland manager not initialized") models.RespondError(conn, req.ID, "wayland manager not initialized")
return return
@@ -48,26 +38,27 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleSetTemperature(conn net.Conn, req Request, manager *Manager) { func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
var lowTemp, highTemp int var lowTemp, highTemp int
if temp, ok := req.Params["temp"].(float64); ok { if temp, ok := req.Params["temp"].(float64); ok {
lowTemp = int(temp) lowTemp = int(temp)
highTemp = int(temp) highTemp = int(temp)
} else { } else {
low, okLow := req.Params["low"].(float64) low, err := params.Float(req.Params, "low")
high, okHigh := req.Params["high"].(float64) if err != nil {
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
if !okLow || !okHigh { return
}
high, err := params.Float(req.Params, "high")
if err != nil {
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')") models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
return return
} }
lowTemp = int(low) lowTemp = int(low)
highTemp = int(high) highTemp = int(high)
} }
@@ -77,19 +68,19 @@ func handleSetTemperature(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "temperature set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "temperature set"})
} }
func handleSetLocation(conn net.Conn, req Request, manager *Manager) { func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
lat, ok := req.Params["latitude"].(float64) lat, err := params.Float(req.Params, "latitude")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'latitude' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
lon, ok := req.Params["longitude"].(float64) lon, err := params.Float(req.Params, "longitude")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'longitude' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -98,30 +89,30 @@ func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
} }
func handleSetManualTimes(conn net.Conn, req Request, manager *Manager) { func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
sunriseParam := req.Params["sunrise"] sunriseParam := req.Params["sunrise"]
sunsetParam := req.Params["sunset"] sunsetParam := req.Params["sunset"]
if sunriseParam == nil || sunsetParam == nil { if sunriseParam == nil || sunsetParam == nil {
manager.ClearManualTimes() manager.ClearManualTimes()
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return return
} }
sunriseStr, ok := sunriseParam.(string) sunriseStr, ok := sunriseParam.(string)
if !ok || sunriseStr == "" { if !ok || sunriseStr == "" {
manager.ClearManualTimes() manager.ClearManualTimes()
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return return
} }
sunsetStr, ok := sunsetParam.(string) sunsetStr, ok := sunsetParam.(string)
if !ok || sunsetStr == "" { if !ok || sunsetStr == "" {
manager.ClearManualTimes() manager.ClearManualTimes()
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
return return
} }
@@ -142,24 +133,24 @@ func handleSetManualTimes(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times set"})
} }
func handleSetUseIPLocation(conn net.Conn, req Request, manager *Manager) { func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
use, ok := req.Params["use"].(bool) use, err := params.Bool(req.Params, "use")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'use' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetUseIPLocation(use) manager.SetUseIPLocation(use)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "IP location preference set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "IP location preference set"})
} }
func handleSetGamma(conn net.Conn, req Request, manager *Manager) { func handleSetGamma(conn net.Conn, req models.Request, manager *Manager) {
gamma, ok := req.Params["gamma"].(float64) gamma, err := params.Float(req.Params, "gamma")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'gamma' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -168,21 +159,21 @@ func handleSetGamma(conn net.Conn, req Request, manager *Manager) {
return return
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "gamma set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "gamma set"})
} }
func handleSetEnabled(conn net.Conn, req Request, manager *Manager) { func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool) enabled, err := params.Bool(req.Params, "enabled")
if !ok { if err != nil {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter") models.RespondError(conn, req.ID, err.Error())
return return
} }
manager.SetEnabled(enabled) manager.SetEnabled(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "enabled state set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "enabled state set"})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
File diff suppressed because it is too large Load Diff
+95 -59
View File
@@ -6,81 +6,117 @@ import (
) )
const ( const (
degToRad = math.Pi / 180.0 degToRad = math.Pi / 180.0
radToDeg = 180.0 / math.Pi radToDeg = 180.0 / math.Pi
solarNoon = 12.0
sunriseAngle = -0.833
) )
func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes { type SunCondition int
utcDate := date.UTC()
year, month, day := utcDate.Date()
loc := date.Location()
dayOfYear := utcDate.YearDay() const (
SunNormal SunCondition = iota
SunMidnightSun
SunPolarNight
)
gamma := 2 * math.Pi / 365 * float64(dayOfYear-1) type SunTimes struct {
Dawn time.Time
Sunrise time.Time
Sunset time.Time
Night time.Time
}
eqTime := 229.18 * (0.000075 + func daysInYear(year int) int {
0.001868*math.Cos(gamma) - if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
0.032077*math.Sin(gamma) - return 366
0.014615*math.Cos(2*gamma) - }
0.040849*math.Sin(2*gamma)) return 365
}
decl := 0.006918 - func dateOrbitAngle(t time.Time) float64 {
0.399912*math.Cos(gamma) + return 2 * math.Pi / float64(daysInYear(t.Year())) * float64(t.YearDay()-1)
0.070257*math.Sin(gamma) - }
0.006758*math.Cos(2*gamma) +
0.000907*math.Sin(2*gamma) -
0.002697*math.Cos(3*gamma) +
0.00148*math.Sin(3*gamma)
func equationOfTime(orbitAngle float64) float64 {
return 4 * (0.000075 +
0.001868*math.Cos(orbitAngle) -
0.032077*math.Sin(orbitAngle) -
0.014615*math.Cos(2*orbitAngle) -
0.040849*math.Sin(2*orbitAngle))
}
func sunDeclination(orbitAngle float64) float64 {
return 0.006918 -
0.399912*math.Cos(orbitAngle) +
0.070257*math.Sin(orbitAngle) -
0.006758*math.Cos(2*orbitAngle) +
0.000907*math.Sin(2*orbitAngle) -
0.002697*math.Cos(3*orbitAngle) +
0.00148*math.Sin(3*orbitAngle)
}
func sunHourAngle(latRad, declination, targetSunRad float64) float64 {
return math.Acos(math.Cos(targetSunRad)/
math.Cos(latRad)*math.Cos(declination) -
math.Tan(latRad)*math.Tan(declination))
}
func hourAngleToSeconds(hourAngle, eqtime float64) float64 {
return radToDeg * (4.0*math.Pi - 4*hourAngle - eqtime) * 60
}
func sunCondition(latRad, declination float64) SunCondition {
signLat := latRad >= 0
signDecl := declination >= 0
if signLat == signDecl {
return SunMidnightSun
}
return SunPolarNight
}
func CalculateSunTimesWithTwilight(lat, lon float64, date time.Time, elevTwilight, elevDaylight float64) (SunTimes, SunCondition) {
latRad := lat * degToRad latRad := lat * degToRad
elevTwilightRad := (90.833 - elevTwilight) * degToRad
elevDaylightRad := (90.833 - elevDaylight) * degToRad
cosHourAngle := (math.Sin(sunriseAngle*degToRad) - utc := date.UTC()
math.Sin(latRad)*math.Sin(decl)) / orbitAngle := dateOrbitAngle(utc)
(math.Cos(latRad) * math.Cos(decl)) decl := sunDeclination(orbitAngle)
eqtime := equationOfTime(orbitAngle)
if cosHourAngle > 1 { haTwilight := sunHourAngle(latRad, decl, elevTwilightRad)
return SunTimes{ haDaylight := sunHourAngle(latRad, decl, elevDaylightRad)
Sunrise: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
Sunset: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc), if math.IsNaN(haTwilight) || math.IsNaN(haDaylight) {
} cond := sunCondition(latRad, decl)
} return SunTimes{}, cond
if cosHourAngle < -1 {
return SunTimes{
Sunrise: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
Sunset: time.Date(year, month, day, 23, 59, 59, 0, time.UTC).In(loc),
}
} }
hourAngle := math.Acos(cosHourAngle) * radToDeg dayStart := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC)
lonOffset := time.Duration(-lon*4) * time.Minute
sunriseTime := solarNoon - hourAngle/15.0 - lon/15.0 - eqTime/60.0 dawnSecs := hourAngleToSeconds(math.Abs(haTwilight), eqtime)
sunsetTime := solarNoon + hourAngle/15.0 - lon/15.0 - eqTime/60.0 sunriseSecs := hourAngleToSeconds(math.Abs(haDaylight), eqtime)
sunsetSecs := hourAngleToSeconds(-math.Abs(haDaylight), eqtime)
sunrise := timeOfDayToTime(sunriseTime, year, month, day, time.UTC).In(loc) nightSecs := hourAngleToSeconds(-math.Abs(haTwilight), eqtime)
sunset := timeOfDayToTime(sunsetTime, year, month, day, time.UTC).In(loc)
return SunTimes{ return SunTimes{
Sunrise: sunrise, Dawn: dayStart.Add(time.Duration(dawnSecs)*time.Second + lonOffset).In(date.Location()),
Sunset: sunset, Sunrise: dayStart.Add(time.Duration(sunriseSecs)*time.Second + lonOffset).In(date.Location()),
} Sunset: dayStart.Add(time.Duration(sunsetSecs)*time.Second + lonOffset).In(date.Location()),
Night: dayStart.Add(time.Duration(nightSecs)*time.Second + lonOffset).In(date.Location()),
}, SunNormal
} }
func timeOfDayToTime(hours float64, year int, month time.Month, day int, loc *time.Location) time.Time { func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes {
h := int(hours) times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
m := int((hours - float64(h)) * 60) switch cond {
s := int(((hours-float64(h))*60 - float64(m)) * 60) case SunMidnightSun:
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
if h < 0 { dayEnd := dayStart.Add(24*time.Hour - time.Second)
h += 24 return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayEnd, Night: dayEnd}
day-- case SunPolarNight:
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayStart, Night: dayStart}
} }
if h >= 24 { return times
h -= 24
day++
}
return time.Date(year, month, day, h, m, s, 0, loc)
} }
+32 -23
View File
@@ -340,38 +340,47 @@ func TestCalculateNextTransition(t *testing.T) {
} }
} }
func TestTimeOfDayToTime(t *testing.T) { func TestSunTimesWithTwilight(t *testing.T) {
lat := 40.7128
lon := -74.0060
date := time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local)
times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
if cond != SunNormal {
t.Errorf("expected SunNormal, got %v", cond)
}
if !times.Dawn.Before(times.Sunrise) {
t.Error("dawn should be before sunrise")
}
if !times.Sunrise.Before(times.Sunset) {
t.Error("sunrise should be before sunset")
}
if !times.Sunset.Before(times.Night) {
t.Error("sunset should be before night")
}
}
func TestSunConditions(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
hours float64 lat float64
expected time.Time date time.Time
expected SunCondition
}{ }{
{ {
name: "noon", name: "normal_conditions",
hours: 12.0, lat: 40.0,
expected: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local), date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.UTC),
}, expected: SunNormal,
{
name: "half_past",
hours: 12.5,
expected: time.Date(2024, 6, 21, 12, 30, 0, 0, time.Local),
},
{
name: "early_morning",
hours: 6.25,
expected: time.Date(2024, 6, 21, 6, 15, 0, 0, time.Local),
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := timeOfDayToTime(tt.hours, 2024, 6, 21, time.Local) _, cond := CalculateSunTimesWithTwilight(tt.lat, 0, tt.date, -6.0, 3.0)
if cond != tt.expected {
if result.Hour() != tt.expected.Hour() { t.Errorf("expected condition %v, got %v", tt.expected, cond)
t.Errorf("hour = %d, want %d", result.Hour(), tt.expected.Hour())
}
if result.Minute() != tt.expected.Minute() {
t.Errorf("minute = %d, want %d", result.Minute(), tt.expected.Minute())
} }
}) })
} }
+46 -28
View File
@@ -11,18 +11,28 @@ import (
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
type GammaState int
const (
StateNormal GammaState = iota
StateTransition
StateStatic
)
type Config struct { type Config struct {
Outputs []string Outputs []string
LowTemp int LowTemp int
HighTemp int HighTemp int
Latitude *float64 Latitude *float64
Longitude *float64 Longitude *float64
UseIPLocation bool UseIPLocation bool
ManualSunrise *time.Time ManualSunrise *time.Time
ManualSunset *time.Time ManualSunset *time.Time
ManualDuration *time.Duration ManualDuration *time.Duration
Gamma float64 Gamma float64
Enabled bool Enabled bool
ElevationTwilight float64
ElevationDaylight float64
} }
type State struct { type State struct {
@@ -31,13 +41,24 @@ type State struct {
NextTransition time.Time `json:"nextTransition"` NextTransition time.Time `json:"nextTransition"`
SunriseTime time.Time `json:"sunriseTime"` SunriseTime time.Time `json:"sunriseTime"`
SunsetTime time.Time `json:"sunsetTime"` SunsetTime time.Time `json:"sunsetTime"`
DawnTime time.Time `json:"dawnTime"`
NightTime time.Time `json:"nightTime"`
IsDay bool `json:"isDay"` IsDay bool `json:"isDay"`
SunPosition float64 `json:"sunPosition"`
} }
type cmd struct { type cmd struct {
fn func() fn func()
} }
type sunSchedule struct {
times SunTimes
condition SunCondition
dawnStepTime time.Duration
nightStepTime time.Duration
calcDay time.Time
}
type Manager struct { type Manager struct {
config Config config Config
configMutex sync.RWMutex configMutex sync.RWMutex
@@ -60,10 +81,9 @@ type Manager struct {
updateTrigger chan struct{} updateTrigger chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
currentTemp int schedule sunSchedule
targetTemp int scheduleMutex sync.RWMutex
transitionMutex sync.RWMutex gammaState GammaState
transitionChan chan int
cachedIPLat *float64 cachedIPLat *float64
cachedIPLon *float64 cachedIPLon *float64
@@ -80,7 +100,6 @@ type Manager struct {
type outputState struct { type outputState struct {
id uint32 id uint32
name string
registryName uint32 registryName uint32
output *wlclient.Output output *wlclient.Output
gammaControl any gammaControl any
@@ -91,18 +110,15 @@ type outputState struct {
lastFailTime time.Time lastFailTime time.Time
} }
type SunTimes struct {
Sunrise time.Time
Sunset time.Time
}
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
Outputs: []string{}, Outputs: []string{},
LowTemp: 4000, LowTemp: 4000,
HighTemp: 6500, HighTemp: 6500,
Gamma: 1.0, Gamma: 1.0,
Enabled: false, Enabled: false,
ElevationTwilight: -6.0,
ElevationDaylight: 3.0,
} }
} }
@@ -140,8 +156,7 @@ func (m *Manager) GetState() State {
if m.state == nil { if m.state == nil {
return State{} return State{}
} }
stateCopy := *m.state return *m.state
return stateCopy
} }
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
@@ -185,5 +200,8 @@ func stateChanged(old, new *State) bool {
if old.Config.Enabled != new.Config.Enabled { if old.Config.Enabled != new.Config.Enabled {
return true return true
} }
if old.SunPosition != new.SunPosition {
return true
}
return false return false
} }
+6 -18
View File
@@ -11,17 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
type Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]any `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type HeadConfig struct { type HeadConfig struct {
Name string `json:"name"` Name string `json:"name"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@@ -42,7 +31,7 @@ type ConfigurationRequest struct {
Test bool `json:"test"` Test bool `json:"test"`
} }
func HandleRequest(conn net.Conn, req Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil { if manager == nil {
models.RespondError(conn, req.ID, "wlroutput manager not initialized") models.RespondError(conn, req.ID, "wlroutput manager not initialized")
return return
@@ -62,12 +51,11 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
} }
} }
func handleGetState(conn net.Conn, req Request, manager *Manager) { func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState() models.Respond(conn, req.ID, manager.GetState())
models.Respond(conn, req.ID, state)
} }
func handleApplyConfiguration(conn net.Conn, req Request, manager *Manager, test bool) { func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
headsParam, ok := req.Params["heads"] headsParam, ok := req.Params["heads"]
if !ok { if !ok {
models.RespondError(conn, req.ID, "missing 'heads' parameter") models.RespondError(conn, req.ID, "missing 'heads' parameter")
@@ -95,10 +83,10 @@ func handleApplyConfiguration(conn net.Conn, req Request, manager *Manager, test
if test { if test {
msg = "configuration test succeeded" msg = "configuration test succeeded"
} }
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: msg}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: msg})
} }
func handleSubscribe(conn net.Conn, req Request, manager *Manager) { func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
+74 -2
View File
@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@@ -118,6 +120,59 @@ func (m Model) viewInstallingPackages() string {
return b.String() return b.String()
} }
func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
config, ok := distros.Registry[distroID]
if !ok {
return "dms"
}
var isGit bool
for _, dep := range dependencies {
if dep.Name == "dms (DankMaterialShell)" {
isGit = dep.Variant == deps.VariantGit
break
}
}
switch config.Family {
case distros.FamilyArch:
if isGit {
return "dms-shell-git"
}
return "dms-shell-bin"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit {
return "dms-git"
}
return "dms"
default:
return "dms"
}
}
func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
config, ok := distros.Registry[distroID]
if !ok {
return ""
}
if config.Family == distros.FamilyGentoo {
return "rm -rf ~/.config/quickshell/dms && sudo rm /usr/local/bin/dms"
}
pkg := dmsPackageName(distroID, dependencies)
switch config.Family {
case distros.FamilyArch:
return "sudo pacman -Rs " + pkg
case distros.FamilyFedora:
return "sudo dnf remove " + pkg
case distros.FamilyUbuntu, distros.FamilyDebian:
return "sudo apt remove " + pkg
case distros.FamilySUSE:
return "sudo zypper remove " + pkg
default:
return ""
}
}
func (m Model) viewInstallComplete() string { func (m Model) viewInstallComplete() string {
var b strings.Builder var b strings.Builder
@@ -132,7 +187,6 @@ func (m Model) viewInstallComplete() string {
b.WriteString(success) b.WriteString(success)
b.WriteString("\n\n") b.WriteString("\n\n")
// Show what was accomplished
accomplishments := []string{ accomplishments := []string{
"• Window manager and dependencies installed", "• Window manager and dependencies installed",
"• Terminal and development tools configured", "• Terminal and development tools configured",
@@ -146,8 +200,26 @@ func (m Model) viewInstallComplete() string {
} }
b.WriteString("\n") b.WriteString("\n")
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\" \n\nPress Enter to exit.") info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\"")
b.WriteString(info) b.WriteString(info)
b.WriteString("\n\n")
theme := TerminalTheme()
cmdStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Accent))
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("journalctl --user -u dms") + "\n")
if m.osInfo != nil {
if cmd := uninstallCommand(m.osInfo.Distribution.ID, m.dependencies); cmd != "" {
b.WriteString(labelStyle.Render(" Uninstall: ") + cmdStyle.Render(cmd) + "\n")
}
}
b.WriteString("\n")
b.WriteString(m.styles.Normal.Render("Press Enter to exit."))
if m.logFilePath != "" { if m.logFilePath != "" {
b.WriteString("\n\n") b.WriteString("\n\n")
+1 -1
View File
@@ -40,7 +40,7 @@ func (m Model) viewWelcome() string {
subtitle := lipgloss.NewStyle(). subtitle := lipgloss.NewStyle().
Foreground(lipgloss.Color(theme.Subtle)). Foreground(lipgloss.Color(theme.Subtle)).
Italic(true). Italic(true).
Render("Quickstart for a Dank Desktop") Render("Quickstart for a Dank Desktop")
b.WriteString(decorator) b.WriteString(decorator)
b.WriteString("\n") b.WriteString("\n")
+160 -1
View File
@@ -13,8 +13,23 @@ const (
FormatXRGB8888 PixelFormat = 1 FormatXRGB8888 PixelFormat = 1
FormatABGR8888 PixelFormat = 0x34324241 FormatABGR8888 PixelFormat = 0x34324241
FormatXBGR8888 PixelFormat = 0x34324258 FormatXBGR8888 PixelFormat = 0x34324258
FormatRGB888 PixelFormat = 0x34324752
FormatBGR888 PixelFormat = 0x34324742
) )
func (f PixelFormat) BytesPerPixel() int {
switch f {
case FormatRGB888, FormatBGR888:
return 3
default:
return 4
}
}
func (f PixelFormat) Is24Bit() bool {
return f == FormatRGB888 || f == FormatBGR888
}
type Buffer struct { type Buffer struct {
fd int fd int
data []byte data []byte
@@ -78,6 +93,52 @@ func (b *Buffer) Close() error {
return firstErr return firstErr
} }
func (b *Buffer) ConvertTo32Bit(srcFormat PixelFormat) (*Buffer, PixelFormat, error) {
if !srcFormat.Is24Bit() {
return b, srcFormat, nil
}
dstFormat := FormatXRGB8888
dstStride := b.Width * 4
dst, err := CreateBuffer(b.Width, b.Height, dstStride)
if err != nil {
return nil, srcFormat, err
}
dst.Format = dstFormat
srcData := b.data
dstData := dst.data
// DRM format names are counterintuitive on little-endian:
// RGB888 memory layout: B, G, R (name is logical order, not memory)
// BGR888 memory layout: R, G, B
isBGRMemory := srcFormat == FormatRGB888
for y := 0; y < b.Height; y++ {
srcRow := y * b.Stride
dstRow := y * dstStride
for x := 0; x < b.Width; x++ {
si := srcRow + x*3
di := dstRow + x*4
if isBGRMemory {
// RGB888: src memory is B,G,R -> dst XRGB8888 memory B,G,R,X
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
} else {
// BGR888: src memory is R,G,B -> dst XRGB8888 memory B,G,R,X
dstData[di+0] = srcData[si+2]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+0]
}
dstData[di+3] = 0xFF
}
}
return dst, dstFormat, nil
}
func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) { func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
if x < 0 || x >= b.Width || y < 0 || y >= b.Height { if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
return return
@@ -88,7 +149,12 @@ func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
return return
} }
return b.data[off+2], b.data[off+1], b.data[off], b.data[off+3] switch b.Format {
case FormatXBGR8888, FormatABGR8888:
return b.data[off], b.data[off+1], b.data[off+2], 0xFF
default:
return b.data[off+2], b.data[off+1], b.data[off], 0xFF
}
} }
func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) { func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) {
@@ -137,3 +203,96 @@ func (b *Buffer) Clear() {
func (b *Buffer) CopyFrom(src *Buffer) { func (b *Buffer) CopyFrom(src *Buffer) {
copy(b.data, src.data) copy(b.data, src.data)
} }
const (
TransformNormal = 0
Transform90 = 1
Transform180 = 2
Transform270 = 3
TransformFlipped = 4
TransformFlipped90 = 5
TransformFlipped180 = 6
TransformFlipped270 = 7
)
func (b *Buffer) ApplyTransform(transform int32) (*Buffer, error) {
if transform == TransformNormal {
return b, nil
}
var newW, newH int
switch transform {
case Transform90, Transform270, TransformFlipped90, TransformFlipped270:
newW, newH = b.Height, b.Width
default:
newW, newH = b.Width, b.Height
}
newBuf, err := CreateBuffer(newW, newH, newW*4)
if err != nil {
return nil, err
}
newBuf.Format = b.Format
srcData := b.data
dstData := newBuf.data
for sy := 0; sy < b.Height; sy++ {
for sx := 0; sx < b.Width; sx++ {
var dx, dy int
switch transform {
case Transform90: // 90° CCW
dx = sy
dy = b.Width - 1 - sx
case Transform180:
dx = b.Width - 1 - sx
dy = b.Height - 1 - sy
case Transform270: // 270° CCW = 90° CW
dx = b.Height - 1 - sy
dy = sx
case TransformFlipped:
dx = b.Width - 1 - sx
dy = sy
case TransformFlipped90:
dx = sy
dy = sx
case TransformFlipped180:
dx = sx
dy = b.Height - 1 - sy
case TransformFlipped270:
dx = b.Height - 1 - sy
dy = b.Width - 1 - sx
default:
dx, dy = sx, sy
}
si := sy*b.Stride + sx*4
di := dy*newBuf.Stride + dx*4
if si+3 < len(srcData) && di+3 < len(dstData) {
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
dstData[di+3] = srcData[si+3]
}
}
}
return newBuf, nil
}
func InverseTransform(transform int32) int32 {
switch transform {
case Transform90:
return Transform270
case Transform270:
return Transform90
case TransformFlipped90:
return TransformFlipped270
case TransformFlipped270:
return TransformFlipped90
default:
return transform
}
}
+4 -10
View File
@@ -1,12 +1,6 @@
dms-git (0.6.2+git2264.c5c5ce84) nightly; urgency=medium dms-git (0.6.2+git2419.993f14a3) nightly; urgency=medium
* Add VERSION file creation to all distro packages * widgets: make dank icon picker a popup
* Fix Fedora COPR to pass VERSION/COMMIT to make dist * Previous updates included in build
* Fix obs-upload.sh auto-increment to preserve git hash and add ppa suffix
* Fix debian/rules to use source at root level (native format)
* Remove incorrect dms-git-source subdirectory references
* Build dms binary from source for true git version strings
* Match Fedora COPR git build behavior
* Add golang-go and make as build dependencies
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 03 Dec 2025 01:50:00 +0000 -- Avenge Media <AvengeMedia.US@gmail.com> Mon, 09 Dec 2025 14:00:00 +0000
+3
View File
@@ -17,6 +17,9 @@ export GOTOOLCHAIN := local
%: %:
dh $@ dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build: override_dh_auto_build:
# Create Go cache directories # Create Go cache directories
mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE) mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE)
+3
View File
@@ -7,6 +7,9 @@ DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
%: %:
dh $@ dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build: override_dh_auto_build:
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \ if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
if [ -f dms-distropkg-amd64.gz ]; then \ if [ -f dms-distropkg-amd64.gz ]; then \
+3 -44
View File
@@ -26,7 +26,7 @@ BuildRequires: systemd-rpm-macros
# Core requirements # Core requirements
Requires: (quickshell-git or quickshell) Requires: (quickshell-git or quickshell)
Requires: accountsservice Requires: accountsservice
Requires: dms-cli Requires: dms-cli = %{epoch}:%{version}-%{release}
Requires: dgop Requires: dgop
# Core utilities (Highly recommended for DMS functionality) # Core utilities (Highly recommended for DMS functionality)
@@ -60,41 +60,9 @@ URL: https://github.com/AvengeMedia/DankMaterialShell
Command-line interface for DankMaterialShell configuration and management. Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities. Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep %prep
{{{ git_repo_setup_macro }}} {{{ git_repo_setup_macro }}}
# Download and extract DGOP binary for target architecture
case "%{_arch}" in
x86_64)
DGOP_ARCH="amd64"
;;
aarch64)
DGOP_ARCH="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${DGOP_ARCH}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build %build
# Build DMS CLI from source (core/subdirectory) # Build DMS CLI from source (core/subdirectory)
VERSION="%{version}" VERSION="%{version}"
@@ -128,9 +96,6 @@ core/bin/${DMS_BINARY} completion bash > %{buildroot}%{_datadir}/bash-completion
core/bin/${DMS_BINARY} completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || : core/bin/${DMS_BINARY} completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
core/bin/${DMS_BINARY} completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : core/bin/${DMS_BINARY} completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
# Install dgop binary
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Install systemd user service # Install systemd user service
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
@@ -150,11 +115,8 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans %posttrans
# Signal running DMS instances to reload
# Restart DMS for active users after upgrade pkill -USR1 -x dms >/dev/null 2>&1 || :
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
@@ -171,8 +133,5 @@ fi
%{_datadir}/zsh/site-functions/_dms %{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish %{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog %changelog
{{{ git_repo_changelog }}} {{{ git_repo_changelog }}}
+16 -6
View File
@@ -10,13 +10,14 @@
user = config.services.greetd.settings.default_session.user; user = config.services.greetd.settings.default_session.user;
cacheDir = "/var/lib/dms-greeter";
greeterScript = pkgs.writeShellScriptBin "dms-greeter" '' greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
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" cacheDir
"--command" "--command"
cfg.compositor.name cfg.compositor.name
"-p" "-p"
@@ -27,6 +28,8 @@
"${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}" "${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}"
])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"} ])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
''; '';
jq = lib.getExe pkgs.jq;
in { in {
imports = let imports = let
msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead."; msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead.";
@@ -93,17 +96,17 @@ in {
material-symbols material-symbols
]; ];
systemd.tmpfiles.settings."10-dmsgreeter" = { systemd.tmpfiles.settings."10-dmsgreeter" = {
"/var/lib/dmsgreeter".d = { ${cacheDir}.d = {
user = user; user = user;
group = group =
if config.users.users.${user}.group != "" if config.users.users.${user}.group != ""
then config.users.users.${user}.group then config.users.users.${user}.group
else "greeter"; else "greeter";
mode = "0755"; mode = "0750";
}; };
}; };
systemd.services.greetd.preStart = '' systemd.services.greetd.preStart = ''
cd /var/lib/dmsgreeter cd ${cacheDir}
${lib.concatStringsSep "\n" (lib.map (f: '' ${lib.concatStringsSep "\n" (lib.map (f: ''
if [ -f "${f}" ]; then if [ -f "${f}" ]; then
cp "${f}" . cp "${f}" .
@@ -112,9 +115,16 @@ in {
cfg.configFiles)} cfg.configFiles)}
if [ -f session.json ]; then if [ -f session.json ]; then
if cp "$(${lib.getExe pkgs.jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then if cp "$(${jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then
mv session.json session.orig.json mv session.json session.orig.json
${lib.getExe pkgs.jq} '.wallpaperPath = "/var/lib/dmsgreeter/wallpaper.jpg"' session.orig.json > session.json ${jq} '.wallpaperPath = "${cacheDir}/wallpaper.jpg"' session.orig.json > session.json
fi
fi
if [ -f settings.json ]; then
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi
fi fi
+2 -4
View File
@@ -110,10 +110,8 @@ if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
fi fi
# Signal running DMS instances to reload
if [ "$1" -ge 2 ]; then pkill -USR1 -x dms >/dev/null 2>&1 || :
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
+2 -4
View File
@@ -80,10 +80,8 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans %posttrans
# Signal running DMS instances to reload
if [ "$1" -ge 2 ]; then pkill -USR1 -x dms >/dev/null 2>&1 || :
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
+4 -6
View File
@@ -1,8 +1,6 @@
dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium dms-git (0.6.2+git2419.993f14a3) questing; urgency=medium
* Add VERSION file creation to all distro packages * widgets: make dank icon picker a popup
* Fix Fedora COPR to pass VERSION/COMMIT to make dist * Previous updates included in build
* Fix obs-upload.sh auto-increment to preserve git hash and add ppa suffix
* Git snapshot (commit 2264: c5c5ce84)
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 03 Dec 2025 01:50:00 +0000 -- Avenge Media <AvengeMedia.US@gmail.com> Mon, 09 Dec 2025 14:00:00 +0000
+3
View File
@@ -17,6 +17,9 @@ export GOTOOLCHAIN := local
%: %:
dh $@ dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build: override_dh_auto_build:
# Create Go cache directories (sbuild sets HOME to non-existent path) # Create Go cache directories (sbuild sets HOME to non-existent path)
mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE) mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE)
+3
View File
@@ -13,6 +13,9 @@ BASE_VERSION := $(shell echo $(UPSTREAM_VERSION) | sed 's/ppa[0-9]*$$//' | sed '
%: %:
dh $@ dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build: override_dh_auto_build:
# All files are included in source package (downloaded by build-source.sh) # All files are included in source package (downloaded by build-source.sh)
# Launchpad build environment has no internet access # Launchpad build environment has no internet access
+3
View File
@@ -99,6 +99,9 @@
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
+1
View File
@@ -0,0 +1 @@
The Dark Knight
+47 -15
View File
@@ -39,11 +39,12 @@ const KEY_MAP = {
16777329: "XF86AudioMute", 16777329: "XF86AudioMute",
16842808: "XF86AudioMicMute", 16842808: "XF86AudioMicMute",
16777344: "XF86AudioPlay", 16777344: "XF86AudioPlay",
16777345: "XF86AudioPause", 16777345: "XF86AudioStop",
16777346: "XF86AudioStop", 16777346: "XF86AudioPrev",
16777347: "XF86AudioNext", 16777347: "XF86AudioNext",
16777348: "XF86AudioPrev", 16777348: "XF86AudioPause",
16842792: "XF86AudioRecord", 16777349: "XF86AudioMedia",
16777350: "XF86AudioRecord",
16842798: "XF86MonBrightnessUp", 16842798: "XF86MonBrightnessUp",
16842797: "XF86MonBrightnessDown", 16842797: "XF86MonBrightnessDown",
16842800: "XF86KbdBrightnessUp", 16842800: "XF86KbdBrightnessUp",
@@ -79,12 +80,49 @@ const KEY_MAP = {
124: "Backslash", 124: "Backslash",
95: "Minus", 95: "Minus",
43: "Equal", 43: "Equal",
126: "grave" 126: "grave",
196: "Adiaeresis",
214: "Odiaeresis",
220: "Udiaeresis",
228: "adiaeresis",
246: "odiaeresis",
252: "udiaeresis",
223: "ssharp",
201: "Eacute",
233: "eacute",
200: "Egrave",
232: "egrave",
202: "Ecircumflex",
234: "ecircumflex",
203: "Ediaeresis",
235: "ediaeresis",
192: "Agrave",
224: "agrave",
194: "Acircumflex",
226: "acircumflex",
199: "Ccedilla",
231: "ccedilla",
206: "Icircumflex",
238: "icircumflex",
207: "Idiaeresis",
239: "idiaeresis",
212: "Ocircumflex",
244: "ocircumflex",
217: "Ugrave",
249: "ugrave",
219: "Ucircumflex",
251: "ucircumflex",
209: "Ntilde",
241: "ntilde",
191: "questiondown",
161: "exclamdown"
}; };
function xkbKeyFromQtKey(qk) { function xkbKeyFromQtKey(qk) {
if (qk >= 65 && qk <= 90) if (qk >= 65 && qk <= 90)
return String.fromCharCode(qk); return String.fromCharCode(qk);
if (qk >= 97 && qk <= 122)
return String.fromCharCode(qk - 32);
if (qk >= 48 && qk <= 57) if (qk >= 48 && qk <= 57)
return String.fromCharCode(qk); return String.fromCharCode(qk);
if (qk >= 16777264 && qk <= 16777298) if (qk >= 16777264 && qk <= 16777298)
@@ -94,16 +132,10 @@ function xkbKeyFromQtKey(qk) {
function modsFromEvent(mods) { function modsFromEvent(mods) {
var result = []; var result = [];
var hasAlt = mods & 0x08000000; if (mods & 0x10000000)
var hasSuper = mods & 0x10000000; result.push("Super");
if (hasAlt && hasSuper) { if (mods & 0x08000000)
result.push("Mod"); result.push("Alt");
} else {
if (hasSuper)
result.push("Super");
if (hasAlt)
result.push("Alt");
}
if (mods & 0x04000000) if (mods & 0x04000000)
result.push("Ctrl"); result.push("Ctrl");
if (mods & 0x02000000) if (mods & 0x02000000)
+95 -22
View File
@@ -49,26 +49,26 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call inhibit toggle", label: "Idle Inhibit: Toggle" }, { id: "spawn dms ipc call inhibit toggle", label: "Idle Inhibit: Toggle" },
{ id: "spawn dms ipc call inhibit enable", label: "Idle Inhibit: Enable" }, { id: "spawn dms ipc call inhibit enable", label: "Idle Inhibit: Enable" },
{ id: "spawn dms ipc call inhibit disable", label: "Idle Inhibit: Disable" }, { id: "spawn dms ipc call inhibit disable", label: "Idle Inhibit: Disable" },
{ id: "spawn dms ipc call audio increment", label: "Volume Up" }, { id: "spawn dms ipc call audio increment 5", label: "Volume Up" },
{ id: "spawn dms ipc call audio increment 1", label: "Volume Up (1%)" }, { id: "spawn dms ipc call audio increment 1", label: "Volume Up (1%)" },
{ id: "spawn dms ipc call audio increment 5", label: "Volume Up (5%)" }, { id: "spawn dms ipc call audio increment 5", label: "Volume Up (5%)" },
{ id: "spawn dms ipc call audio increment 10", label: "Volume Up (10%)" }, { id: "spawn dms ipc call audio increment 10", label: "Volume Up (10%)" },
{ id: "spawn dms ipc call audio decrement", label: "Volume Down" }, { id: "spawn dms ipc call audio decrement 5", label: "Volume Down" },
{ id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" }, { id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" },
{ id: "spawn dms ipc call audio decrement 5", label: "Volume Down (5%)" }, { id: "spawn dms ipc call audio decrement 5", label: "Volume Down (5%)" },
{ id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" }, { id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" },
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" }, { id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" }, { id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" }, { id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
{ id: "spawn dms ipc call brightness increment", label: "Brightness Up" }, { id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" },
{ id: "spawn dms ipc call brightness increment 1", label: "Brightness Up (1%)" }, { id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
{ id: "spawn dms ipc call brightness increment 5", label: "Brightness Up (5%)" }, { id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up (5%)" },
{ id: "spawn dms ipc call brightness increment 10", label: "Brightness Up (10%)" }, { id: "spawn dms ipc call brightness increment 10 \"\"", label: "Brightness Up (10%)" },
{ id: "spawn dms ipc call brightness decrement", label: "Brightness Down" }, { id: "spawn dms ipc call brightness decrement 5 \"\"", label: "Brightness Down" },
{ id: "spawn dms ipc call brightness decrement 1", label: "Brightness Down (1%)" }, { id: "spawn dms ipc call brightness decrement 1 \"\"", label: "Brightness Down (1%)" },
{ id: "spawn dms ipc call brightness decrement 5", label: "Brightness Down (5%)" }, { id: "spawn dms ipc call brightness decrement 5 \"\"", label: "Brightness Down (5%)" },
{ id: "spawn dms ipc call brightness decrement 10", label: "Brightness Down (10%)" }, { id: "spawn dms ipc call brightness decrement 10 \"\"", label: "Brightness Down (10%)" },
{ id: "spawn dms ipc call brightness toggleExponential", label: "Brightness: Toggle Exponential" }, { id: "spawn dms ipc call brightness toggleExponential \"\"", label: "Brightness: Toggle Exponential" },
{ id: "spawn dms ipc call theme toggle", label: "Theme: Toggle Light/Dark" }, { id: "spawn dms ipc call theme toggle", label: "Theme: Toggle Light/Dark" },
{ id: "spawn dms ipc call theme light", label: "Theme: Light Mode" }, { id: "spawn dms ipc call theme light", label: "Theme: Light Mode" },
{ id: "spawn dms ipc call theme dark", label: "Theme: Dark Mode" }, { id: "spawn dms ipc call theme dark", label: "Theme: Dark Mode" },
@@ -223,19 +223,37 @@ const ACTION_ARGS = {
const DMS_ACTION_ARGS = { const DMS_ACTION_ARGS = {
"audio increment": { "audio increment": {
base: "spawn dms ipc call audio increment", base: "spawn dms ipc call audio increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }] args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
}, },
"audio decrement": { "audio decrement": {
base: "spawn dms ipc call audio decrement", base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }] args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
}, },
"brightness increment": { "brightness increment": {
base: "spawn dms ipc call brightness increment", base: "spawn dms ipc call brightness increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }] args: [
{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" },
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
}, },
"brightness decrement": { "brightness decrement": {
base: "spawn dms ipc call brightness decrement", base: "spawn dms ipc call brightness decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }] args: [
{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" },
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
},
"brightness toggleExponential": {
base: "spawn dms ipc call brightness toggleExponential",
args: [
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
},
"dash toggle": {
base: "spawn dms ipc call dash toggle",
args: [
{ name: "tab", type: "text", label: "Tab", placeholder: "overview, media, wallpaper, weather", default: "" }
]
} }
}; };
@@ -243,6 +261,10 @@ function getActionTypes() {
return ACTION_TYPES; return ACTION_TYPES;
} }
function getDmsActionArgs() {
return DMS_ACTION_ARGS;
}
function getDmsActions(isNiri, isHyprland) { function getDmsActions(isNiri, isHyprland) {
const result = []; const result = [];
for (let i = 0; i < DMS_ACTIONS.length; i++) { for (let i = 0; i < DMS_ACTIONS.length; i++) {
@@ -495,10 +517,48 @@ function parseDmsActionArgs(action) {
for (var key in DMS_ACTION_ARGS) { for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key]; var config = DMS_ACTION_ARGS[key];
if (action.startsWith(config.base)) { if (!action.startsWith(config.base))
var rest = action.slice(config.base.length).trim(); continue;
return { base: key, args: { amount: rest || "" } };
var rest = action.slice(config.base.length).trim();
var result = { base: key, args: {} };
if (!rest)
return result;
var tokens = [];
var current = "";
var inQuotes = false;
var hadQuotes = false;
for (var i = 0; i < rest.length; i++) {
var c = rest[i];
switch (c) {
case '"':
inQuotes = !inQuotes;
hadQuotes = true;
break;
case ' ':
if (inQuotes) {
current += c;
} else if (current || hadQuotes) {
tokens.push(current);
current = "";
hadQuotes = false;
}
break;
default:
current += c;
break;
}
} }
if (current || hadQuotes)
tokens.push(current);
for (var j = 0; j < config.args.length && j < tokens.length; j++) {
result.args[config.args[j].name] = tokens[j];
}
return result;
} }
return { base: action, args: {} }; return { base: action, args: {} };
@@ -509,11 +569,24 @@ function buildDmsAction(baseKey, args) {
if (!config) if (!config)
return ""; return "";
var action = config.base; var parts = [config.base];
if (args && args.amount)
action += " " + args.amount;
return action; for (var i = 0; i < config.args.length; i++) {
var argDef = config.args[i];
var value = args?.[argDef.name];
if (value === undefined || value === null)
value = argDef.default ?? "";
if (argDef.type === "text" && value === "") {
parts.push('""');
} else if (value !== "") {
parts.push(value);
} else {
break;
}
}
return parts.join(" ");
} }
function getScreenshotOptions() { function getScreenshotOptions() {
+1 -1
View File
@@ -53,7 +53,7 @@ Singleton {
if (appId === "home assistant desktop") if (appId === "home assistant desktop")
return "homeassistant-desktop"; return "homeassistant-desktop";
if (appId.includes("com.transmissionbt.transmission")) if (appId.includes("com.transmissionbt.transmission"))
return "transmission-gtk"; return "transmission";
return appId; return appId;
} }
-1
View File
@@ -255,7 +255,6 @@ Singleton {
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
property string batteryProfileName: "" property string batteryProfileName: ""
property bool lockBeforeSuspend: false property bool lockBeforeSuspend: false
property bool preventIdleForMedia: false
property bool loginctlLockIntegration: true property bool loginctlLockIntegration: true
property bool fadeToLockEnabled: false property bool fadeToLockEnabled: false
property int fadeToLockGracePeriod: 5 property int fadeToLockGracePeriod: 5
+7 -15
View File
@@ -128,21 +128,13 @@ Singleton {
setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType); setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
} }
} }
} else { } else if (currentTheme !== "custom") {
let primaryColor; const darkTheme = StockThemes.getThemeByName(currentTheme, false);
let matugenType; const lightTheme = StockThemes.getThemeByName(currentTheme, true);
if (currentTheme === "custom") { if (darkTheme && darkTheme.primary) {
if (customThemeData && customThemeData.primary) { const stockColors = buildMatugenColorsFromTheme(darkTheme, lightTheme);
primaryColor = customThemeData.primary; const themeData = isLight ? lightTheme : darkTheme;
matugenType = customThemeData.matugen_type; setDesiredTheme("hex", themeData.primary, isLight, iconTheme, themeData.matugen_type, stockColors);
}
} else {
primaryColor = currentThemeData.primary;
matugenType = currentThemeData.matugen_type;
}
if (primaryColor) {
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType);
} }
} }
}, 0); }, 0);
@@ -154,7 +154,6 @@ var SPEC = {
batterySuspendBehavior: { def: 0 }, batterySuspendBehavior: { def: 0 },
batteryProfileName: { def: "" }, batteryProfileName: { def: "" },
lockBeforeSuspend: { def: false }, lockBeforeSuspend: { def: false },
preventIdleForMedia: { def: false },
loginctlLockIntegration: { def: true }, loginctlLockIntegration: { def: true },
fadeToLockEnabled: { def: false }, fadeToLockEnabled: { def: false },
fadeToLockGracePeriod: { def: 5 }, fadeToLockGracePeriod: { def: 5 },
+72 -37
View File
@@ -108,13 +108,14 @@ Item {
id: barRepeaterModel id: barRepeaterModel
values: { values: {
const configs = SettingsData.barConfigs; const configs = SettingsData.barConfigs;
return configs return configs.map(c => ({
.map(c => ({ id: c.id, position: c.position })) id: c.id,
.sort((a, b) => { position: c.position
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right; })).sort((a, b) => {
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right; const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
return aVertical - bVertical; const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
}); return aVertical - bVertical;
});
} }
} }
@@ -142,9 +143,34 @@ Item {
} }
} }
property bool dockEnabled: false
Timer {
id: dockRecreateDebounce
interval: 500
repeat: false
onTriggered: {
root.dockEnabled = false;
Qt.callLater(() => {
root.dockEnabled = true;
});
}
}
Component.onCompleted: {
dockRecreateDebounce.start();
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
dockRecreateDebounce.restart();
}
}
Loader { Loader {
id: dockLoader id: dockLoader
active: true active: root.dockEnabled
asynchronous: false asynchronous: false
property var currentPosition: SettingsData.dockPosition property var currentPosition: SettingsData.dockPosition
@@ -440,62 +466,71 @@ Item {
title: I18n.tr("Open with...") title: I18n.tr("Open with...")
function shellEscape(str) { function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'" return "'" + str.replace(/'/g, "'\\''") + "'";
} }
onApplicationSelected: (app, filePath) => { onApplicationSelected: (app, filePath) => {
if (!app) return if (!app)
return;
let cmd = app.exec || "";
const escapedPath = shellEscape(filePath);
const escapedUri = shellEscape("file://" + filePath);
let cmd = app.exec || "" let hasField = false;
const escapedPath = shellEscape(filePath) if (cmd.includes("%f")) {
const escapedUri = shellEscape("file://" + filePath) cmd = cmd.replace("%f", escapedPath);
hasField = true;
let hasField = false } else if (cmd.includes("%F")) {
if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedPath); hasField = true } cmd = cmd.replace("%F", escapedPath);
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedPath); hasField = true } hasField = true;
else if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUri); hasField = true } } else if (cmd.includes("%u")) {
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUri); hasField = true } cmd = cmd.replace("%u", escapedUri);
hasField = true;
cmd = cmd.replace(/%[ikc]/g, "") } else if (cmd.includes("%U")) {
cmd = cmd.replace("%U", escapedUri);
if (!hasField) { hasField = true;
cmd += " " + escapedPath
} }
console.log("FilePicker: Launching", cmd) cmd = cmd.replace(/%[ikc]/g, "");
if (!hasField) {
cmd += " " + escapedPath;
}
console.log("FilePicker: Launching", cmd);
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
}) });
} }
} }
Connections { Connections {
target: DMSService target: DMSService
function onOpenUrlRequested(url) { function onOpenUrlRequested(url) {
browserPickerModal.url = url browserPickerModal.url = url;
browserPickerModal.open() browserPickerModal.open();
} }
function onAppPickerRequested(data) { function onAppPickerRequested(data) {
console.log("DMSShell: App picker requested with data:", JSON.stringify(data)) console.log("DMSShell: App picker requested with data:", JSON.stringify(data));
if (!data || !data.target) { if (!data || !data.target) {
console.warn("DMSShell: Invalid app picker request data") console.warn("DMSShell: Invalid app picker request data");
return return;
} }
filePickerModal.targetData = data.target filePickerModal.targetData = data.target;
filePickerModal.targetDataLabel = data.requestType || "file" filePickerModal.targetDataLabel = data.requestType || "file";
if (data.categories && data.categories.length > 0) { if (data.categories && data.categories.length > 0) {
filePickerModal.categoryFilter = data.categories filePickerModal.categoryFilter = data.categories;
} else { } else {
filePickerModal.categoryFilter = [] filePickerModal.categoryFilter = [];
} }
filePickerModal.usageHistoryKey = "filePickerUsageHistory" filePickerModal.usageHistoryKey = "filePickerUsageHistory";
filePickerModal.open() filePickerModal.open();
} }
} }
+8 -14
View File
@@ -332,18 +332,16 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.currentProvider = provider; KeybindsService.loadCheatsheet(provider);
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`; return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) { if (root.hyprKeybindsModalLoader.item.shouldBeVisible)
root.hyprKeybindsModalLoader.item.close(); root.hyprKeybindsModalLoader.item.close();
} else { else
root.hyprKeybindsModalLoader.item.open(); root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`; return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
} }
@@ -351,18 +349,16 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.currentProvider = provider; KeybindsService.loadCheatsheet(provider);
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`; return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) { if (root.hyprKeybindsModalLoader.item.shouldBeVisible)
root.hyprKeybindsModalLoader.item.close(); root.hyprKeybindsModalLoader.item.close();
} else { else
root.hyprKeybindsModalLoader.item.open(); root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`; return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
} }
@@ -370,8 +366,7 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.currentProvider = provider; KeybindsService.loadCheatsheet(provider);
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
@@ -385,8 +380,7 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.currentProvider = provider; KeybindsService.loadCheatsheet(provider);
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
@@ -46,6 +46,7 @@ FocusScope {
property bool pathInputHasFocus: false property bool pathInputHasFocus: false
property int actualGridColumns: 5 property int actualGridColumns: 5
property bool _initialized: false property bool _initialized: false
property bool closeOnEscape: true
signal fileSelected(string path) signal fileSelected(string path)
signal closeRequested signal closeRequested
@@ -298,7 +299,7 @@ FocusScope {
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns) property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) { function handleKey(event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape && root.closeOnEscape) {
closeRequested(); closeRequested();
event.accepted = true; event.accepted = true;
return; return;
@@ -35,7 +35,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 800
implicitHeight: 600 implicitHeight: 600
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -59,6 +59,7 @@ FloatingWindow {
id: content id: content
anchors.fill: parent anchors.fill: parent
focus: true focus: true
closeOnEscape: false
browserTitle: fileBrowserModal.browserTitle browserTitle: fileBrowserModal.browserTitle
browserIcon: fileBrowserModal.browserIcon browserIcon: fileBrowserModal.browserIcon
+51 -18
View File
@@ -17,7 +17,11 @@ DankModal {
modalWidth: _maxW modalWidth: _maxW
modalHeight: _maxH modalHeight: _maxH
onBackgroundClicked: close() onBackgroundClicked: close()
onOpened: () => Qt.callLater(() => modalFocusScope.forceActiveFocus()) onOpened: {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
}
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [root.contentWindow] windows: [root.contentWindow]
@@ -66,7 +70,7 @@ DankModal {
spacing: Theme.spacingL spacing: Theme.spacingL
StyledText { StyledText {
text: KeybindsService.keybinds.title || "Keybinds" text: KeybindsService.cheatsheet.title || "Keybinds"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.primary color: Theme.primary
@@ -82,7 +86,7 @@ DankModal {
Component.onCompleted: root.activeFlickable = mainFlickable Component.onCompleted: root.activeFlickable = mainFlickable
property var rawBinds: KeybindsService.keybinds.binds || {} property var rawBinds: KeybindsService.cheatsheet.binds || {}
property var categories: { property var categories: {
const processed = {}; const processed = {};
for (const cat in rawBinds) { for (const cat in rawBinds) {
@@ -92,6 +96,8 @@ DankModal {
for (let i = 0; i < binds.length; i++) { for (let i = 0; i < binds.length; i++) {
const bind = binds[i]; const bind = binds[i];
if (bind.hideOnOverlay)
continue;
if (bind.subcat) { if (bind.subcat) {
hasSubcats = true; hasSubcats = true;
if (!subcats[bind.subcat]) if (!subcats[bind.subcat])
@@ -104,6 +110,9 @@ DankModal {
} }
} }
if (Object.keys(subcats).length === 0)
continue;
processed[cat] = { processed[cat] = {
hasSubcats: hasSubcats, hasSubcats: hasSubcats,
subcats: subcats, subcats: subcats,
@@ -114,12 +123,36 @@ DankModal {
} }
property var categoryKeys: Object.keys(categories) property var categoryKeys: Object.keys(categories)
function estimateCategoryHeight(catName) {
const catData = categories[catName];
if (!catData)
return 0;
let bindCount = 0;
for (const key of catData.subcatKeys) {
bindCount += catData.subcats[key]?.length || 0;
if (key !== "_root")
bindCount += 1;
}
return 40 + bindCount * 28;
}
function distributeCategories(cols) { function distributeCategories(cols) {
const columns = []; const columns = [];
for (let i = 0; i < cols; i++) const heights = [];
for (let i = 0; i < cols; i++) {
columns.push([]); columns.push([]);
for (let i = 0; i < categoryKeys.length; i++) heights.push(0);
columns[i % cols].push(categoryKeys[i]); }
const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
for (const cat of sorted) {
let minIdx = 0;
for (let i = 1; i < cols; i++) {
if (heights[i] < heights[minIdx])
minIdx = i;
}
columns[minIdx].push(cat);
heights[minIdx] += estimateCategoryHeight(cat);
}
return columns; return columns;
} }
@@ -137,7 +170,7 @@ DankModal {
Column { Column {
id: masonryColumn id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingM spacing: Theme.spacingXL
Repeater { Repeater {
model: rowLayout.columnCategories[index] || [] model: rowLayout.columnCategories[index] || []
@@ -199,37 +232,37 @@ DankModal {
Repeater { Repeater {
model: parent.parent.subcatBinds model: parent.parent.subcatBinds
Row { Item {
width: parent.width width: parent.width
spacing: Theme.spacingS height: 24
StyledRect { StyledRect {
width: Math.min(140, parent.width * 0.42) id: keyBadge
width: Math.min(keyText.implicitWidth + 12, 160)
height: 22 height: 22
radius: 4 radius: 4
opacity: 0.9 anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
id: keyText
anchors.centerIn: parent anchors.centerIn: parent
anchors.margins: 2
width: parent.width - 4
color: Theme.secondary color: Theme.secondary
text: modelData.key || "" text: modelData.key || ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
isMonospace: true isMonospace: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
} }
} }
StyledText { StyledText {
width: parent.width - 150 anchors.left: parent.left
text: modelData.desc || "" anchors.leftMargin: 170
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.desc || modelData.action || ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
opacity: 0.9 opacity: 0.9
elide: Text.ElideRight elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
} }
} }
} }
+1
View File
@@ -59,6 +59,7 @@ DankModal {
modalWidth: 500 modalWidth: 500
modalHeight: 700 modalHeight: 700
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onBackgroundClicked: hide() onBackgroundClicked: hide()
onOpened: () => { onOpened: () => {
+1 -1
View File
@@ -45,7 +45,7 @@ FloatingWindow {
title: I18n.tr("Authentication") title: I18n.tr("Authentication")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
+1 -7
View File
@@ -66,7 +66,7 @@ FloatingWindow {
minimumSize: Qt.size(650, 400) minimumSize: Qt.size(650, 400)
implicitWidth: 900 implicitWidth: 900
implicitHeight: 680 implicitHeight: 680
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -112,12 +112,6 @@ FloatingWindow {
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_1: case Qt.Key_1:
currentTab = 0; currentTab = 0;
+1 -6
View File
@@ -60,7 +60,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 800
implicitHeight: 940 implicitHeight: 940
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onIsCompactModeChanged: { onIsCompactModeChanged: {
@@ -139,11 +139,6 @@ FloatingWindow {
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) { if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) {
sidebar.navigateNext(); sidebar.navigateNext();
event.accepted = true; event.accepted = true;
+1 -1
View File
@@ -191,7 +191,7 @@ FloatingWindow {
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password") title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -97,7 +97,7 @@ DankPopout {
property alias searchField: searchField property alias searchField: searchField
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true antialiasing: true
smooth: true smooth: true
@@ -113,11 +113,7 @@ DankPopout {
implicitHeight: mainColumn.implicitHeight + Theme.spacingM implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector property alias bluetoothCodecSelector: bluetoothCodecSelector
color: { color: "transparent"
const transparency = Theme.popupTransparency;
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
return Qt.rgba(surface.r, surface.g, surface.b, transparency);
}
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
@@ -15,19 +15,19 @@ Rectangle {
function isActiveProfile(profile) { function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
return false return false;
} }
return PowerProfiles.profile === profile return PowerProfiles.profile === profile;
} }
function setProfile(profile) { function setProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available") ToastService.showError("power-profiles-daemon not available");
return return;
} }
PowerProfiles.profile = profile PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) { if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile") ToastService.showError("Failed to set power profile");
} }
} }
@@ -42,7 +42,6 @@ Rectangle {
Row { Row {
id: headerRow id: headerRow
width: parent.width width: parent.width
height: 48
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
@@ -50,10 +49,10 @@ Rectangle {
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error return Theme.error;
if (BatteryService.isCharging || BatteryService.isPluggedIn) if (BatteryService.isCharging || BatteryService.isPluggedIn)
return Theme.primary return Theme.primary;
return Theme.surfaceText return Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -71,12 +70,12 @@ Rectangle {
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) { if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error return Theme.error;
} }
if (BatteryService.isCharging) { if (BatteryService.isCharging) {
return Theme.primary return Theme.primary;
} }
return Theme.surfaceText return Theme.surfaceText;
} }
font.weight: Font.Bold font.weight: Font.Bold
} }
@@ -86,12 +85,12 @@ Rectangle {
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) { if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error return Theme.error;
} }
if (BatteryService.isCharging) { if (BatteryService.isCharging) {
return Theme.primary return Theme.primary;
} }
return Theme.surfaceText return Theme.surfaceText;
} }
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -100,12 +99,13 @@ Rectangle {
StyledText { StyledText {
text: { text: {
if (!BatteryService.batteryAvailable) return "Power profile management available" if (!BatteryService.batteryAvailable)
const time = BatteryService.formatTimeRemaining() return "Power profile management available";
const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") { if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}` return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
} }
return "" return "";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
@@ -145,10 +145,10 @@ Rectangle {
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: { color: {
if (BatteryService.batteryHealth === "N/A") { if (BatteryService.batteryHealth === "N/A") {
return Theme.surfaceText return Theme.surfaceText;
} }
const healthNum = parseInt(BatteryService.batteryHealth) const healthNum = parseInt(BatteryService.batteryHealth);
return healthNum < 80 ? Theme.error : Theme.surfaceText return healthNum < 80 ? Theme.error : Theme.surfaceText;
} }
font.weight: Font.Bold font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -189,8 +189,9 @@ Rectangle {
DankButtonGroup { DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: { property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined") return 1 if (typeof PowerProfiles === "undefined")
return profileModel.findIndex(profile => isActiveProfile(profile)) return 1;
return profileModel.findIndex(profile => isActiveProfile(profile));
} }
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -198,8 +199,9 @@ Rectangle {
selectionMode: "single" selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) return if (!selected)
setProfile(profileModel[index]) return;
setProfile(profileModel[index]);
} }
} }
@@ -255,4 +257,4 @@ Rectangle {
} }
} }
} }
} }
@@ -1,9 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Services.UPower import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -26,13 +23,12 @@ DankPopout {
function setProfile(profile) { function setProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available"); ToastService.showError("power-profiles-daemon not available");
return ; return;
} }
PowerProfiles.profile = profile; PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) { if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile"); ToastService.showError("Failed to set power profile");
} }
} }
popupWidth: 400 popupWidth: 400
@@ -48,7 +44,7 @@ DankPopout {
id: batteryContent id: batteryContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -59,9 +55,8 @@ DankPopout {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus(); forceActiveFocus();
} }
} }
Keys.onPressed: function(event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close(); root.close();
event.accepted = true; event.accepted = true;
@@ -71,11 +66,10 @@ DankPopout {
Connections { Connections {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(function() { Qt.callLater(function () {
batteryContent.forceActiveFocus(); batteryContent.forceActiveFocus();
}); });
} }
} }
target: root target: root
@@ -246,7 +240,8 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!BatteryService.batteryAvailable) return "Power profile management available" if (!BatteryService.batteryAvailable)
return "Power profile management available";
const time = BatteryService.formatTimeRemaining(); const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") { if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`; return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
@@ -470,14 +465,14 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0) if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A" return "N/A";
return `${Math.round(modelData.healthPercentage)}%` return `${Math.round(modelData.healthPercentage)}%`;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0) if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText return Theme.surfaceText;
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText;
} }
font.weight: Font.Bold font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -526,10 +521,7 @@ DankPopout {
spacing: 2 spacing: 2
StyledText { StyledText {
text: modelData.state === UPowerDeviceState.Charging text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : ""
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
font.weight: Font.Medium font.weight: Font.Medium
@@ -538,17 +530,14 @@ DankPopout {
StyledText { StyledText {
text: { text: {
const time = modelData.state === UPowerDeviceState.Charging const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0;
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
if (!time || time <= 0 || time > 86400) if (!time || time <= 0 || time > 86400)
return "N/A" return "N/A";
const hours = Math.floor(time / 3600) const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60) const minutes = Math.floor((time % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m` return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
@@ -566,8 +555,9 @@ DankPopout {
DankButtonGroup { DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: { property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined") return 1 if (typeof PowerProfiles === "undefined")
return profileModel.findIndex(profile => root.isActiveProfile(profile)) return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
} }
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -575,8 +565,9 @@ DankPopout {
selectionMode: "single" selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) return if (!selected)
root.setProfile(profileModel[index]) return;
root.setProfile(profileModel[index]);
} }
} }
@@ -634,5 +625,4 @@ DankPopout {
} }
} }
} }
}
}
@@ -1,8 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -15,31 +11,31 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) { function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x triggerX = x;
triggerY = y triggerY = y;
triggerWidth = width triggerWidth = width;
triggerSection = section triggerSection = section;
root.screen = screen root.screen = screen;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4) storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4 storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
storedBarConfig = barConfig storedBarConfig = barConfig;
const pos = barPosition !== undefined ? barPosition : 0 const pos = barPosition !== undefined ? barPosition : 0;
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0 const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
setBarContext(pos, bottomGap) setBarContext(pos, bottomGap);
updateOutputState() updateOutputState();
} }
onScreenChanged: updateOutputState() onScreenChanged: updateOutputState()
function updateOutputState() { function updateOutputState() {
if (screen && DwlService.dwlAvailable) { if (screen && DwlService.dwlAvailable) {
outputState = DwlService.getOutputState(screen.name) outputState = DwlService.getOutputState(screen.name);
} else { } else {
outputState = null outputState = null;
} }
} }
@@ -47,56 +43,56 @@ DankPopout {
property string currentLayoutSymbol: outputState?.layoutSymbol || "" property string currentLayoutSymbol: outputState?.layoutSymbol || ""
readonly property var layoutNames: ({ readonly property var layoutNames: ({
"CT": I18n.tr("Center Tiling"), "CT": I18n.tr("Center Tiling"),
"G": I18n.tr("Grid"), "G": I18n.tr("Grid"),
"K": I18n.tr("Deck"), "K": I18n.tr("Deck"),
"M": I18n.tr("Monocle"), "M": I18n.tr("Monocle"),
"RT": I18n.tr("Right Tiling"), "RT": I18n.tr("Right Tiling"),
"S": I18n.tr("Scrolling"), "S": I18n.tr("Scrolling"),
"T": I18n.tr("Tiling"), "T": I18n.tr("Tiling"),
"VG": I18n.tr("Vertical Grid"), "VG": I18n.tr("Vertical Grid"),
"VK": I18n.tr("Vertical Deck"), "VK": I18n.tr("Vertical Deck"),
"VS": I18n.tr("Vertical Scrolling"), "VS": I18n.tr("Vertical Scrolling"),
"VT": I18n.tr("Vertical Tiling") "VT": I18n.tr("Vertical Tiling")
}) })
readonly property var layoutIcons: ({ readonly property var layoutIcons: ({
"CT": "view_compact", "CT": "view_compact",
"G": "grid_view", "G": "grid_view",
"K": "layers", "K": "layers",
"M": "fullscreen", "M": "fullscreen",
"RT": "view_sidebar", "RT": "view_sidebar",
"S": "view_carousel", "S": "view_carousel",
"T": "view_quilt", "T": "view_quilt",
"VG": "grid_on", "VG": "grid_on",
"VK": "view_day", "VK": "view_day",
"VS": "scrollable_header", "VS": "scrollable_header",
"VT": "clarify" "VT": "clarify"
}) })
function getLayoutName(symbol) { function getLayoutName(symbol) {
return layoutNames[symbol] || symbol return layoutNames[symbol] || symbol;
} }
function getLayoutIcon(symbol) { function getLayoutIcon(symbol) {
return layoutIcons[symbol] || "view_quilt" return layoutIcons[symbol] || "view_quilt";
} }
Connections { Connections {
target: DwlService target: DwlService
function onStateChanged() { function onStateChanged() {
updateOutputState() updateOutputState();
} }
} }
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
updateOutputState() updateOutputState();
} }
} }
Component.onCompleted: { Component.onCompleted: {
updateOutputState() updateOutputState();
} }
popupWidth: 300 popupWidth: 300
@@ -111,7 +107,7 @@ DankPopout {
id: layoutContent id: layoutContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -121,14 +117,14 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus() forceActiveFocus();
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close() root.close();
event.accepted = true event.accepted = true;
} }
} }
@@ -137,8 +133,8 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(() => { Qt.callLater(() => {
layoutContent.forceActiveFocus() layoutContent.forceActiveFocus();
}) });
} }
} }
} }
@@ -212,7 +208,7 @@ DankPopout {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
root.close() root.close();
} }
} }
} }
@@ -282,14 +278,14 @@ DankPopout {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
if (!root.triggerScreen) { if (!root.triggerScreen) {
return return;
} }
if (!DwlService.dwlAvailable) { if (!DwlService.dwlAvailable) {
return return;
} }
DwlService.setLayout(root.triggerScreen.name, index) DwlService.setLayout(root.triggerScreen.name, index);
root.close() root.close();
} }
} }
@@ -36,7 +36,7 @@ DankPopout {
id: content id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2 implicitHeight: contentColumn.height + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -167,7 +167,7 @@ DankPopout {
id: mainContainer id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
focus: true focus: true
@@ -358,6 +358,8 @@ DankPopout {
popoutWidth: root.alignedWidth popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
section: root.triggerSection
barPosition: root.effectiveBarPosition
Component.onCompleted: root.__mediaTabRef = this Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => { onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players); root.__showVolumeDropdown(pos, rightEdge, player, players);
@@ -382,7 +384,7 @@ DankPopout {
active: true active: true
tabBarItem: tabBar tabBarItem: tabBar
keyForwardTarget: mainContainer keyForwardTarget: mainContainer
targetScreen: root.triggerScreen targetScreen: root.screen
parentPopout: root parentPopout: root
} }
} }

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