1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-04 19:42:08 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Marcus Ramberg
8fad2826b1 ci: add flake check 2025-12-08 16:09:16 +01:00
125 changed files with 3538 additions and 4487 deletions

View File

@@ -10,14 +10,21 @@ jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Check the flake
- name: Update vendorHash in flake.nix
run: nix flake check

View File

@@ -511,7 +511,7 @@ jobs:
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli = %{version}-%{release}
Requires: dms-cli
Requires: dgop
Recommends: cava
@@ -541,6 +541,17 @@ jobs:
Command-line interface for DankMaterialShell configuration and management.
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
%setup -q -c -n dms-qml
@@ -565,10 +576,18 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build
%install
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}/zsh/site-functions
@@ -598,8 +617,10 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
@@ -615,10 +636,14 @@ jobs:
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec

View File

@@ -62,7 +62,7 @@ jobs:
}
echo "✅ Source downloaded"
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
@@ -94,7 +94,7 @@ jobs:
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli = %{version}-%{release}
Requires: dms-cli
Requires: dgop
Recommends: cava
@@ -125,6 +125,17 @@ jobs:
Command-line interface for DankMaterialShell configuration and management.
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
%setup -q -c -n dms-qml
@@ -151,10 +162,19 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_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
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions
@@ -182,8 +202,11 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Signal running DMS instances to reload (harmless if none running)
pkill -USR1 -x dms >/dev/null 2>&1 || :
# Restart DMS for active users after upgrade
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
@@ -197,10 +220,14 @@ jobs:
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec

View File

@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
## Documentation
- **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)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)

View File

@@ -22,8 +22,6 @@ linters:
- (*os.Process).Signal
- (*os.Process).Kill
- syscall.Kill
# Seek on memfd (reset position before passing fd)
- syscall.Seek
# DBus cleanup
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal

View File

@@ -12,11 +12,6 @@ import (
var Version = "dev"
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()
if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err)

View File

@@ -295,14 +295,7 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data()
rgb := make([]byte, dstW*dstH*3)
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale)
@@ -316,17 +309,16 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
}
si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3
if si+3 >= len(data) {
continue
}
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
if si+2 < len(data) {
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
}
}
}
@@ -378,37 +370,7 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
}
for _, o := range outputs {
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
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)
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
}
}

View File

@@ -29,7 +29,6 @@ func runSetup() error {
wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.")
@@ -68,14 +67,14 @@ func runSetup() error {
var err error
if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
} else if wmSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
if len(results) > 1 {
results = results[:1]
}
} else if terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:]
}
@@ -145,19 +144,6 @@ 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 {
homeDir := os.Getenv("HOME")
willBackup := false

View File

@@ -30,7 +30,6 @@ type Output struct {
height int32
scale int32
fractionalScale float64
transform int32
}
type LayerSurface struct {
@@ -277,7 +276,6 @@ func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
if o, ok := p.outputs[name]; ok {
o.x = e.X
o.y = e.Y
o.transform = int32(e.Transform)
}
p.outputsMu.Unlock()
})
@@ -487,19 +485,8 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady()
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()
screenBuf := ls.state.ScreenBuffer()
if logicalW > 0 && screenBuf != nil {
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
}

View File

@@ -4,25 +4,10 @@ import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
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) {
return shm.CreateBuffer(width, height, stride)
}
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
}

View File

@@ -1,7 +1,6 @@
package colorpicker
import (
"fmt"
"math"
"strings"
"sync"
@@ -16,8 +15,6 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
)
type SurfaceState struct {
@@ -82,11 +79,6 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
s.mu.Lock()
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 {
s.screenBuf.Close()
s.screenBuf = nil
@@ -98,7 +90,6 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
}
s.screenBuf = buf
s.screenBuf.Format = format
s.screenFormat = format
return nil
}
@@ -115,20 +106,6 @@ func (s *SurfaceState) ScreenFormat() PixelFormat {
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) {
s.mu.Lock()
s.yInverted = (flags & 1) != 0
@@ -143,15 +120,6 @@ func (s *SurfaceState) OnScreencopyReady() {
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.ensureRenderBuffers()
s.readyForDisplay = true
@@ -311,10 +279,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.Height,
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
px, py, picked, s.yInverted, s.screenFormat,
px, py, picked, s.yInverted,
)
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
return dst
}
@@ -422,7 +390,6 @@ func drawMagnifierWithInversion(
cx, cy int,
borderColor Color,
yInverted bool,
format PixelFormat,
) {
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
return
@@ -440,14 +407,6 @@ func drawMagnifierWithInversion(
innerRadius := float64(outerRadius - borderThickness)
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++ {
y := cy + dy
if y < 0 || y >= dstH {
@@ -472,9 +431,9 @@ func drawMagnifierWithInversion(
}
bgColor := Color{
R: dst[dstOff+rOff],
B: dst[dstOff+0],
G: dst[dstOff+1],
B: dst[dstOff+bOff],
R: dst[dstOff+2],
A: dst[dstOff+3],
}
@@ -503,7 +462,7 @@ func drawMagnifierWithInversion(
}
srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) {
magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
finalColor = blendColors(magColor, borderColor, alpha)
} else {
finalColor = borderColor
@@ -524,25 +483,24 @@ func drawMagnifierWithInversion(
}
srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) {
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
} else {
continue
}
}
dst[dstOff+rOff] = finalColor.R
dst[dstOff+0] = finalColor.B
dst[dstOff+1] = finalColor.G
dst[dstOff+bOff] = finalColor.B
dst[dstOff+2] = finalColor.R
dst[dstOff+3] = 255
}
}
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius)
}
func drawMagnifierCrosshair(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) {
if width <= 0 || height <= 0 {
return
@@ -1040,7 +998,7 @@ var fontGlyphs = map[rune][fontH]uint8{
},
}
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) {
text := formatColorForPreview(c, format, lowercase)
if len(text) == 0 {
return
@@ -1075,8 +1033,9 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
y = height - boxH
}
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c)
// Use contrasting text color based on luminance
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
var fg Color
if lum > 128 {
@@ -1084,7 +1043,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else {
fg = Color{R: 255, G: 255, B: 255, A: 255}
}
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg)
}
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
@@ -1105,7 +1064,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
}
}
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) {
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) {
if w <= 0 || h <= 0 {
return
}
@@ -1114,14 +1073,6 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
x = clamp(x, 0, width)
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++ {
rowOff := yy * stride
for xx := x; xx < xEnd; xx++ {
@@ -1129,34 +1080,26 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
if off+4 > len(data) {
continue
}
data[off+rOff] = col.R
data[off+0] = col.B
data[off+1] = col.G
data[off+bOff] = col.B
data[off+2] = col.R
data[off+3] = 255
}
}
}
func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) {
func drawText(data []byte, stride, width, height, x, y int, text string, col Color) {
for i, r := range text {
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format)
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col)
}
}
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) {
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) {
g, ok := fontGlyphs[r]
if !ok {
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++ {
yy := y + row
if yy < 0 || yy >= height {
@@ -1180,9 +1123,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
continue
}
data[off+rOff] = col.R
data[off+0] = col.B
data[off+1] = col.G
data[off+bOff] = col.B
data[off+2] = col.R
data[off+3] = 255
}
}

View File

@@ -46,20 +46,11 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
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) {
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) {
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
shouldReplaceConfig := func(configType string) bool {
@@ -73,7 +64,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
switch wm {
case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal, useSystemd)
result, err := cd.deployNiriConfig(terminal)
results = append(results, result)
if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
@@ -81,7 +72,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
}
case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal, useSystemd)
result, err := cd.deployHyprlandConfig(terminal)
results = append(results, result)
if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
@@ -119,7 +110,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
return results, nil
}
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
@@ -157,6 +148,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
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
switch terminal {
case deps.TerminalGhostty:
@@ -169,11 +166,8 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
terminalCommand = "ghostty"
}
newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
@@ -410,6 +404,41 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
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
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones)
@@ -453,7 +482,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
}
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
@@ -485,6 +514,14 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
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
switch terminal {
case deps.TerminalGhostty:
@@ -494,15 +531,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
case deps.TerminalAlacritty:
terminalCommand = "alacritty"
default:
terminalCommand = "ghostty"
terminalCommand = "ghostty" // fallback to ghostty
}
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
// If there was an existing config, merge the monitor sections
if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
if err != nil {
@@ -525,16 +560,24 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
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*=.*$`)
// Find all monitor lines in the existing config
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 {
// No monitor sections to merge
return newConfig, nil
}
// Remove the example monitor line from the new config
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
@@ -542,7 +585,8 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return "", fmt.Errorf("could not find MONITOR CONFIG section")
}
insertPos := headerMatch[1] + 1
// Insert after the header
insertPos := headerMatch[1] + 1 // +1 for the newline
var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos])
@@ -557,69 +601,3 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
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
}

View File

@@ -3,6 +3,7 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -10,6 +11,23 @@ import (
"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) {
cd := &ConfigDeployer{}
@@ -254,6 +272,17 @@ func getGhosttyPath() string {
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) {
cd := &ConfigDeployer{}
@@ -395,7 +424,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType)
@@ -406,7 +435,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
assert.Contains(t, string(content), "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, string(content), "exec-once = ")
})
@@ -425,7 +454,7 @@ general {
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType)
@@ -442,7 +471,7 @@ general {
require.NoError(t, err)
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), "bind = $mod, T, exec, kitty")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, $TERMINAL")
assert.NotContains(t, string(newContent), "monitor = eDP-2")
})
}
@@ -450,6 +479,7 @@ general {
func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
@@ -460,9 +490,11 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
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, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
assert.Contains(t, HyprlandConfig, "windowrule {")
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
}
func TestGhosttyConfigStructure(t *testing.T) {

View File

@@ -10,9 +10,8 @@ monitor = , preferred,auto,auto
# ==================
# 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 = {{POLKIT_AGENT_PATH}}
# ==================
# INPUT CONFIG
@@ -91,36 +90,132 @@ misc {
# ==================
# WINDOW RULES
# ==================
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
windowrule {
name = windowrule-1
tile = on
match:class = ^(org\.wezfurlong\.wezterm)$
border_size = 0
}
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrulev2 = tile, class:^(gnome-control-center)$
windowrulev2 = tile, class:^(pavucontrol)$
windowrulev2 = tile, class:^(nm-connection-editor)$
windowrule {
name = windowrule-2
rounding = 12
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)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$
windowrule {
name = windowrule-3
tile = on
match:class = ^(gnome-control-center)$
}
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
windowrule {
name = windowrule-4
tile = on
match:class = ^(pavucontrol)$
}
layerrule = noanim, ^(quickshell)$
windowrule {
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
@@ -128,7 +223,7 @@ layerrule = noanim, ^(quickshell)$
$mod = SUPER
# === Application Launchers ===
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, T, exec, $TERMINAL
bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle

View File

@@ -44,6 +44,7 @@ input {
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
gaps 5
background-color "transparent"
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
@@ -86,6 +87,11 @@ layout {
inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red
}
focus-ring {
width 2
active-color "#808080" // Medium gray
inactive-color "#505050" // Dark gray
}
shadow {
softness 30
spread 5
@@ -110,6 +116,7 @@ overview {
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment {
XDG_CURRENT_DESKTOP "niri"
}

View File

@@ -182,11 +182,13 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
}
// ! 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}
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
}
@@ -360,10 +362,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
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 {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}

View File

@@ -611,41 +611,6 @@ func (b *BaseDistribution) EnableDMSService(ctx context.Context) error {
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
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
b.log("Installing/updating DMS binary...")

View File

@@ -208,7 +208,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
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,
"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")
"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 {
return fmt.Errorf("failed to install development tools: %w", err)
}
@@ -338,10 +338,6 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
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 {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -453,7 +449,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
}
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
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)
@@ -471,7 +467,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
}
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
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)
}
@@ -506,7 +502,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
args := []string{"apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
@@ -616,7 +612,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}
@@ -655,7 +651,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
}

View File

@@ -166,7 +166,10 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
}
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
func (f *FedoraDistribution) getHyprlandMapping(variant 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"}
}
@@ -174,7 +177,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
}
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -359,10 +362,6 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
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 {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}

View File

@@ -95,6 +95,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectPolkitAgent())
dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland {
@@ -126,6 +127,20 @@ 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 {
status := deps.StatusMissing
if g.packageInstalled("gui-apps/xwayland-satellite") {
@@ -172,6 +187,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
@@ -207,8 +223,12 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
}
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
archKeyword := 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 {
@@ -436,10 +456,6 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
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 {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}

View File

@@ -377,10 +377,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
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 {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -476,7 +472,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true

View File

@@ -357,10 +357,6 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
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 {
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}

View File

@@ -154,12 +154,11 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
}
bind := keybinds.Keybind{
Key: keyStr,
Description: kb.Description,
Action: rawAction,
Subcategory: subcategory,
Source: source,
HideOnOverlay: kb.HideOnOverlay,
Key: keyStr,
Description: kb.Description,
Action: rawAction,
Subcategory: subcategory,
Source: source,
}
if source == "dms" && conflicts != nil {

View File

@@ -11,13 +11,12 @@ import (
)
type NiriKeyBinding struct {
Mods []string
Key string
Action string
Args []string
Description string
HideOnOverlay bool
Source string
Mods []string
Key string
Action string
Args []string
Description string
Source string
}
type NiriSection struct {
@@ -274,26 +273,19 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
}
var description string
var hideOnOverlay bool
if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
switch val.ValueString() {
case "null", "":
hideOnOverlay = true
default:
description = val.ValueString()
}
description = val.ValueString()
}
}
return &NiriKeyBinding{
Mods: mods,
Key: key,
Action: action,
Args: args,
Description: description,
HideOnOverlay: hideOnOverlay,
Source: p.currentSource,
Mods: mods,
Key: key,
Action: action,
Args: args,
Description: description,
Source: p.currentSource,
}
}

View File

@@ -1,13 +1,12 @@
package keybinds
type Keybind struct {
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"`
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
}
type DMSBindsStatus struct {

View File

@@ -238,17 +238,9 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
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
if proxy != nil {
e.Head = proxy.(*ZwlrOutputHeadV1)
} else {
// Stale proxy of wrong type (can happen after suspend/resume)
// Replace it with the correct type
head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context())
head.SetID(objectID)
@@ -723,17 +715,9 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
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
if proxy != nil {
e.Mode = proxy.(*ZwlrOutputModeV1)
} 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)
@@ -759,26 +743,7 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
}
var e ZwlrOutputHeadV1CurrentModeEvent
l := 0
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
}
e.Mode = i.Context().GetProxy(client.Uint32(data[l : l+4])).(*ZwlrOutputModeV1)
l += 4
i.currentModeHandler(e)

View File

@@ -7,7 +7,6 @@ import (
"os/exec"
"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"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
@@ -90,15 +89,12 @@ func SetCompositorDWL() {
}
type WindowGeometry struct {
X int32
Y int32
Width int32
Height int32
Output string
Scale float64
OutputX int32
OutputY int32
OutputTransform int32
X int32
Y int32
Width int32
Height int32
Output string
Scale float64
}
func GetActiveWindow() (*WindowGeometry, error) {
@@ -386,92 +382,6 @@ func GetFocusedMonitor() string {
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) {
display, err := client.Connect("")
if err != nil {
@@ -599,23 +509,14 @@ func getDWLActiveWindow() (*WindowGeometry, error) {
if scale <= 0 {
scale = 1.0
}
geom := &WindowGeometry{
return &WindowGeometry{
X: state.x,
Y: state.y,
Width: state.w,
Height: state.h,
Output: state.name,
Scale: scale,
}
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
}, nil
}
return nil, fmt.Errorf("no active output found")

View File

@@ -20,13 +20,7 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
data := buf.Data()
var swapRB bool
switch format {
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
for y := 0; y < buf.Height; y++ {
srcOff := y * buf.Stride

View File

@@ -380,24 +380,19 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
return
}
var capturedBuf *ShmBuffer
var capturedFormat PixelFormat
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))
if err != nil {
log.Error("create screen buffer failed", "err", err)
return
}
capturedBuf = buf
buf.Format = capturedFormat
if withCursor {
pc.screenBuf = buf
pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil {
@@ -426,47 +421,6 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
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()
})

View File

@@ -150,33 +150,51 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
case CompositorHyprland:
return s.captureAndCrop(output, region)
case CompositorDWL:
return s.captureDWLWindow(output, region, geom)
return s.captureDWLWindow(output, region, geom.Scale)
default:
return s.captureRegionOnOutput(output, region)
}
}
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, dwlScale float64) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
}
scale := geom.Scale
if scale <= 0 || scale == 1.0 {
if output.fractionalScale > 1.0 {
scale = output.fractionalScale
}
scale := dwlScale
if scale <= 0 {
scale = float64(result.Buffer.Width) / float64(output.width)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-geom.OutputX) * scale)
localY := int(float64(region.Y-geom.OutputY) * scale)
localX := int(float64(region.X) * scale)
localY := int(float64(region.Y) * 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)
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 {
w += localX
localX = 0
@@ -324,18 +342,13 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
outX, outY := output.x, output.y
scale := float64(output.scale)
switch DetectCompositor() {
case CompositorHyprland:
if DetectCompositor() == CompositorHyprland {
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy
}
if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s
}
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
}
if scale <= 0 {
scale = 1.0
@@ -463,42 +476,13 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
return nil, fmt.Errorf("capture output: %w", err)
}
result, err := s.processFrame(frame, Region{
return s.processFrame(frame, Region{
X: output.x,
Y: output.y,
Width: output.width,
Height: output.height,
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) {
@@ -579,10 +563,6 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
}
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
if output.transform != TransformNormal {
return s.captureRegionOnTransformedOutput(output, region)
}
scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
@@ -637,76 +617,6 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
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) {
var buf *ShmBuffer
var pool *client.ShmPool
@@ -717,18 +627,13 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
failed := false
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
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil {
log.Error("failed to create buffer", "err", err)
return
}
format = PixelFormat(e.Format)
buf.Format = format
})
@@ -791,19 +696,6 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
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{
Buffer: buf,
Region: region,
@@ -1032,32 +924,16 @@ func ListOutputs() ([]Output, error) {
sc.outputsMu.Lock()
defer sc.outputsMu.Unlock()
compositor := DetectCompositor()
result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs {
out := Output{
Name: o.name,
X: o.x,
Y: o.y,
Width: o.width,
Height: o.height,
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)
result = append(result, Output{
Name: o.name,
X: o.x,
Y: o.y,
Width: o.width,
Height: o.height,
Scale: o.scale,
})
}
return result, nil
}

View File

@@ -9,19 +9,6 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888
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
@@ -29,7 +16,3 @@ type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride)
}
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}

View File

@@ -32,13 +32,11 @@ func (r Region) IsEmpty() bool {
}
type Output struct {
Name string
X, Y int32
Width int32
Height int32
Scale int32
FractionalScale float64
Transform int32
Name string
X, Y int32
Width int32
Height int32
Scale int32
}
type Config struct {

View File

@@ -7,7 +7,13 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
type Request struct {
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 {
case "apppicker.open", "browser.open":
handleOpen(conn, req, manager)
@@ -16,7 +22,7 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
func handleOpen(conn net.Conn, req Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := req.Params["target"].(string)

View File

@@ -6,15 +6,25 @@ import (
"net"
"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 string `json:"type"`
Data BluetoothState `json:"data"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "bluetooth.getState":
handleGetState(conn, req, manager)
@@ -47,30 +57,31 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
func handleStartDiscovery(conn net.Conn, req Request, manager *Manager) {
if err := manager.StartDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery started"})
}
func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
func handleStopDiscovery(conn net.Conn, req Request, manager *Manager) {
if err := manager.StopDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery stopped"})
}
func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
powered, err := params.Bool(req.Params, "powered")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetPowered(conn net.Conn, req Request, manager *Manager) {
powered, ok := req.Params["powered"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'powered' parameter")
return
}
@@ -79,13 +90,13 @@ func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "powered state updated"})
}
func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handlePairDevice(conn net.Conn, req Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
return
}
@@ -94,13 +105,13 @@ func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing initiated"})
}
func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleConnectDevice(conn net.Conn, req Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
return
}
@@ -109,13 +120,13 @@ func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
}
func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleDisconnectDevice(conn net.Conn, req Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
return
}
@@ -124,13 +135,13 @@ func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager)
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"})
}
func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleRemoveDevice(conn net.Conn, req Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
return
}
@@ -139,13 +150,13 @@ func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device removed"})
}
func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleTrustDevice(conn net.Conn, req Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
return
}
@@ -154,13 +165,13 @@ func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device trusted"})
}
func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleUntrustDevice(conn net.Conn, req Request, manager *Manager) {
devicePath, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
return
}
@@ -169,31 +180,43 @@ func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device untrusted"})
}
func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handlePairingSubmit(conn net.Conn, req Request, manager *Manager) {
token, ok := req.Params["token"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
return
}
secrets := params.StringMapOpt(req.Params, "secrets")
accept := params.BoolOpt(req.Params, "accept", false)
secretsRaw, ok := req.Params["secrets"].(map[string]any)
secrets := make(map[string]string)
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 {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing response submitted"})
}
func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handlePairingCancel(conn net.Conn, req Request, manager *Manager) {
token, ok := req.Params["token"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
return
}
@@ -202,10 +225,10 @@ func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing cancelled"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)

View File

@@ -2,14 +2,12 @@ package brightness
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
func HandleRequest(conn net.Conn, req Request, m *Manager) {
switch req.Method {
case "brightness.getState":
handleGetState(conn, req, m)
@@ -24,90 +22,131 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
case "brightness.subscribe":
handleSubscribe(conn, req, m)
default:
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method)
}
}
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
func handleGetState(conn net.Conn, req Request, m *Manager) {
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}
func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetBrightness(conn net.Conn, req Request, m *Manager) {
var params SetBrightnessParams
device, ok := req.Params["device"].(string)
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
}
percent, err := params.Int(req.Params, "percent")
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())
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}
func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleIncrement(conn net.Conn, req Request, m *Manager) {
device, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter")
return
}
step := params.IntOpt(req.Params, "step", 10)
exponential := params.BoolOpt(req.Params, "exponential", false)
exponent := params.FloatOpt(req.Params, "exponent", 1.2)
step := 10
if stepFloat, ok := req.Params["step"].(float64); ok {
step = int(stepFloat)
}
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 {
models.RespondError(conn, req.ID, err.Error())
models.RespondError(conn, req.ID.(int), err.Error())
return
}
models.Respond(conn, req.ID, m.GetState())
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}
func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleDecrement(conn net.Conn, req Request, m *Manager) {
device, ok := req.Params["device"].(string)
if !ok {
models.RespondError(conn, req.ID.(int), "missing or invalid device parameter")
return
}
step := params.IntOpt(req.Params, "step", 10)
exponential := params.BoolOpt(req.Params, "exponential", false)
exponent := params.FloatOpt(req.Params, "exponent", 1.2)
step := 10
if stepFloat, ok := req.Params["step"].(float64); ok {
step = int(stepFloat)
}
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 {
models.RespondError(conn, req.ID, err.Error())
models.RespondError(conn, req.ID.(int), err.Error())
return
}
models.Respond(conn, req.ID, m.GetState())
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}
func handleRescan(conn net.Conn, req models.Request, m *Manager) {
func handleRescan(conn net.Conn, req Request, m *Manager) {
m.Rescan()
models.Respond(conn, req.ID, m.GetState())
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("brightness-%d", req.ID)
func handleSubscribe(conn net.Conn, req Request, m *Manager) {
clientID := "brightness-subscriber"
if idStr, ok := req.ID.(string); ok && idStr != "" {
clientID = idStr
}
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
ID: req.ID.(int),
Result: &initialState,
}); err != nil {
return
@@ -115,7 +154,7 @@ func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
ID: req.ID.(int),
Result: &state,
}); err != nil {
return

View File

@@ -33,6 +33,12 @@ type DeviceUpdate struct {
Device Device `json:"device"`
}
type Request struct {
ID any `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
type Manager struct {
logindBackend *LogindBackend
sysfsBackend *SysfsBackend
@@ -106,6 +112,13 @@ type ddcCapability struct {
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 {
ch := make(chan State, 16)

View File

@@ -6,7 +6,13 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
type Request struct {
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 {
case "browser.open":
url, ok := req.Params["url"].(string)

View File

@@ -6,21 +6,25 @@ import (
"net"
"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 string `json:"type"`
Data CUPSState `json:"data"`
}
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) {
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "cups.subscribe":
handleSubscribe(conn, req, manager)
@@ -75,19 +79,20 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
func handleGetPrinters(conn net.Conn, req Request, manager *Manager) {
printers, err := manager.GetPrinters()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, printers)
}
func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleGetJobs(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
@@ -96,13 +101,14 @@ func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, jobs)
}
func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handlePausePrinter(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
@@ -110,13 +116,13 @@ func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "paused"})
}
func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleResumePrinter(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
@@ -124,27 +130,28 @@ func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "resumed"})
}
func handleCancelJob(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())
func handleCancelJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobid' parameter")
return
}
jobID := int(jobIDFloat)
if err := manager.CancelJob(jobID); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job canceled"})
}
func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
@@ -152,10 +159,10 @@ func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "jobs canceled"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -186,7 +193,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
func handleGetDevices(conn net.Conn, req Request, manager *Manager) {
devices, err := manager.GetDevices()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -195,7 +202,7 @@ func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, devices)
}
func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
func handleGetPPDs(conn net.Conn, req Request, manager *Manager) {
ppds, err := manager.GetPPDs()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -204,7 +211,7 @@ func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, ppds)
}
func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
func handleGetClasses(conn net.Conn, req Request, manager *Manager) {
classes, err := manager.GetClasses()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -213,41 +220,41 @@ func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, classes)
}
func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
name, err := params.StringNonEmpty(req.Params, "name")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleCreatePrinter(conn net.Conn, req Request, manager *Manager) {
name, ok := req.Params["name"].(string)
if !ok || name == "" {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
deviceURI, err := params.StringNonEmpty(req.Params, "deviceURI")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
deviceURI, ok := req.Params["deviceURI"].(string)
if !ok || deviceURI == "" {
models.RespondError(conn, req.ID, "missing or invalid 'deviceURI' parameter")
return
}
ppd, err := params.StringNonEmpty(req.Params, "ppd")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
ppd, ok := req.Params["ppd"].(string)
if !ok || ppd == "" {
models.RespondError(conn, req.ID, "missing or invalid 'ppd' parameter")
return
}
shared := params.BoolOpt(req.Params, "shared", false)
errorPolicy := params.StringOpt(req.Params, "errorPolicy", "")
information := params.StringOpt(req.Params, "information", "")
location := params.StringOpt(req.Params, "location", "")
shared, _ := req.Params["shared"].(bool)
errorPolicy, _ := req.Params["errorPolicy"].(string)
information, _ := req.Params["information"].(string)
location, _ := req.Params["location"].(string)
if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer created"})
}
func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleDeletePrinter(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
}
@@ -255,13 +262,13 @@ func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer deleted"})
}
func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleAcceptJobs(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
}
@@ -269,13 +276,13 @@ func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "accepting jobs"})
}
func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleRejectJobs(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
}
@@ -283,19 +290,19 @@ func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "rejecting jobs"})
}
func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetPrinterShared(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
}
shared, err := params.Bool(req.Params, "shared")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
shared, ok := req.Params["shared"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'shared' parameter")
return
}
@@ -303,19 +310,19 @@ func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager)
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sharing updated"})
}
func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetPrinterLocation(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
}
location, err := params.String(req.Params, "location")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
location, ok := req.Params["location"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter")
return
}
@@ -323,19 +330,19 @@ func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manage
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location updated"})
}
func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetPrinterInfo(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
}
info, err := params.String(req.Params, "info")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
info, ok := req.Params["info"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'info' parameter")
return
}
@@ -343,33 +350,39 @@ func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "info updated"})
}
func handleMoveJob(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())
func handleMoveJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
destPrinter, err := params.StringNonEmpty(req.Params, "destPrinter")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
destPrinter, ok := req.Params["destPrinter"].(string)
if !ok || destPrinter == "" {
models.RespondError(conn, req.ID, "missing or invalid 'destPrinter' parameter")
return
}
if err := manager.MoveJob(jobID, destPrinter); err != nil {
if err := manager.MoveJob(int(jobIDFloat), destPrinter); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job moved"})
}
func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
type TestPageResult struct {
Success bool `json:"success"`
JobID int `json:"jobId"`
Message string `json:"message"`
}
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
}
@@ -381,16 +394,16 @@ func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
}
func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) {
className, ok := req.Params["className"].(string)
if !ok || className == "" {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter")
return
}
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
@@ -398,19 +411,19 @@ func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer added to class"})
}
func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager) {
className, ok := req.Params["className"].(string)
if !ok || className == "" {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter")
return
}
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
@@ -418,13 +431,13 @@ func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Ma
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer removed from class"})
}
func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleDeleteClass(conn net.Conn, req Request, manager *Manager) {
className, ok := req.Params["className"].(string)
if !ok || className == "" {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter")
return
}
@@ -432,35 +445,38 @@ func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "class deleted"})
}
func handleRestartJob(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())
func handleRestartJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
if err := manager.RestartJob(jobID); err != nil {
if err := manager.RestartJob(int(jobIDFloat)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
models.Respond(conn, req.ID, 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())
func handleHoldJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
holdUntil := params.StringOpt(req.Params, "holdUntil", "indefinite")
holdUntil, _ := req.Params["holdUntil"].(string)
if holdUntil == "" {
holdUntil = "indefinite"
}
if err := manager.HoldJob(jobID, holdUntil); err != nil {
if err := manager.HoldJob(int(jobIDFloat), holdUntil); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job held"})
}

View File

@@ -43,7 +43,7 @@ func TestHandleGetPrinters(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.getPrinters",
}
@@ -68,7 +68,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.getPrinters",
}
@@ -100,7 +100,7 @@ func TestHandleGetJobs(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.getJobs",
Params: map[string]any{
@@ -127,7 +127,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.getJobs",
Params: map[string]any{},
@@ -152,7 +152,7 @@ func TestHandlePausePrinter(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.pausePrinter",
Params: map[string]any{
@@ -162,7 +162,7 @@ func TestHandlePausePrinter(t *testing.T) {
handlePausePrinter(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -179,7 +179,7 @@ func TestHandleResumePrinter(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.resumePrinter",
Params: map[string]any{
@@ -189,7 +189,7 @@ func TestHandleResumePrinter(t *testing.T) {
handleResumePrinter(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -206,7 +206,7 @@ func TestHandleCancelJob(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.cancelJob",
Params: map[string]any{
@@ -216,7 +216,7 @@ func TestHandleCancelJob(t *testing.T) {
handleCancelJob(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -233,7 +233,7 @@ func TestHandlePurgeJobs(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.purgeJobs",
Params: map[string]any{
@@ -243,7 +243,7 @@ func TestHandlePurgeJobs(t *testing.T) {
handlePurgeJobs(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -260,7 +260,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.unknownMethod",
}
@@ -287,7 +287,7 @@ func TestHandleGetDevices(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getDevices"}
req := Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m)
var resp models.Response[[]Device]
@@ -309,7 +309,7 @@ func TestHandleGetPPDs(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getPPDs"}
req := Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m)
var resp models.Response[[]PPD]
@@ -332,7 +332,7 @@ func TestHandleGetClasses(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getClasses"}
req := Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m)
var resp models.Response[[]PrinterClass]
@@ -353,7 +353,7 @@ func TestHandleCreatePrinter(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.createPrinter",
Params: map[string]any{
@@ -364,7 +364,7 @@ func TestHandleCreatePrinter(t *testing.T) {
}
handleCreatePrinter(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -377,7 +377,7 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m)
var resp models.Response[any]
@@ -396,14 +396,14 @@ func TestHandleDeletePrinter(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.deletePrinter",
Params: map[string]any{"printerName": "printer1"},
}
handleDeletePrinter(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -419,14 +419,14 @@ func TestHandleAcceptJobs(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.acceptJobs",
Params: map[string]any{"printerName": "printer1"},
}
handleAcceptJobs(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -442,14 +442,14 @@ func TestHandleRejectJobs(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.rejectJobs",
Params: map[string]any{"printerName": "printer1"},
}
handleRejectJobs(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -465,14 +465,14 @@ func TestHandleSetPrinterShared(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.setPrinterShared",
Params: map[string]any{"printerName": "printer1", "shared": true},
}
handleSetPrinterShared(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -488,14 +488,14 @@ func TestHandleSetPrinterLocation(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.setPrinterLocation",
Params: map[string]any{"printerName": "printer1", "location": "Office"},
}
handleSetPrinterLocation(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -511,14 +511,14 @@ func TestHandleSetPrinterInfo(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.setPrinterInfo",
Params: map[string]any{"printerName": "printer1", "info": "Main Printer"},
}
handleSetPrinterInfo(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -534,14 +534,14 @@ func TestHandleMoveJob(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.moveJob",
Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"},
}
handleMoveJob(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -557,7 +557,7 @@ func TestHandlePrintTestPage(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.printTestPage",
Params: map[string]any{"printerName": "printer1"},
@@ -581,14 +581,14 @@ func TestHandleAddPrinterToClass(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.addPrinterToClass",
Params: map[string]any{"className": "office", "printerName": "printer1"},
}
handleAddPrinterToClass(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -604,14 +604,14 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.removePrinterFromClass",
Params: map[string]any{"className": "office", "printerName": "printer1"},
}
handleRemovePrinterFromClass(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -627,14 +627,14 @@ func TestHandleDeleteClass(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.deleteClass",
Params: map[string]any{"className": "office"},
}
handleDeleteClass(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -650,14 +650,14 @@ func TestHandleRestartJob(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.restartJob",
Params: map[string]any{"jobID": float64(1)},
}
handleRestartJob(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -673,14 +673,14 @@ func TestHandleHoldJob(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1)},
}
handleHoldJob(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
@@ -696,14 +696,14 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
req := Request{
ID: 1,
Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"},
}
handleHoldJob(conn, req, m)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)

View File

@@ -8,12 +8,18 @@ import (
"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"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized")
return
@@ -35,12 +41,12 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
func handleSetTags(conn net.Conn, req Request, manager *Manager) {
output, ok := req.Params["output"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -67,7 +73,7 @@ func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"})
}
func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
func handleSetClientTags(conn net.Conn, req Request, manager *Manager) {
output, ok := req.Params["output"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -94,7 +100,7 @@ func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"})
}
func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
func handleSetLayout(conn net.Conn, req Request, manager *Manager) {
output, ok := req.Params["output"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
@@ -115,7 +121,7 @@ func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)

View File

@@ -6,15 +6,22 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
type Request struct {
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 {
case "evdev.getState":
handleGetState(conn, req, m)
default:
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method)
}
}
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
func handleGetState(conn net.Conn, req Request, m *Manager) {
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}

View File

@@ -53,7 +53,7 @@ func TestHandleRequest(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "evdev.getState",
Params: map[string]any{},
@@ -82,7 +82,7 @@ func TestHandleRequest(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 456,
Method: "evdev.unknownMethod",
Params: map[string]any{},
@@ -111,7 +111,7 @@ func TestHandleGetState(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 789,
Method: "evdev.getState",
Params: map[string]any{},

View File

@@ -306,15 +306,6 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
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]
m.updateCapsLockStateDirect(capsLockState)
}

View File

@@ -8,12 +8,18 @@ import (
"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"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "extworkspace manager not initialized")
return
@@ -37,12 +43,12 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
func handleActivateWorkspace(conn net.Conn, req Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
groupID = ""
@@ -62,7 +68,7 @@ func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"})
}
func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
func handleDeactivateWorkspace(conn net.Conn, req Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
groupID = ""
@@ -82,7 +88,7 @@ func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manag
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"})
}
func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
func handleRemoveWorkspace(conn net.Conn, req Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
groupID = ""
@@ -102,7 +108,7 @@ func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"})
}
func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
func handleCreateWorkspace(conn net.Conn, req Request, manager *Manager) {
groupID, ok := req.Params["groupID"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
@@ -123,7 +129,7 @@ func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)

View File

@@ -5,10 +5,21 @@ import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
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"`
Value string `json:"value,omitempty"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "freedesktop.getState":
handleGetState(conn, req, manager)
@@ -33,14 +44,15 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
iconPath, err := params.String(req.Params, "path")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetIconFile(conn net.Conn, req Request, manager *Manager) {
iconPath, ok := req.Params["path"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'path' parameter")
return
}
@@ -49,13 +61,13 @@ func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon file set"})
}
func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
name, err := params.String(req.Params, "name")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetRealName(conn net.Conn, req Request, manager *Manager) {
name, ok := req.Params["name"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
@@ -64,13 +76,13 @@ func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "real name set"})
}
func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
email, err := params.String(req.Params, "email")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetEmail(conn net.Conn, req Request, manager *Manager) {
email, ok := req.Params["email"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'email' parameter")
return
}
@@ -79,13 +91,13 @@ func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "email set"})
}
func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
language, err := params.String(req.Params, "language")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetLanguage(conn net.Conn, req Request, manager *Manager) {
language, ok := req.Params["language"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'language' parameter")
return
}
@@ -94,13 +106,13 @@ func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "language set"})
}
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
location, err := params.String(req.Params, "location")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
location, ok := req.Params["location"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter")
return
}
@@ -109,13 +121,13 @@ func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"})
}
func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
username, err := params.String(req.Params, "username")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleGetUserIconFile(conn net.Conn, req Request, manager *Manager) {
username, ok := req.Params["username"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'username' parameter")
return
}
@@ -125,10 +137,10 @@ func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager)
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
models.Respond(conn, req.ID, SuccessResult{Success: true, Value: iconFile})
}
func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
func handleGetColorScheme(conn net.Conn, req Request, manager *Manager) {
if err := manager.updateSettingsState(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -138,10 +150,10 @@ func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
}
func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
iconTheme, err := params.String(req.Params, "iconTheme")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetIconTheme(conn net.Conn, req Request, manager *Manager) {
iconTheme, ok := req.Params["iconTheme"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'iconTheme' parameter")
return
}
@@ -150,5 +162,5 @@ func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon theme set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon theme set"})
}

View File

@@ -74,10 +74,10 @@ func TestRespondError_Freedesktop(t *testing.T) {
func TestRespond_Freedesktop(t *testing.T) {
conn := newMockNetConn()
result := models.SuccessResult{Success: true, Message: "test"}
result := SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -106,7 +106,7 @@ func TestHandleGetState(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "freedesktop.getState"}
req := Request{ID: 123, Method: "freedesktop.getState"}
handleGetState(conn, req, manager)
@@ -131,7 +131,7 @@ func TestHandleSetIconFile(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{},
@@ -164,7 +164,7 @@ func TestHandleSetIconFile(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{
@@ -174,7 +174,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -196,7 +196,7 @@ func TestHandleSetIconFile(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
Params: map[string]any{
@@ -206,7 +206,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -223,7 +223,7 @@ func TestHandleSetRealName(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setRealName",
Params: map[string]any{},
@@ -256,7 +256,7 @@ func TestHandleSetRealName(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setRealName",
Params: map[string]any{
@@ -266,7 +266,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -286,7 +286,7 @@ func TestHandleSetEmail(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setEmail",
Params: map[string]any{},
@@ -319,7 +319,7 @@ func TestHandleSetEmail(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setEmail",
Params: map[string]any{
@@ -329,7 +329,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -349,7 +349,7 @@ func TestHandleSetLanguage(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setLanguage",
Params: map[string]any{},
@@ -374,7 +374,7 @@ func TestHandleSetLocation(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.setLocation",
Params: map[string]any{},
@@ -399,7 +399,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.getUserIconFile",
Params: map[string]any{},
@@ -426,7 +426,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.accounts.getUserIconFile",
Params: map[string]any{
@@ -436,7 +436,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -457,7 +457,7 @@ func TestHandleGetColorScheme(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager)
@@ -488,7 +488,7 @@ func TestHandleGetColorScheme(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager)
@@ -516,7 +516,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.unknown",
}
@@ -533,7 +533,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "freedesktop.getState",
}
@@ -561,7 +561,7 @@ func TestHandleRequest(t *testing.T) {
for _, method := range tests {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: method,
Params: map[string]any{},

View File

@@ -6,10 +6,20 @@ import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
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"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "loginctl.getState":
handleGetState(conn, req, manager)
@@ -36,38 +46,39 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleLock(conn net.Conn, req models.Request, manager *Manager) {
func handleLock(conn net.Conn, req Request, manager *Manager) {
if err := manager.Lock(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "locked"})
}
func handleUnlock(conn net.Conn, req models.Request, manager *Manager) {
func handleUnlock(conn net.Conn, req Request, manager *Manager) {
if err := manager.Unlock(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "unlocked"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "unlocked"})
}
func handleActivate(conn net.Conn, req models.Request, manager *Manager) {
func handleActivate(conn net.Conn, req Request, manager *Manager) {
if err := manager.Activate(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "activated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "activated"})
}
func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
idle, err := params.Bool(req.Params, "idle")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) {
idle, ok := req.Params["idle"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'idle' parameter")
return
}
@@ -75,32 +86,32 @@ func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "idle hint set"})
}
func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetLockBeforeSuspend(conn net.Conn, req Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter")
return
}
manager.SetLockBeforeSuspend(enabled)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "lock before suspend set"})
}
func handleSetSleepInhibitorEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetSleepInhibitorEnabled(conn net.Conn, req Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter")
return
}
manager.SetSleepInhibitorEnabled(enabled)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
}
func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) {
func handleLockerReady(conn net.Conn, req Request, manager *Manager) {
manager.lockTimerMu.Lock()
if manager.lockTimer != nil {
manager.lockTimer.Stop()
@@ -114,18 +125,18 @@ func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) {
if manager.inSleepCycle.Load() {
manager.signalLockerReady()
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "ok"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "ok"})
}
func handleTerminate(conn net.Conn, req models.Request, manager *Manager) {
func handleTerminate(conn net.Conn, req Request, manager *Manager) {
if err := manager.Terminate(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "terminated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "terminated"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)

View File

@@ -58,10 +58,10 @@ func TestRespondError_Loginctl(t *testing.T) {
func TestRespond_Loginctl(t *testing.T) {
conn := newMockNetConn()
result := models.SuccessResult{Success: true, Message: "test"}
result := SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -86,7 +86,7 @@ func TestHandleGetState(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.getState"}
req := Request{ID: 123, Method: "loginctl.getState"}
handleGetState(conn, req, manager)
@@ -115,10 +115,10 @@ func TestHandleLock(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.lock"}
req := Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -141,10 +141,10 @@ func TestHandleLock(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.lock"}
req := Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -166,10 +166,10 @@ func TestHandleUnlock(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.unlock"}
req := Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -192,10 +192,10 @@ func TestHandleUnlock(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.unlock"}
req := Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -217,10 +217,10 @@ func TestHandleActivate(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.activate"}
req := Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -243,10 +243,10 @@ func TestHandleActivate(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.activate"}
req := Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -263,7 +263,7 @@ func TestHandleSetIdleHint(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "loginctl.setIdleHint",
Params: map[string]any{},
@@ -291,7 +291,7 @@ func TestHandleSetIdleHint(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "loginctl.setIdleHint",
Params: map[string]any{
@@ -301,7 +301,7 @@ func TestHandleSetIdleHint(t *testing.T) {
handleSetIdleHint(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -324,7 +324,7 @@ func TestHandleSetIdleHint(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "loginctl.setIdleHint",
Params: map[string]any{
@@ -334,7 +334,7 @@ func TestHandleSetIdleHint(t *testing.T) {
handleSetIdleHint(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -356,10 +356,10 @@ func TestHandleTerminate(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.terminate"}
req := Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -382,10 +382,10 @@ func TestHandleTerminate(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.terminate"}
req := Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -405,7 +405,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "loginctl.unknown",
}
@@ -422,7 +422,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "loginctl.getState",
}
@@ -445,7 +445,7 @@ func TestHandleRequest(t *testing.T) {
manager.sessionObj = mockSessionObj
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "loginctl.lock",
}
@@ -470,7 +470,7 @@ func TestHandleSubscribe(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "loginctl.subscribe"}
req := Request{ID: 123, Method: "loginctl.subscribe"}
done := make(chan bool)
go func() {

View File

@@ -29,9 +29,3 @@ func Respond[T any](conn net.Conn, id int, result T) {
resp := Response[T]{ID: id, Result: &result}
json.NewEncoder(conn).Encode(resp)
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Value string `json:"value,omitempty"`
}

View File

@@ -7,10 +7,20 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
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"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "network.getState":
handleGetState(conn, req, manager)
@@ -79,22 +89,32 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleCredentialsSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
func handleCredentialsSubmit(conn net.Conn, req Request, manager *Manager) {
token, ok := req.Params["token"].(string)
if !ok {
log.Warnf("handleCredentialsSubmit: missing or invalid token parameter")
models.RespondError(conn, req.ID, err.Error())
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
return
}
secrets, err := params.StringMap(req.Params, "secrets")
if err != nil {
secretsRaw, ok := req.Params["secrets"].(map[string]any)
if !ok {
log.Warnf("handleCredentialsSubmit: missing or invalid secrets parameter")
models.RespondError(conn, req.ID, err.Error())
models.RespondError(conn, req.ID, "missing or invalid 'secrets' parameter")
return
}
save := params.BoolOpt(req.Params, "save", true)
secrets := make(map[string]string)
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 {
log.Warnf("handleCredentialsSubmit: failed to submit credentials: %v", err)
@@ -103,13 +123,13 @@ func handleCredentialsSubmit(conn net.Conn, req models.Request, manager *Manager
}
log.Infof("handleCredentialsSubmit: credentials submitted successfully")
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials submitted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials submitted"})
}
func handleCredentialsCancel(conn net.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleCredentialsCancel(conn net.Conn, req Request, manager *Manager) {
token, ok := req.Params["token"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
return
}
@@ -118,15 +138,16 @@ func handleCredentialsCancel(conn net.Conn, req models.Request, manager *Manager
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "credentials cancelled"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "credentials cancelled"})
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleScanWiFi(conn net.Conn, req models.Request, manager *Manager) {
device := params.StringOpt(req.Params, "device", "")
func handleScanWiFi(conn net.Conn, req Request, manager *Manager) {
device, _ := req.Params["device"].(string)
var err error
if device != "" {
err = manager.ScanWiFiDevice(device)
@@ -137,25 +158,33 @@ func handleScanWiFi(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "scanning"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "scanning"})
}
func handleGetWiFiNetworks(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetWiFiNetworks())
func handleGetWiFiNetworks(conn net.Conn, req Request, manager *Manager) {
networks := manager.GetWiFiNetworks()
models.Respond(conn, req.ID, networks)
}
func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
ssid, err := params.String(req.Params, "ssid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
return
}
var connReq ConnectionRequest
connReq.SSID = ssid
connReq.Password = params.StringOpt(req.Params, "password", "")
connReq.Username = params.StringOpt(req.Params, "username", "")
connReq.Device = params.StringOpt(req.Params, "device", "")
if password, ok := req.Params["password"].(string); ok {
connReq.Password = password
}
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 {
connReq.Interactive = interactive
@@ -177,14 +206,27 @@ func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
}
}
connReq.AnonymousIdentity = params.StringOpt(req.Params, "anonymousIdentity", "")
connReq.DomainSuffixMatch = params.StringOpt(req.Params, "domainSuffixMatch", "")
connReq.EAPMethod = params.StringOpt(req.Params, "eapMethod", "")
connReq.Phase2Auth = params.StringOpt(req.Params, "phase2Auth", "")
connReq.CACertPath = params.StringOpt(req.Params, "caCertPath", "")
connReq.ClientCertPath = params.StringOpt(req.Params, "clientCertPath", "")
connReq.PrivateKeyPath = params.StringOpt(req.Params, "privateKeyPath", "")
if anonymousIdentity, ok := req.Params["anonymousIdentity"].(string); ok {
connReq.AnonymousIdentity = anonymousIdentity
}
if domainSuffixMatch, ok := req.Params["domainSuffixMatch"].(string); ok {
connReq.DomainSuffixMatch = domainSuffixMatch
}
if eapMethod, ok := req.Params["eapMethod"].(string); ok {
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 {
connReq.UseSystemCACerts = &useSystemCACerts
}
@@ -194,11 +236,11 @@ func handleConnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
}
func handleDisconnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
device := params.StringOpt(req.Params, "device", "")
func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) {
device, _ := req.Params["device"].(string)
var err error
if device != "" {
err = manager.DisconnectWiFiDevice(device)
@@ -209,13 +251,13 @@ func handleDisconnectWiFi(conn net.Conn, req models.Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"})
}
func handleForgetWiFi(conn net.Conn, req models.Request, manager *Manager) {
ssid, err := params.String(req.Params, "ssid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleForgetWiFi(conn net.Conn, req Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
return
}
@@ -224,10 +266,10 @@ func handleForgetWiFi(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "forgotten"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "forgotten"})
}
func handleToggleWiFi(conn net.Conn, req models.Request, manager *Manager) {
func handleToggleWiFi(conn net.Conn, req Request, manager *Manager) {
if err := manager.ToggleWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -237,7 +279,7 @@ func handleToggleWiFi(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": state.WiFiEnabled})
}
func handleEnableWiFi(conn net.Conn, req models.Request, manager *Manager) {
func handleEnableWiFi(conn net.Conn, req Request, manager *Manager) {
if err := manager.EnableWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -245,7 +287,7 @@ func handleEnableWiFi(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": true})
}
func handleDisableWiFi(conn net.Conn, req models.Request, manager *Manager) {
func handleDisableWiFi(conn net.Conn, req Request, manager *Manager) {
if err := manager.DisableWiFi(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -253,29 +295,29 @@ func handleDisableWiFi(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]bool{"enabled": false})
}
func handleConnectEthernetSpecificConfig(conn net.Conn, req models.Request, manager *Manager) {
uuid, err := params.String(req.Params, "uuid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleConnectEthernetSpecificConfig(conn net.Conn, req Request, manager *Manager) {
uuid, ok := req.Params["uuid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter")
return
}
if err := manager.activateConnection(uuid); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
}
func handleConnectEthernet(conn net.Conn, req models.Request, manager *Manager) {
func handleConnectEthernet(conn net.Conn, req Request, manager *Manager) {
if err := manager.ConnectEthernet(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
}
func handleDisconnectEthernet(conn net.Conn, req models.Request, manager *Manager) {
device := params.StringOpt(req.Params, "device", "")
func handleDisconnectEthernet(conn net.Conn, req Request, manager *Manager) {
device, _ := req.Params["device"].(string)
var err error
if device != "" {
err = manager.DisconnectEthernetDevice(device)
@@ -286,13 +328,13 @@ func handleDisconnectEthernet(conn net.Conn, req models.Request, manager *Manage
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"})
}
func handleSetPreference(conn net.Conn, req models.Request, manager *Manager) {
preference, err := params.String(req.Params, "preference")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetPreference(conn net.Conn, req Request, manager *Manager) {
preference, ok := req.Params["preference"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'preference' parameter")
return
}
@@ -304,10 +346,10 @@ func handleSetPreference(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]string{"preference": preference})
}
func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
ssid, err := params.String(req.Params, "ssid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleGetNetworkInfo(conn net.Conn, req Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
return
}
@@ -320,10 +362,10 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, network)
}
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
uuid, err := params.String(req.Params, "uuid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleGetWiredNetworkInfo(conn net.Conn, req Request, manager *Manager) {
uuid, ok := req.Params["uuid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'uuid' parameter")
return
}
@@ -336,7 +378,7 @@ func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manag
models.Respond(conn, req.ID, network)
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -366,7 +408,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleListVPNProfiles(conn net.Conn, req models.Request, manager *Manager) {
func handleListVPNProfiles(conn net.Conn, req Request, manager *Manager) {
profiles, err := manager.ListVPNProfiles()
if err != nil {
log.Warnf("handleListVPNProfiles: failed to list profiles: %v", err)
@@ -377,7 +419,7 @@ func handleListVPNProfiles(conn net.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, profiles)
}
func handleListActiveVPN(conn net.Conn, req models.Request, manager *Manager) {
func handleListActiveVPN(conn net.Conn, req Request, manager *Manager) {
active, err := manager.ListActiveVPN()
if err != nil {
log.Warnf("handleListActiveVPN: failed to list active VPNs: %v", err)
@@ -388,15 +430,27 @@ func handleListActiveVPN(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, active)
}
func handleConnectVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid")
func handleConnectVPN(conn net.Conn, req Request, manager *Manager) {
uuidOrName, ok := req.Params["uuidOrName"].(string)
if !ok {
log.Warnf("handleConnectVPN: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
return
name, nameOk := req.Params["name"].(string)
uuid, uuidOk := req.Params["uuid"].(string)
if nameOk {
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
}
}
singleActive := params.BoolOpt(req.Params, "singleActive", true)
// Default to true - only allow one VPN connection at a time
singleActive := true
if sa, ok := req.Params["singleActive"].(bool); ok {
singleActive = sa
}
if err := manager.ConnectVPN(uuidOrName, singleActive); err != nil {
log.Warnf("handleConnectVPN: failed to connect: %v", err)
@@ -404,15 +458,23 @@ func handleConnectVPN(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN connection initiated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN connection initiated"})
}
func handleDisconnectVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := params.StringAlt(req.Params, "uuidOrName", "name", "uuid")
func handleDisconnectVPN(conn net.Conn, req Request, manager *Manager) {
uuidOrName, ok := req.Params["uuidOrName"].(string)
if !ok {
log.Warnf("handleDisconnectVPN: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing 'uuidOrName', 'name', or 'uuid' parameter")
return
name, nameOk := req.Params["name"].(string)
uuid, uuidOk := req.Params["uuid"].(string)
if nameOk {
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 {
@@ -421,21 +483,27 @@ func handleDisconnectVPN(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN disconnected"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN disconnected"})
}
func handleDisconnectAllVPN(conn net.Conn, req models.Request, manager *Manager) {
func handleDisconnectAllVPN(conn net.Conn, req Request, manager *Manager) {
if err := manager.DisconnectAllVPN(); err != nil {
log.Warnf("handleDisconnectAllVPN: failed: %v", err)
models.RespondError(conn, req.ID, fmt.Sprintf("failed to disconnect all VPNs: %v", err))
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "All VPNs disconnected"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "All VPNs disconnected"})
}
func handleClearVPNCredentials(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
func handleClearVPNCredentials(conn net.Conn, req Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string)
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok {
log.Warnf("handleClearVPNCredentials: missing uuidOrName/name/uuid parameter")
models.RespondError(conn, req.ID, "missing uuidOrName/name/uuid parameter")
@@ -448,19 +516,19 @@ func handleClearVPNCredentials(conn net.Conn, req models.Request, manager *Manag
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials cleared"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials cleared"})
}
func handleSetWiFiAutoconnect(conn net.Conn, req models.Request, manager *Manager) {
ssid, err := params.String(req.Params, "ssid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetWiFiAutoconnect(conn net.Conn, req Request, manager *Manager) {
ssid, ok := req.Params["ssid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'ssid' parameter")
return
}
autoconnect, err := params.Bool(req.Params, "autoconnect")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
autoconnect, ok := req.Params["autoconnect"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'autoconnect' parameter")
return
}
@@ -469,10 +537,10 @@ func handleSetWiFiAutoconnect(conn net.Conn, req models.Request, manager *Manage
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "autoconnect updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "autoconnect updated"})
}
func handleListVPNPlugins(conn net.Conn, req models.Request, manager *Manager) {
func handleListVPNPlugins(conn net.Conn, req Request, manager *Manager) {
plugins, err := manager.ListVPNPlugins()
if err != nil {
log.Warnf("handleListVPNPlugins: failed to list plugins: %v", err)
@@ -483,14 +551,17 @@ func handleListVPNPlugins(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, plugins)
}
func handleImportVPN(conn net.Conn, req models.Request, manager *Manager) {
filePath, ok := params.StringAlt(req.Params, "file", "path")
func handleImportVPN(conn net.Conn, req Request, manager *Manager) {
filePath, ok := req.Params["file"].(string)
if !ok {
filePath, ok = req.Params["path"].(string)
}
if !ok {
models.RespondError(conn, req.ID, "missing 'file' or 'path' parameter")
return
}
name := params.StringOpt(req.Params, "name", "")
name, _ := req.Params["name"].(string)
result, err := manager.ImportVPN(filePath, name)
if err != nil {
@@ -502,8 +573,14 @@ func handleImportVPN(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, result)
}
func handleGetVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
func handleGetVPNConfig(conn net.Conn, req Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string)
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok {
models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter")
return
@@ -519,10 +596,10 @@ func handleGetVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, config)
}
func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager) {
connUUID, err := params.String(req.Params, "uuid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleUpdateVPNConfig(conn net.Conn, req Request, manager *Manager) {
connUUID, ok := req.Params["uuid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing 'uuid' parameter")
return
}
@@ -549,11 +626,17 @@ func handleUpdateVPNConfig(conn net.Conn, req models.Request, manager *Manager)
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN config updated"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN config updated"})
}
func handleDeleteVPN(conn net.Conn, req models.Request, manager *Manager) {
uuidOrName, ok := params.StringAlt(req.Params, "uuid", "name", "uuidOrName")
func handleDeleteVPN(conn net.Conn, req Request, manager *Manager) {
uuidOrName, ok := req.Params["uuid"].(string)
if !ok {
uuidOrName, ok = req.Params["name"].(string)
}
if !ok {
uuidOrName, ok = req.Params["uuidOrName"].(string)
}
if !ok {
models.RespondError(conn, req.ID, "missing 'uuid', 'name', or 'uuidOrName' parameter")
return
@@ -565,19 +648,23 @@ func handleDeleteVPN(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN deleted"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN deleted"})
}
func handleSetVPNCredentials(conn net.Conn, req models.Request, manager *Manager) {
connUUID, err := params.String(req.Params, "uuid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetVPNCredentials(conn net.Conn, req Request, manager *Manager) {
connUUID, ok := req.Params["uuid"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing 'uuid' parameter")
return
}
username := params.StringOpt(req.Params, "username", "")
password := params.StringOpt(req.Params, "password", "")
save := params.BoolOpt(req.Params, "save", true)
username, _ := req.Params["username"].(string)
password, _ := req.Params["password"].(string)
save := true
if saveParam, ok := req.Params["save"].(bool); ok {
save = saveParam
}
if err := manager.SetVPNCredentials(connUUID, username, password, save); err != nil {
log.Warnf("handleSetVPNCredentials: failed to set credentials: %v", err)
@@ -585,5 +672,5 @@ func handleSetVPNCredentials(conn net.Conn, req models.Request, manager *Manager
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "VPN credentials set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "VPN credentials set"})
}

View File

@@ -53,10 +53,10 @@ func TestRespondError_Network(t *testing.T) {
func TestRespond_Network(t *testing.T) {
conn := newMockNetConn()
result := models.SuccessResult{Success: true, Message: "test"}
result := SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result)
var resp models.Response[models.SuccessResult]
var resp models.Response[SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -77,7 +77,7 @@ func TestHandleGetState(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "network.getState"}
req := Request{ID: 123, Method: "network.getState"}
handleGetState(conn, req, manager)
@@ -103,7 +103,7 @@ func TestHandleGetWiFiNetworks(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "network.wifi.networks"}
req := Request{ID: 123, Method: "network.wifi.networks"}
handleGetWiFiNetworks(conn, req, manager)
@@ -125,7 +125,7 @@ func TestHandleConnectWiFi(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "network.wifi.connect",
Params: map[string]any{},
@@ -149,7 +149,7 @@ func TestHandleSetPreference(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "network.preference.set",
Params: map[string]any{},
@@ -173,7 +173,7 @@ func TestHandleGetNetworkInfo(t *testing.T) {
}
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "network.info",
Params: map[string]any{},
@@ -199,7 +199,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "network.unknown",
}
@@ -216,7 +216,7 @@ func TestHandleRequest(t *testing.T) {
t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn()
req := models.Request{
req := Request{
ID: 123,
Method: "network.getState",
}

View File

@@ -1,113 +0,0 @@
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
}

View File

@@ -1,154 +0,0 @@
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)
}

View File

@@ -27,7 +27,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "network manager not initialized")
return
}
network.HandleRequest(conn, req, networkManager)
netReq := network.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
network.HandleRequest(conn, netReq, networkManager)
return
}
@@ -41,7 +46,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "loginctl manager not initialized")
return
}
loginctl.HandleRequest(conn, req, loginctlManager)
loginReq := loginctl.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
loginctl.HandleRequest(conn, loginReq, loginctlManager)
return
}
@@ -50,7 +60,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "freedesktop manager not initialized")
return
}
freedesktop.HandleRequest(conn, req, freedesktopManager)
freedeskReq := freedesktop.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
freedesktop.HandleRequest(conn, freedeskReq, freedesktopManager)
return
}
@@ -59,7 +74,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "wayland manager not initialized")
return
}
wayland.HandleRequest(conn, req, waylandManager)
waylandReq := wayland.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
wayland.HandleRequest(conn, waylandReq, waylandManager)
return
}
@@ -68,7 +88,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "bluetooth manager not initialized")
return
}
bluez.HandleRequest(conn, req, bluezManager)
bluezReq := bluez.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
bluez.HandleRequest(conn, bluezReq, bluezManager)
return
}
@@ -77,7 +102,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "apppicker manager not initialized")
return
}
apppicker.HandleRequest(conn, req, appPickerManager)
appPickerReq := apppicker.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
apppicker.HandleRequest(conn, appPickerReq, appPickerManager)
return
}
@@ -86,7 +116,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "CUPS manager not initialized")
return
}
cups.HandleRequest(conn, req, cupsManager)
cupsReq := cups.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
cups.HandleRequest(conn, cupsReq, cupsManager)
return
}
@@ -95,7 +130,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "dwl manager not initialized")
return
}
dwl.HandleRequest(conn, req, dwlManager)
dwlReq := dwl.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
dwl.HandleRequest(conn, dwlReq, dwlManager)
return
}
@@ -104,7 +144,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "brightness manager not initialized")
return
}
brightness.HandleRequest(conn, req, brightnessManager)
brightnessReq := brightness.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
brightness.HandleRequest(conn, brightnessReq, brightnessManager)
return
}
@@ -125,7 +170,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
}
extworkspace.HandleRequest(conn, req, extWorkspaceManager)
extWorkspaceReq := extworkspace.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
extworkspace.HandleRequest(conn, extWorkspaceReq, extWorkspaceManager)
return
}
@@ -134,7 +184,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "wlroutput manager not initialized")
return
}
wlroutput.HandleRequest(conn, req, wlrOutputManager)
wlrOutputReq := wlroutput.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
wlroutput.HandleRequest(conn, wlrOutputReq, wlrOutputManager)
return
}
@@ -143,7 +198,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, "evdev manager not initialized")
return
}
evdev.HandleRequest(conn, req, evdevManager)
evdevReq := evdev.Request{
ID: req.ID,
Method: req.Method,
Params: req.Params,
}
evdev.HandleRequest(conn, evdevReq, evdevManager)
return
}

View File

@@ -2,6 +2,8 @@ package wayland
import (
"math"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type GammaRamp struct {
@@ -10,126 +12,6 @@ type GammaRamp struct {
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 {
ramp := GammaRamp{
Red: make([]uint16, size),
@@ -137,13 +19,16 @@ func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp {
Blue: make([]uint16, size),
}
wp := calcWhitepoint(temp)
for i := uint32(0); i < size; i++ {
val := float64(i) / float64(size-1)
ramp.Red[i] = uint16(clamp01(math.Pow(val*wp.r, 1.0/gamma)) * 65535.0)
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)
valGamma := math.Pow(val, 1.0/gamma)
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
@@ -165,3 +50,39 @@ func GenerateIdentityRamp(size uint32) GammaRamp {
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
}

View File

@@ -54,7 +54,7 @@ func TestGenerateGammaRamp(t *testing.T) {
}
}
func TestCalcWhitepoint(t *testing.T) {
func TestTemperatureToRGB(t *testing.T) {
tests := []struct {
name string
temp int
@@ -67,32 +67,32 @@ func TestCalcWhitepoint(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wp := calcWhitepoint(tt.temp)
r, g, b := temperatureToRGB(tt.temp)
if wp.r < 0 || wp.r > 1 {
t.Errorf("red out of range: %f", wp.r)
if r < 0 || r > 1 {
t.Errorf("red out of range: %f", r)
}
if wp.g < 0 || wp.g > 1 {
t.Errorf("green out of range: %f", wp.g)
if g < 0 || g > 1 {
t.Errorf("green out of range: %f", g)
}
if wp.b < 0 || wp.b > 1 {
t.Errorf("blue out of range: %f", wp.b)
if b < 0 || b > 1 {
t.Errorf("blue out of range: %f", b)
}
})
}
}
func TestWhitepointProgression(t *testing.T) {
func TestTemperatureProgression(t *testing.T) {
temps := []int{3000, 4000, 5000, 6000, 6500}
var prevBlue float64
for i, temp := range temps {
wp := calcWhitepoint(temp)
if i > 0 && wp.b < prevBlue {
_, _, b := temperatureToRGB(temp)
if i > 0 && b < prevBlue {
t.Errorf("blue should increase with temperature, %d->%d: %f->%f",
temps[i-1], temp, prevBlue, wp.b)
temps[i-1], temp, prevBlue, b)
}
prevBlue = wp.b
prevBlue = b
}
}

View File

@@ -7,10 +7,20 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
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"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "wayland manager not initialized")
return
@@ -38,27 +48,26 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
func handleSetTemperature(conn net.Conn, req Request, manager *Manager) {
var lowTemp, highTemp int
if temp, ok := req.Params["temp"].(float64); ok {
lowTemp = int(temp)
highTemp = int(temp)
} else {
low, err := params.Float(req.Params, "low")
if err != nil {
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
return
}
high, err := params.Float(req.Params, "high")
if err != nil {
low, okLow := req.Params["low"].(float64)
high, okHigh := req.Params["high"].(float64)
if !okLow || !okHigh {
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
return
}
lowTemp = int(low)
highTemp = int(high)
}
@@ -68,19 +77,19 @@ func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "temperature set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "temperature set"})
}
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
lat, err := params.Float(req.Params, "latitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
lat, ok := req.Params["latitude"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'latitude' parameter")
return
}
lon, err := params.Float(req.Params, "longitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
lon, ok := req.Params["longitude"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'longitude' parameter")
return
}
@@ -89,30 +98,30 @@ func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"})
}
func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
func handleSetManualTimes(conn net.Conn, req Request, manager *Manager) {
sunriseParam := req.Params["sunrise"]
sunsetParam := req.Params["sunset"]
if sunriseParam == nil || sunsetParam == nil {
manager.ClearManualTimes()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"})
return
}
sunriseStr, ok := sunriseParam.(string)
if !ok || sunriseStr == "" {
manager.ClearManualTimes()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"})
return
}
sunsetStr, ok := sunsetParam.(string)
if !ok || sunsetStr == "" {
manager.ClearManualTimes()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"})
return
}
@@ -133,24 +142,24 @@ func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times set"})
}
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
use, err := params.Bool(req.Params, "use")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetUseIPLocation(conn net.Conn, req Request, manager *Manager) {
use, ok := req.Params["use"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'use' parameter")
return
}
manager.SetUseIPLocation(use)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "IP location preference set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "IP location preference set"})
}
func handleSetGamma(conn net.Conn, req models.Request, manager *Manager) {
gamma, err := params.Float(req.Params, "gamma")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetGamma(conn net.Conn, req Request, manager *Manager) {
gamma, ok := req.Params["gamma"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'gamma' parameter")
return
}
@@ -159,21 +168,21 @@ func handleSetGamma(conn net.Conn, req models.Request, manager *Manager) {
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "gamma set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "gamma set"})
}
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
func handleSetEnabled(conn net.Conn, req Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter")
return
}
manager.SetEnabled(enabled)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "enabled state set"})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "enabled state set"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)

File diff suppressed because it is too large Load Diff

View File

@@ -6,117 +6,81 @@ import (
)
const (
degToRad = math.Pi / 180.0
radToDeg = 180.0 / math.Pi
degToRad = math.Pi / 180.0
radToDeg = 180.0 / math.Pi
solarNoon = 12.0
sunriseAngle = -0.833
)
type SunCondition int
const (
SunNormal SunCondition = iota
SunMidnightSun
SunPolarNight
)
type SunTimes struct {
Dawn time.Time
Sunrise time.Time
Sunset time.Time
Night time.Time
}
func daysInYear(year int) int {
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
return 366
}
return 365
}
func dateOrbitAngle(t time.Time) float64 {
return 2 * math.Pi / float64(daysInYear(t.Year())) * float64(t.YearDay()-1)
}
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
elevTwilightRad := (90.833 - elevTwilight) * degToRad
elevDaylightRad := (90.833 - elevDaylight) * degToRad
utc := date.UTC()
orbitAngle := dateOrbitAngle(utc)
decl := sunDeclination(orbitAngle)
eqtime := equationOfTime(orbitAngle)
haTwilight := sunHourAngle(latRad, decl, elevTwilightRad)
haDaylight := sunHourAngle(latRad, decl, elevDaylightRad)
if math.IsNaN(haTwilight) || math.IsNaN(haDaylight) {
cond := sunCondition(latRad, decl)
return SunTimes{}, cond
}
dayStart := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC)
lonOffset := time.Duration(-lon*4) * time.Minute
dawnSecs := hourAngleToSeconds(math.Abs(haTwilight), eqtime)
sunriseSecs := hourAngleToSeconds(math.Abs(haDaylight), eqtime)
sunsetSecs := hourAngleToSeconds(-math.Abs(haDaylight), eqtime)
nightSecs := hourAngleToSeconds(-math.Abs(haTwilight), eqtime)
return SunTimes{
Dawn: dayStart.Add(time.Duration(dawnSecs)*time.Second + lonOffset).In(date.Location()),
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 CalculateSunTimes(lat, lon float64, date time.Time) SunTimes {
times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
switch cond {
case SunMidnightSun:
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dayEnd := dayStart.Add(24*time.Hour - time.Second)
return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayEnd, Night: dayEnd}
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}
utcDate := date.UTC()
year, month, day := utcDate.Date()
loc := date.Location()
dayOfYear := utcDate.YearDay()
gamma := 2 * math.Pi / 365 * float64(dayOfYear-1)
eqTime := 229.18 * (0.000075 +
0.001868*math.Cos(gamma) -
0.032077*math.Sin(gamma) -
0.014615*math.Cos(2*gamma) -
0.040849*math.Sin(2*gamma))
decl := 0.006918 -
0.399912*math.Cos(gamma) +
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)
latRad := lat * degToRad
cosHourAngle := (math.Sin(sunriseAngle*degToRad) -
math.Sin(latRad)*math.Sin(decl)) /
(math.Cos(latRad) * math.Cos(decl))
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, 0, 0, 0, 0, time.UTC).In(loc),
}
}
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
sunriseTime := solarNoon - hourAngle/15.0 - lon/15.0 - eqTime/60.0
sunsetTime := solarNoon + hourAngle/15.0 - lon/15.0 - eqTime/60.0
sunrise := timeOfDayToTime(sunriseTime, year, month, day, time.UTC).In(loc)
sunset := timeOfDayToTime(sunsetTime, year, month, day, time.UTC).In(loc)
return SunTimes{
Sunrise: sunrise,
Sunset: sunset,
}
return times
}
func timeOfDayToTime(hours float64, year int, month time.Month, day int, loc *time.Location) time.Time {
h := int(hours)
m := int((hours - float64(h)) * 60)
s := int(((hours-float64(h))*60 - float64(m)) * 60)
if h < 0 {
h += 24
day--
}
if h >= 24 {
h -= 24
day++
}
return time.Date(year, month, day, h, m, s, 0, loc)
}

View File

@@ -340,47 +340,38 @@ func TestCalculateNextTransition(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) {
func TestTimeOfDayToTime(t *testing.T) {
tests := []struct {
name string
lat float64
date time.Time
expected SunCondition
hours float64
expected time.Time
}{
{
name: "normal_conditions",
lat: 40.0,
date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.UTC),
expected: SunNormal,
name: "noon",
hours: 12.0,
expected: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local),
},
{
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 {
t.Run(tt.name, func(t *testing.T) {
_, cond := CalculateSunTimesWithTwilight(tt.lat, 0, tt.date, -6.0, 3.0)
if cond != tt.expected {
t.Errorf("expected condition %v, got %v", tt.expected, cond)
result := timeOfDayToTime(tt.hours, 2024, 6, 21, time.Local)
if result.Hour() != tt.expected.Hour() {
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())
}
})
}

View File

@@ -11,28 +11,18 @@ import (
"github.com/godbus/dbus/v5"
)
type GammaState int
const (
StateNormal GammaState = iota
StateTransition
StateStatic
)
type Config struct {
Outputs []string
LowTemp int
HighTemp int
Latitude *float64
Longitude *float64
UseIPLocation bool
ManualSunrise *time.Time
ManualSunset *time.Time
ManualDuration *time.Duration
Gamma float64
Enabled bool
ElevationTwilight float64
ElevationDaylight float64
Outputs []string
LowTemp int
HighTemp int
Latitude *float64
Longitude *float64
UseIPLocation bool
ManualSunrise *time.Time
ManualSunset *time.Time
ManualDuration *time.Duration
Gamma float64
Enabled bool
}
type State struct {
@@ -41,24 +31,13 @@ type State struct {
NextTransition time.Time `json:"nextTransition"`
SunriseTime time.Time `json:"sunriseTime"`
SunsetTime time.Time `json:"sunsetTime"`
DawnTime time.Time `json:"dawnTime"`
NightTime time.Time `json:"nightTime"`
IsDay bool `json:"isDay"`
SunPosition float64 `json:"sunPosition"`
}
type cmd struct {
fn func()
}
type sunSchedule struct {
times SunTimes
condition SunCondition
dawnStepTime time.Duration
nightStepTime time.Duration
calcDay time.Time
}
type Manager struct {
config Config
configMutex sync.RWMutex
@@ -81,9 +60,10 @@ type Manager struct {
updateTrigger chan struct{}
wg sync.WaitGroup
schedule sunSchedule
scheduleMutex sync.RWMutex
gammaState GammaState
currentTemp int
targetTemp int
transitionMutex sync.RWMutex
transitionChan chan int
cachedIPLat *float64
cachedIPLon *float64
@@ -100,6 +80,7 @@ type Manager struct {
type outputState struct {
id uint32
name string
registryName uint32
output *wlclient.Output
gammaControl any
@@ -110,15 +91,18 @@ type outputState struct {
lastFailTime time.Time
}
type SunTimes struct {
Sunrise time.Time
Sunset time.Time
}
func DefaultConfig() Config {
return Config{
Outputs: []string{},
LowTemp: 4000,
HighTemp: 6500,
Gamma: 1.0,
Enabled: false,
ElevationTwilight: -6.0,
ElevationDaylight: 3.0,
Outputs: []string{},
LowTemp: 4000,
HighTemp: 6500,
Gamma: 1.0,
Enabled: false,
}
}
@@ -156,7 +140,8 @@ func (m *Manager) GetState() State {
if m.state == nil {
return State{}
}
return *m.state
stateCopy := *m.state
return stateCopy
}
func (m *Manager) Subscribe(id string) chan State {
@@ -200,8 +185,5 @@ func stateChanged(old, new *State) bool {
if old.Config.Enabled != new.Config.Enabled {
return true
}
if old.SunPosition != new.SunPosition {
return true
}
return false
}

View File

@@ -11,6 +11,17 @@ import (
"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 {
Name string `json:"name"`
Enabled bool `json:"enabled"`
@@ -31,7 +42,7 @@ type ConfigurationRequest struct {
Test bool `json:"test"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "wlroutput manager not initialized")
return
@@ -51,11 +62,12 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
func handleApplyConfiguration(conn net.Conn, req Request, manager *Manager, test bool) {
headsParam, ok := req.Params["heads"]
if !ok {
models.RespondError(conn, req.ID, "missing 'heads' parameter")
@@ -83,10 +95,10 @@ func handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manage
if test {
msg = "configuration test succeeded"
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: msg})
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: msg})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)

View File

@@ -4,8 +4,6 @@ import (
"fmt"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@@ -120,59 +118,6 @@ func (m Model) viewInstallingPackages() 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 {
var b strings.Builder
@@ -187,6 +132,7 @@ func (m Model) viewInstallComplete() string {
b.WriteString(success)
b.WriteString("\n\n")
// Show what was accomplished
accomplishments := []string{
"• Window manager and dependencies installed",
"• Terminal and development tools configured",
@@ -200,26 +146,8 @@ func (m Model) viewInstallComplete() string {
}
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\"")
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.")
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 != "" {
b.WriteString("\n\n")

View File

@@ -40,7 +40,7 @@ func (m Model) viewWelcome() string {
subtitle := lipgloss.NewStyle().
Foreground(lipgloss.Color(theme.Subtle)).
Italic(true).
Render("Quickstart for a Dank Desktop")
Render("Quickstart for a Dank Desktop")
b.WriteString(decorator)
b.WriteString("\n")

View File

@@ -13,23 +13,8 @@ const (
FormatXRGB8888 PixelFormat = 1
FormatABGR8888 PixelFormat = 0x34324241
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 {
fd int
data []byte
@@ -93,52 +78,6 @@ func (b *Buffer) Close() error {
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) {
if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
return
@@ -149,12 +88,7 @@ func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
return
}
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
}
return b.data[off+2], b.data[off+1], b.data[off], b.data[off+3]
}
func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) {
@@ -203,96 +137,3 @@ func (b *Buffer) Clear() {
func (b *Buffer) CopyFrom(src *Buffer) {
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
}
}

View File

@@ -1,6 +1,12 @@
dms-git (0.6.2+git2419.993f14a3) nightly; urgency=medium
dms-git (0.6.2+git2264.c5c5ce84) nightly; urgency=medium
* widgets: make dank icon picker a popup
* Previous updates included in build
* Add VERSION file creation to all distro packages
* Fix Fedora COPR to pass VERSION/COMMIT to make dist
* 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> Mon, 09 Dec 2025 14:00:00 +0000
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 03 Dec 2025 01:50:00 +0000

View File

@@ -17,9 +17,6 @@ export GOTOOLCHAIN := local
%:
dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build:
# Create Go cache directories
mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE)

View File

@@ -7,9 +7,6 @@ DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
%:
dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build:
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
if [ -f dms-distropkg-amd64.gz ]; then \

View File

@@ -26,7 +26,7 @@ BuildRequires: systemd-rpm-macros
# Core requirements
Requires: (quickshell-git or quickshell)
Requires: accountsservice
Requires: dms-cli = %{epoch}:%{version}-%{release}
Requires: dms-cli
Requires: dgop
# Core utilities (Highly recommended for DMS functionality)
@@ -60,9 +60,41 @@ URL: https://github.com/AvengeMedia/DankMaterialShell
Command-line interface for DankMaterialShell configuration and management.
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
{{{ 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 DMS CLI from source (core/subdirectory)
VERSION="%{version}"
@@ -96,6 +128,9 @@ 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 fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
# Install dgop binary
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Install systemd user service
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
@@ -115,8 +150,11 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
# Restart DMS for active users after upgrade
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
@@ -133,5 +171,8 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog
{{{ git_repo_changelog }}}

View File

@@ -10,14 +10,13 @@
user = config.services.greetd.settings.default_session.user;
cacheDir = "/var/lib/dms-greeter";
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
export PATH=$PATH:${lib.makeBinPath [cfg.quickshell.package config.programs.${cfg.compositor.name}.package]}
${lib.escapeShellArgs ([
"sh"
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
"--cache-dir"
cacheDir
"/var/lib/dmsgreeter"
"--command"
cfg.compositor.name
"-p"
@@ -28,8 +27,6 @@
"${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}"
])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
'';
jq = lib.getExe pkgs.jq;
in {
imports = let
msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead.";
@@ -96,17 +93,17 @@ in {
material-symbols
];
systemd.tmpfiles.settings."10-dmsgreeter" = {
${cacheDir}.d = {
"/var/lib/dmsgreeter".d = {
user = user;
group =
if config.users.users.${user}.group != ""
then config.users.users.${user}.group
else "greeter";
mode = "0750";
mode = "0755";
};
};
systemd.services.greetd.preStart = ''
cd ${cacheDir}
cd /var/lib/dmsgreeter
${lib.concatStringsSep "\n" (lib.map (f: ''
if [ -f "${f}" ]; then
cp "${f}" .
@@ -115,16 +112,9 @@ in {
cfg.configFiles)}
if [ -f session.json ]; then
if cp "$(${jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then
if cp "$(${lib.getExe pkgs.jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then
mv session.json session.orig.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
${lib.getExe pkgs.jq} '.wallpaperPath = "/var/lib/dmsgreeter/wallpaper.jpg"' session.orig.json > session.json
fi
fi

View File

@@ -110,8 +110,10 @@ if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
fi
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE

View File

@@ -80,8 +80,10 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE

View File

@@ -1,6 +1,8 @@
dms-git (0.6.2+git2419.993f14a3) questing; urgency=medium
dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
* widgets: make dank icon picker a popup
* Previous updates included in build
* Add VERSION file creation to all distro packages
* Fix Fedora COPR to pass VERSION/COMMIT to make dist
* 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> Mon, 09 Dec 2025 14:00:00 +0000
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 03 Dec 2025 01:50:00 +0000

View File

@@ -17,9 +17,6 @@ export GOTOOLCHAIN := local
%:
dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build:
# Create Go cache directories (sbuild sets HOME to non-existent path)
mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE)

View File

@@ -13,9 +13,6 @@ BASE_VERSION := $(shell echo $(UPSTREAM_VERSION) | sed 's/ppa[0-9]*$$//' | sed '
%:
dh $@
override_dh_installsystemd:
dh_installsystemd --name=dms
override_dh_auto_build:
# All files are included in source package (downloaded by build-source.sh)
# Launchpad build environment has no internet access

View File

@@ -99,9 +99,6 @@
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--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 \
--bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \

View File

@@ -1 +0,0 @@
The Dark Knight

View File

@@ -39,12 +39,11 @@ const KEY_MAP = {
16777329: "XF86AudioMute",
16842808: "XF86AudioMicMute",
16777344: "XF86AudioPlay",
16777345: "XF86AudioStop",
16777346: "XF86AudioPrev",
16777345: "XF86AudioPause",
16777346: "XF86AudioStop",
16777347: "XF86AudioNext",
16777348: "XF86AudioPause",
16777349: "XF86AudioMedia",
16777350: "XF86AudioRecord",
16777348: "XF86AudioPrev",
16842792: "XF86AudioRecord",
16842798: "XF86MonBrightnessUp",
16842797: "XF86MonBrightnessDown",
16842800: "XF86KbdBrightnessUp",
@@ -80,49 +79,12 @@ const KEY_MAP = {
124: "Backslash",
95: "Minus",
43: "Equal",
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"
126: "grave"
};
function xkbKeyFromQtKey(qk) {
if (qk >= 65 && qk <= 90)
return String.fromCharCode(qk);
if (qk >= 97 && qk <= 122)
return String.fromCharCode(qk - 32);
if (qk >= 48 && qk <= 57)
return String.fromCharCode(qk);
if (qk >= 16777264 && qk <= 16777298)
@@ -132,10 +94,16 @@ function xkbKeyFromQtKey(qk) {
function modsFromEvent(mods) {
var result = [];
if (mods & 0x10000000)
result.push("Super");
if (mods & 0x08000000)
result.push("Alt");
var hasAlt = mods & 0x08000000;
var hasSuper = mods & 0x10000000;
if (hasAlt && hasSuper) {
result.push("Mod");
} else {
if (hasSuper)
result.push("Super");
if (hasAlt)
result.push("Alt");
}
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)

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 enable", label: "Idle Inhibit: Enable" },
{ id: "spawn dms ipc call inhibit disable", label: "Idle Inhibit: Disable" },
{ id: "spawn dms ipc call audio increment 5", label: "Volume Up" },
{ id: "spawn dms ipc call audio increment", label: "Volume Up" },
{ 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 10", label: "Volume Up (10%)" },
{ id: "spawn dms ipc call audio decrement 5", label: "Volume Down" },
{ id: "spawn dms ipc call audio decrement", label: "Volume Down" },
{ 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 10", label: "Volume Down (10%)" },
{ 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 cycleoutput", label: "Audio Output: Cycle" },
{ 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 5 \"\"", label: "Brightness Up (5%)" },
{ id: "spawn dms ipc call brightness increment 10 \"\"", label: "Brightness Up (10%)" },
{ 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 5 \"\"", label: "Brightness Down (5%)" },
{ 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 increment", label: "Brightness Up" },
{ 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 10", label: "Brightness Up (10%)" },
{ id: "spawn dms ipc call brightness decrement", label: "Brightness Down" },
{ 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 10", label: "Brightness Down (10%)" },
{ 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 light", label: "Theme: Light Mode" },
{ id: "spawn dms ipc call theme dark", label: "Theme: Dark Mode" },
@@ -223,37 +223,19 @@ const ACTION_ARGS = {
const DMS_ACTION_ARGS = {
"audio increment": {
base: "spawn dms ipc call audio increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"audio decrement": {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [
{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" },
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness decrement": {
base: "spawn dms ipc call brightness decrement",
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: "" }
]
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
}
};
@@ -261,10 +243,6 @@ function getActionTypes() {
return ACTION_TYPES;
}
function getDmsActionArgs() {
return DMS_ACTION_ARGS;
}
function getDmsActions(isNiri, isHyprland) {
const result = [];
for (let i = 0; i < DMS_ACTIONS.length; i++) {
@@ -517,48 +495,10 @@ function parseDmsActionArgs(action) {
for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key];
if (!action.startsWith(config.base))
continue;
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 (action.startsWith(config.base)) {
var rest = action.slice(config.base.length).trim();
return { base: key, args: { amount: rest || "" } };
}
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: {} };
@@ -569,24 +509,11 @@ function buildDmsAction(baseKey, args) {
if (!config)
return "";
var parts = [config.base];
var action = config.base;
if (args && args.amount)
action += " " + args.amount;
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(" ");
return action;
}
function getScreenshotOptions() {

View File

@@ -53,7 +53,7 @@ Singleton {
if (appId === "home assistant desktop")
return "homeassistant-desktop";
if (appId.includes("com.transmissionbt.transmission"))
return "transmission";
return "transmission-gtk";
return appId;
}

View File

@@ -255,6 +255,7 @@ Singleton {
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
property string batteryProfileName: ""
property bool lockBeforeSuspend: false
property bool preventIdleForMedia: false
property bool loginctlLockIntegration: true
property bool fadeToLockEnabled: false
property int fadeToLockGracePeriod: 5

View File

@@ -128,13 +128,21 @@ Singleton {
setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
}
}
} else if (currentTheme !== "custom") {
const darkTheme = StockThemes.getThemeByName(currentTheme, false);
const lightTheme = StockThemes.getThemeByName(currentTheme, true);
if (darkTheme && darkTheme.primary) {
const stockColors = buildMatugenColorsFromTheme(darkTheme, lightTheme);
const themeData = isLight ? lightTheme : darkTheme;
setDesiredTheme("hex", themeData.primary, isLight, iconTheme, themeData.matugen_type, stockColors);
} else {
let primaryColor;
let matugenType;
if (currentTheme === "custom") {
if (customThemeData && customThemeData.primary) {
primaryColor = customThemeData.primary;
matugenType = customThemeData.matugen_type;
}
} else {
primaryColor = currentThemeData.primary;
matugenType = currentThemeData.matugen_type;
}
if (primaryColor) {
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType);
}
}
}, 0);

View File

@@ -154,6 +154,7 @@ var SPEC = {
batterySuspendBehavior: { def: 0 },
batteryProfileName: { def: "" },
lockBeforeSuspend: { def: false },
preventIdleForMedia: { def: false },
loginctlLockIntegration: { def: true },
fadeToLockEnabled: { def: false },
fadeToLockGracePeriod: { def: 5 },

View File

@@ -108,14 +108,13 @@ Item {
id: barRepeaterModel
values: {
const configs = SettingsData.barConfigs;
return configs.map(c => ({
id: c.id,
position: c.position
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
return aVertical - bVertical;
});
return configs
.map(c => ({ id: c.id, position: c.position }))
.sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
return aVertical - bVertical;
});
}
}
@@ -143,34 +142,9 @@ 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 {
id: dockLoader
active: root.dockEnabled
active: true
asynchronous: false
property var currentPosition: SettingsData.dockPosition
@@ -466,71 +440,62 @@ Item {
title: I18n.tr("Open with...")
function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'";
return "'" + str.replace(/'/g, "'\\''") + "'"
}
onApplicationSelected: (app, filePath) => {
if (!app)
return;
let cmd = app.exec || "";
const escapedPath = shellEscape(filePath);
const escapedUri = shellEscape("file://" + filePath);
if (!app) return
let hasField = false;
if (cmd.includes("%f")) {
cmd = cmd.replace("%f", escapedPath);
hasField = true;
} else if (cmd.includes("%F")) {
cmd = cmd.replace("%F", escapedPath);
hasField = true;
} else if (cmd.includes("%u")) {
cmd = cmd.replace("%u", escapedUri);
hasField = true;
} else if (cmd.includes("%U")) {
cmd = cmd.replace("%U", escapedUri);
hasField = true;
}
let cmd = app.exec || ""
const escapedPath = shellEscape(filePath)
const escapedUri = shellEscape("file://" + filePath)
cmd = cmd.replace(/%[ikc]/g, "");
let hasField = false
if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedPath); hasField = true }
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedPath); hasField = true }
else if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUri); hasField = true }
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUri); hasField = true }
cmd = cmd.replace(/%[ikc]/g, "")
if (!hasField) {
cmd += " " + escapedPath;
cmd += " " + escapedPath
}
console.log("FilePicker: Launching", cmd);
console.log("FilePicker: Launching", cmd)
Quickshell.execDetached({
command: ["sh", "-c", cmd]
});
})
}
}
Connections {
target: DMSService
function onOpenUrlRequested(url) {
browserPickerModal.url = url;
browserPickerModal.open();
browserPickerModal.url = url
browserPickerModal.open()
}
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) {
console.warn("DMSShell: Invalid app picker request data");
return;
console.warn("DMSShell: Invalid app picker request data")
return
}
filePickerModal.targetData = data.target;
filePickerModal.targetDataLabel = data.requestType || "file";
filePickerModal.targetData = data.target
filePickerModal.targetDataLabel = data.requestType || "file"
if (data.categories && data.categories.length > 0) {
filePickerModal.categoryFilter = data.categories;
filePickerModal.categoryFilter = data.categories
} else {
filePickerModal.categoryFilter = [];
filePickerModal.categoryFilter = []
}
filePickerModal.usageHistoryKey = "filePickerUsageHistory";
filePickerModal.open();
filePickerModal.usageHistoryKey = "filePickerUsageHistory"
filePickerModal.open()
}
}

View File

@@ -332,16 +332,18 @@ Item {
if (!provider)
return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible)
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
else
} else {
root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
}
@@ -349,16 +351,18 @@ Item {
if (!provider)
return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible)
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
else
} else {
root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
}
@@ -366,7 +370,8 @@ Item {
if (!provider)
return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item)
@@ -380,7 +385,8 @@ Item {
if (!provider)
return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item)

View File

@@ -46,7 +46,6 @@ FocusScope {
property bool pathInputHasFocus: false
property int actualGridColumns: 5
property bool _initialized: false
property bool closeOnEscape: true
signal fileSelected(string path)
signal closeRequested
@@ -299,7 +298,7 @@ FocusScope {
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) {
if (event.key === Qt.Key_Escape && root.closeOnEscape) {
if (event.key === Qt.Key_Escape) {
closeRequested();
event.accepted = true;
return;

View File

@@ -35,7 +35,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400)
implicitWidth: 800
implicitHeight: 600
color: Theme.surfaceContainer
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onVisibleChanged: {
@@ -59,7 +59,6 @@ FloatingWindow {
id: content
anchors.fill: parent
focus: true
closeOnEscape: false
browserTitle: fileBrowserModal.browserTitle
browserIcon: fileBrowserModal.browserIcon

View File

@@ -17,11 +17,7 @@ DankModal {
modalWidth: _maxW
modalHeight: _maxH
onBackgroundClicked: close()
onOpened: {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
}
onOpened: () => Qt.callLater(() => modalFocusScope.forceActiveFocus())
HyprlandFocusGrab {
windows: [root.contentWindow]
@@ -70,7 +66,7 @@ DankModal {
spacing: Theme.spacingL
StyledText {
text: KeybindsService.cheatsheet.title || "Keybinds"
text: KeybindsService.keybinds.title || "Keybinds"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
@@ -86,7 +82,7 @@ DankModal {
Component.onCompleted: root.activeFlickable = mainFlickable
property var rawBinds: KeybindsService.cheatsheet.binds || {}
property var rawBinds: KeybindsService.keybinds.binds || {}
property var categories: {
const processed = {};
for (const cat in rawBinds) {
@@ -96,8 +92,6 @@ DankModal {
for (let i = 0; i < binds.length; i++) {
const bind = binds[i];
if (bind.hideOnOverlay)
continue;
if (bind.subcat) {
hasSubcats = true;
if (!subcats[bind.subcat])
@@ -110,9 +104,6 @@ DankModal {
}
}
if (Object.keys(subcats).length === 0)
continue;
processed[cat] = {
hasSubcats: hasSubcats,
subcats: subcats,
@@ -123,36 +114,12 @@ DankModal {
}
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) {
const columns = [];
const heights = [];
for (let i = 0; i < cols; i++) {
for (let i = 0; i < cols; i++)
columns.push([]);
heights.push(0);
}
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);
}
for (let i = 0; i < categoryKeys.length; i++)
columns[i % cols].push(categoryKeys[i]);
return columns;
}
@@ -170,7 +137,7 @@ DankModal {
Column {
id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingXL
spacing: Theme.spacingM
Repeater {
model: rowLayout.columnCategories[index] || []
@@ -232,37 +199,37 @@ DankModal {
Repeater {
model: parent.parent.subcatBinds
Item {
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledRect {
id: keyBadge
width: Math.min(keyText.implicitWidth + 12, 160)
width: Math.min(140, parent.width * 0.42)
height: 22
radius: 4
anchors.verticalCenter: parent.verticalCenter
opacity: 0.9
StyledText {
id: keyText
anchors.centerIn: parent
anchors.margins: 2
width: parent.width - 4
color: Theme.secondary
text: modelData.key || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: 170
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.desc || modelData.action || ""
width: parent.width - 150
text: modelData.desc || ""
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
}
}

View File

@@ -59,7 +59,6 @@ DankModal {
modalWidth: 500
modalHeight: 700
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onBackgroundClicked: hide()
onOpened: () => {

View File

@@ -45,7 +45,7 @@ FloatingWindow {
title: I18n.tr("Authentication")
minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight)
color: Theme.surfaceContainer
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onVisibleChanged: {

View File

@@ -66,7 +66,7 @@ FloatingWindow {
minimumSize: Qt.size(650, 400)
implicitWidth: 900
implicitHeight: 680
color: Theme.surfaceContainer
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onVisibleChanged: {
@@ -112,6 +112,12 @@ FloatingWindow {
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
switch (event.key) {
case Qt.Key_1:
currentTab = 0;

View File

@@ -60,7 +60,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400)
implicitWidth: 800
implicitHeight: 940
color: Theme.surfaceContainer
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onIsCompactModeChanged: {
@@ -139,6 +139,11 @@ FloatingWindow {
focus: true
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)) {
sidebar.navigateNext();
event.accepted = true;

View File

@@ -191,7 +191,7 @@ FloatingWindow {
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight)
color: Theme.surfaceContainer
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onVisibleChanged: {

View File

@@ -97,7 +97,7 @@ DankPopout {
property alias searchField: searchField
color: "transparent"
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
antialiasing: true
smooth: true

View File

@@ -113,7 +113,11 @@ DankPopout {
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: "transparent"
color: {
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
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0

View File

@@ -15,19 +15,19 @@ Rectangle {
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") {
return false;
return false
}
return PowerProfiles.profile === profile;
return PowerProfiles.profile === profile
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available");
return;
ToastService.showError("power-profiles-daemon not available")
return
}
PowerProfiles.profile = profile;
PowerProfiles.profile = profile
if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile");
ToastService.showError("Failed to set power profile")
}
}
@@ -42,6 +42,7 @@ Rectangle {
Row {
id: headerRow
width: parent.width
height: 48
spacing: Theme.spacingM
DankIcon {
@@ -49,10 +50,10 @@ Rectangle {
size: Theme.iconSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
return Theme.error
if (BatteryService.isCharging || BatteryService.isPluggedIn)
return Theme.primary;
return Theme.surfaceText;
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -70,12 +71,12 @@ Rectangle {
font.pixelSize: Theme.fontSizeXLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
return Theme.error
}
if (BatteryService.isCharging) {
return Theme.primary;
return Theme.primary
}
return Theme.surfaceText;
return Theme.surfaceText
}
font.weight: Font.Bold
}
@@ -85,12 +86,12 @@ Rectangle {
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
return Theme.error
}
if (BatteryService.isCharging) {
return Theme.primary;
return Theme.primary
}
return Theme.surfaceText;
return Theme.surfaceText
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
@@ -99,13 +100,12 @@ Rectangle {
StyledText {
text: {
if (!BatteryService.batteryAvailable)
return "Power profile management available";
const time = BatteryService.formatTimeRemaining();
if (!BatteryService.batteryAvailable) return "Power profile management available"
const time = BatteryService.formatTimeRemaining()
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
color: Theme.surfaceTextMedium
@@ -145,10 +145,10 @@ Rectangle {
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.batteryHealth === "N/A") {
return Theme.surfaceText;
return Theme.surfaceText
}
const healthNum = parseInt(BatteryService.batteryHealth);
return healthNum < 80 ? Theme.error : Theme.surfaceText;
const healthNum = parseInt(BatteryService.batteryHealth)
return healthNum < 80 ? Theme.error : Theme.surfaceText
}
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
@@ -189,9 +189,8 @@ Rectangle {
DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
return profileModel.findIndex(profile => isActiveProfile(profile));
if (typeof PowerProfiles === "undefined") return 1
return profileModel.findIndex(profile => isActiveProfile(profile))
}
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -199,9 +198,8 @@ Rectangle {
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected)
return;
setProfile(profileModel[index]);
if (!selected) return
setProfile(profileModel[index])
}
}
@@ -257,4 +255,4 @@ Rectangle {
}
}
}
}
}

View File

@@ -1,6 +1,9 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -23,12 +26,13 @@ DankPopout {
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available");
return;
return ;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile");
}
}
popupWidth: 400
@@ -44,7 +48,7 @@ DankPopout {
id: batteryContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent"
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
@@ -55,8 +59,9 @@ DankPopout {
if (root.shouldBeVisible) {
forceActiveFocus();
}
}
Keys.onPressed: function (event) {
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.close();
event.accepted = true;
@@ -66,10 +71,11 @@ DankPopout {
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function () {
Qt.callLater(function() {
batteryContent.forceActiveFocus();
});
}
}
target: root
@@ -240,8 +246,7 @@ DankPopout {
StyledText {
text: {
if (!BatteryService.batteryAvailable)
return "Power profile management available";
if (!BatteryService.batteryAvailable) return "Power profile management available"
const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
@@ -465,14 +470,14 @@ DankPopout {
StyledText {
text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A";
return `${Math.round(modelData.healthPercentage)}%`;
return "N/A"
return `${Math.round(modelData.healthPercentage)}%`
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText;
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText;
return Theme.surfaceText
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
}
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
@@ -521,7 +526,10 @@ DankPopout {
spacing: 2
StyledText {
text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : ""
text: modelData.state === UPowerDeviceState.Charging
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
@@ -530,14 +538,17 @@ DankPopout {
StyledText {
text: {
const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0;
const time = modelData.state === UPowerDeviceState.Charging
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
if (!time || time <= 0 || time > 86400)
return "N/A";
return "N/A"
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
const hours = Math.floor(time / 3600)
const minutes = Math.floor((time % 3600) / 60)
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
@@ -555,9 +566,8 @@ DankPopout {
DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
if (typeof PowerProfiles === "undefined") return 1
return profileModel.findIndex(profile => root.isActiveProfile(profile))
}
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -565,9 +575,8 @@ DankPopout {
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected)
return;
root.setProfile(profileModel[index]);
if (!selected) return
root.setProfile(profileModel[index])
}
}
@@ -625,4 +634,5 @@ DankPopout {
}
}
}
}
}

View File

@@ -1,4 +1,8 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -11,31 +15,31 @@ DankPopout {
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x;
triggerY = y;
triggerWidth = width;
triggerSection = section;
root.screen = screen;
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
root.screen = screen
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
storedBarConfig = barConfig;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4)
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4
storedBarConfig = barConfig
const pos = barPosition !== undefined ? barPosition : 0;
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
const pos = barPosition !== undefined ? barPosition : 0
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0
setBarContext(pos, bottomGap);
setBarContext(pos, bottomGap)
updateOutputState();
updateOutputState()
}
onScreenChanged: updateOutputState()
function updateOutputState() {
if (screen && DwlService.dwlAvailable) {
outputState = DwlService.getOutputState(screen.name);
outputState = DwlService.getOutputState(screen.name)
} else {
outputState = null;
outputState = null
}
}
@@ -43,56 +47,56 @@ DankPopout {
property string currentLayoutSymbol: outputState?.layoutSymbol || ""
readonly property var layoutNames: ({
"CT": I18n.tr("Center Tiling"),
"G": I18n.tr("Grid"),
"K": I18n.tr("Deck"),
"M": I18n.tr("Monocle"),
"RT": I18n.tr("Right Tiling"),
"S": I18n.tr("Scrolling"),
"T": I18n.tr("Tiling"),
"VG": I18n.tr("Vertical Grid"),
"VK": I18n.tr("Vertical Deck"),
"VS": I18n.tr("Vertical Scrolling"),
"VT": I18n.tr("Vertical Tiling")
})
"CT": I18n.tr("Center Tiling"),
"G": I18n.tr("Grid"),
"K": I18n.tr("Deck"),
"M": I18n.tr("Monocle"),
"RT": I18n.tr("Right Tiling"),
"S": I18n.tr("Scrolling"),
"T": I18n.tr("Tiling"),
"VG": I18n.tr("Vertical Grid"),
"VK": I18n.tr("Vertical Deck"),
"VS": I18n.tr("Vertical Scrolling"),
"VT": I18n.tr("Vertical Tiling")
})
readonly property var layoutIcons: ({
"CT": "view_compact",
"G": "grid_view",
"K": "layers",
"M": "fullscreen",
"RT": "view_sidebar",
"S": "view_carousel",
"T": "view_quilt",
"VG": "grid_on",
"VK": "view_day",
"VS": "scrollable_header",
"VT": "clarify"
})
"CT": "view_compact",
"G": "grid_view",
"K": "layers",
"M": "fullscreen",
"RT": "view_sidebar",
"S": "view_carousel",
"T": "view_quilt",
"VG": "grid_on",
"VK": "view_day",
"VS": "scrollable_header",
"VT": "clarify"
})
function getLayoutName(symbol) {
return layoutNames[symbol] || symbol;
return layoutNames[symbol] || symbol
}
function getLayoutIcon(symbol) {
return layoutIcons[symbol] || "view_quilt";
return layoutIcons[symbol] || "view_quilt"
}
Connections {
target: DwlService
function onStateChanged() {
updateOutputState();
updateOutputState()
}
}
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
updateOutputState();
updateOutputState()
}
}
Component.onCompleted: {
updateOutputState();
updateOutputState()
}
popupWidth: 300
@@ -107,7 +111,7 @@ DankPopout {
id: layoutContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent"
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
@@ -117,14 +121,14 @@ DankPopout {
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus();
forceActiveFocus()
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close();
event.accepted = true;
root.close()
event.accepted = true
}
}
@@ -133,8 +137,8 @@ DankPopout {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => {
layoutContent.forceActiveFocus();
});
layoutContent.forceActiveFocus()
})
}
}
}
@@ -208,7 +212,7 @@ DankPopout {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.close();
root.close()
}
}
}
@@ -278,14 +282,14 @@ DankPopout {
cursorShape: Qt.PointingHandCursor
onPressed: {
if (!root.triggerScreen) {
return;
return
}
if (!DwlService.dwlAvailable) {
return;
return
}
DwlService.setLayout(root.triggerScreen.name, index);
root.close();
DwlService.setLayout(root.triggerScreen.name, index)
root.close()
}
}

View File

@@ -36,7 +36,7 @@ DankPopout {
id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2
color: "transparent"
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0

View File

@@ -167,7 +167,7 @@ DankPopout {
id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent"
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
focus: true
@@ -358,8 +358,6 @@ DankPopout {
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
section: root.triggerSection
barPosition: root.effectiveBarPosition
Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players);
@@ -384,7 +382,7 @@ DankPopout {
active: true
tabBarItem: tabBar
keyForwardTarget: mainContainer
targetScreen: root.screen
targetScreen: root.triggerScreen
parentPopout: root
}
}

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