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

Compare commits

...

12 Commits

Author SHA1 Message Date
purian23
e307de83e2 packages: Update manual changelogs 2025-12-09 14:17:53 -05:00
bbedward
85968ec417 core/server: refactory to use shared params/request structs 2025-12-09 14:13:20 -05:00
bbedward
993f14a31f widgets: make dank icon picker a popup 2025-12-09 13:41:12 -05:00
purian23
566d617508 Re-adjust systemd debian/ubuntu 2025-12-09 13:40:59 -05:00
purian23
542a279fcb Add systemd debian/ubuntu packages 2025-12-09 12:39:56 -05:00
purian23
e784bb89e1 Version lock dms fedora/opensuse packages 2025-12-09 12:39:21 -05:00
bbedward
f680ace258 keybinds: fix dms args for some commands, some XF86 mappings 2025-12-09 12:21:20 -05:00
bbedward
7aa5976e07 media: fix padding issues with long titles 2025-12-09 11:46:50 -05:00
bbedward
f88f1ea951 gamma: display automation state in UI 2025-12-09 11:26:28 -05:00
bbedward
da4561cb35 keybinds: support more keys, allow Super+Alt 2025-12-09 10:41:39 -05:00
bbedward
1f89ae9813 popout: fix sizing on older QT 2025-12-09 09:57:31 -05:00
bbedward
5647323449 gamma: switch to wlsunset-style transitions 2025-12-09 09:44:16 -05:00
60 changed files with 2935 additions and 2761 deletions

View File

@@ -511,7 +511,7 @@ jobs:
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
@@ -541,17 +541,6 @@ 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
@@ -576,18 +565,10 @@ 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
@@ -617,10 +598,8 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
%files
%license LICENSE
@@ -636,14 +615,10 @@ 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 and dgop binaries will be downloaded during build based on target architecture"
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
@@ -94,7 +94,7 @@ jobs:
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
@@ -125,17 +125,6 @@ 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
@@ -162,19 +151,10 @@ 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
@@ -202,11 +182,8 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Restart DMS for active users after upgrade
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
# Signal running DMS instances to reload (harmless if none running)
pkill -USR1 -x dms >/dev/null 2>&1 || :
%files
%license LICENSE
@@ -220,14 +197,10 @@ 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

@@ -22,6 +22,8 @@ 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

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

View File

@@ -11,12 +11,13 @@ import (
)
type NiriKeyBinding struct {
Mods []string
Key string
Action string
Args []string
Description string
Source string
Mods []string
Key string
Action string
Args []string
Description string
HideOnOverlay bool
Source string
}
type NiriSection struct {
@@ -273,19 +274,26 @@ 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 {
description = val.ValueString()
switch val.ValueString() {
case "null", "":
hideOnOverlay = true
default:
description = val.ValueString()
}
}
}
return &NiriKeyBinding{
Mods: mods,
Key: key,
Action: action,
Args: args,
Description: description,
Source: p.currentSource,
Mods: mods,
Key: key,
Action: action,
Args: args,
Description: description,
HideOnOverlay: hideOnOverlay,
Source: p.currentSource,
}
}

View File

@@ -1,12 +1,13 @@
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"`
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"`
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
}
type DMSBindsStatus struct {

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,14 @@ 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 Request, m *Manager) {
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method {
case "brightness.getState":
handleGetState(conn, req, m)
@@ -22,131 +24,90 @@ func HandleRequest(conn net.Conn, req Request, m *Manager) {
case "brightness.subscribe":
handleSubscribe(conn, req, m)
default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method)
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
}
}
func handleGetState(conn net.Conn, req Request, m *Manager) {
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
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())
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())
return
}
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
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())
}
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")
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())
return
}
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
}
step := params.IntOpt(req.Params, "step", 10)
exponential := params.BoolOpt(req.Params, "exponential", false)
exponent := params.FloatOpt(req.Params, "exponent", 1.2)
if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error())
models.RespondError(conn, req.ID, err.Error())
return
}
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
models.Respond(conn, req.ID, m.GetState())
}
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")
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())
return
}
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
}
step := params.IntOpt(req.Params, "step", 10)
exponential := params.BoolOpt(req.Params, "exponential", false)
exponent := params.FloatOpt(req.Params, "exponent", 1.2)
if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil {
models.RespondError(conn, req.ID.(int), err.Error())
models.RespondError(conn, req.ID, err.Error())
return
}
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
models.Respond(conn, req.ID, m.GetState())
}
func handleRescan(conn net.Conn, req Request, m *Manager) {
func handleRescan(conn net.Conn, req models.Request, m *Manager) {
m.Rescan()
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
models.Respond(conn, req.ID, m.GetState())
}
func handleSubscribe(conn net.Conn, req Request, m *Manager) {
clientID := "brightness-subscriber"
if idStr, ok := req.ID.(string); ok && idStr != "" {
clientID = idStr
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("brightness-%d", req.ID)
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID.(int),
ID: req.ID,
Result: &initialState,
}); err != nil {
return
@@ -154,7 +115,7 @@ func handleSubscribe(conn net.Conn, req Request, m *Manager) {
for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID.(int),
ID: req.ID,
Result: &state,
}); err != nil {
return

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ func TestHandleGetPrinters(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
req := models.Request{
ID: 1,
Method: "cups.getPrinters",
}
@@ -68,7 +68,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
req := models.Request{
ID: 1,
Method: "cups.getPrinters",
}
@@ -100,7 +100,7 @@ func TestHandleGetJobs(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
req := models.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 := Request{
req := models.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 := Request{
req := models.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[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.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[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.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[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.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[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.unknownMethod",
}
@@ -287,7 +287,7 @@ func TestHandleGetDevices(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getDevices"}
req := models.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 := Request{ID: 1, Method: "cups.getPPDs"}
req := models.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 := Request{ID: 1, Method: "cups.getClasses"}
req := models.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 := Request{
req := models.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[SuccessResult]
var resp models.Response[models.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 := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m)
var resp models.Response[any]
@@ -396,14 +396,14 @@ func TestHandleDeletePrinter(t *testing.T) {
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
req := models.Request{
ID: 1,
Method: "cups.deletePrinter",
Params: map[string]any{"printerName": "printer1"},
}
handleDeletePrinter(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.acceptJobs",
Params: map[string]any{"printerName": "printer1"},
}
handleAcceptJobs(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.rejectJobs",
Params: map[string]any{"printerName": "printer1"},
}
handleRejectJobs(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.setPrinterShared",
Params: map[string]any{"printerName": "printer1", "shared": true},
}
handleSetPrinterShared(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.setPrinterLocation",
Params: map[string]any{"printerName": "printer1", "location": "Office"},
}
handleSetPrinterLocation(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.setPrinterInfo",
Params: map[string]any{"printerName": "printer1", "info": "Main Printer"},
}
handleSetPrinterInfo(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.moveJob",
Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"},
}
handleMoveJob(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.addPrinterToClass",
Params: map[string]any{"className": "office", "printerName": "printer1"},
}
handleAddPrinterToClass(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.removePrinterFromClass",
Params: map[string]any{"className": "office", "printerName": "printer1"},
}
handleRemovePrinterFromClass(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.deleteClass",
Params: map[string]any{"className": "office"},
}
handleDeleteClass(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.restartJob",
Params: map[string]any{"jobID": float64(1)},
}
handleRestartJob(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1)},
}
handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.Request{
ID: 1,
Method: "cups.holdJob",
Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"},
}
handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,20 +6,10 @@ 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"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "loginctl.getState":
handleGetState(conn, req, manager)
@@ -46,39 +36,38 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleLock(conn net.Conn, req Request, manager *Manager) {
func handleLock(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Lock(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "locked"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked"})
}
func handleUnlock(conn net.Conn, req Request, manager *Manager) {
func handleUnlock(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Unlock(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "unlocked"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "unlocked"})
}
func handleActivate(conn net.Conn, req Request, manager *Manager) {
func handleActivate(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Activate(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "activated"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "activated"})
}
func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) {
idle, ok := req.Params["idle"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'idle' parameter")
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())
return
}
@@ -86,32 +75,32 @@ func handleSetIdleHint(conn net.Conn, req Request, manager *Manager) {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "idle hint set"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
}
func handleSetLockBeforeSuspend(conn net.Conn, req Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter")
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())
return
}
manager.SetLockBeforeSuspend(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "lock before suspend set"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"})
}
func handleSetSleepInhibitorEnabled(conn net.Conn, req Request, manager *Manager) {
enabled, ok := req.Params["enabled"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter")
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())
return
}
manager.SetSleepInhibitorEnabled(enabled)
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"})
}
func handleLockerReady(conn net.Conn, req Request, manager *Manager) {
func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) {
manager.lockTimerMu.Lock()
if manager.lockTimer != nil {
manager.lockTimer.Stop()
@@ -125,18 +114,18 @@ func handleLockerReady(conn net.Conn, req Request, manager *Manager) {
if manager.inSleepCycle.Load() {
manager.signalLockerReady()
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "ok"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "ok"})
}
func handleTerminate(conn net.Conn, req Request, manager *Manager) {
func handleTerminate(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Terminate(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "terminated"})
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "terminated"})
}
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
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 := SuccessResult{Success: true, Message: "test"}
result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -86,7 +86,7 @@ func TestHandleGetState(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.getState"}
req := models.Request{ID: 123, Method: "loginctl.getState"}
handleGetState(conn, req, manager)
@@ -115,10 +115,10 @@ func TestHandleLock(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.lock"}
req := models.Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -141,10 +141,10 @@ func TestHandleLock(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.lock"}
req := models.Request{ID: 123, Method: "loginctl.lock"}
handleLock(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -166,10 +166,10 @@ func TestHandleUnlock(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.unlock"}
req := models.Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -192,10 +192,10 @@ func TestHandleUnlock(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.unlock"}
req := models.Request{ID: 123, Method: "loginctl.unlock"}
handleUnlock(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -217,10 +217,10 @@ func TestHandleActivate(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.activate"}
req := models.Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -243,10 +243,10 @@ func TestHandleActivate(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.activate"}
req := models.Request{ID: 123, Method: "loginctl.activate"}
handleActivate(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -263,7 +263,7 @@ func TestHandleSetIdleHint(t *testing.T) {
}
conn := newMockNetConn()
req := Request{
req := models.Request{
ID: 123,
Method: "loginctl.setIdleHint",
Params: map[string]any{},
@@ -291,7 +291,7 @@ func TestHandleSetIdleHint(t *testing.T) {
}
conn := newMockNetConn()
req := Request{
req := models.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[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -324,7 +324,7 @@ func TestHandleSetIdleHint(t *testing.T) {
}
conn := newMockNetConn()
req := Request{
req := models.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[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -356,10 +356,10 @@ func TestHandleTerminate(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.terminate"}
req := models.Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
@@ -382,10 +382,10 @@ func TestHandleTerminate(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.terminate"}
req := models.Request{ID: 123, Method: "loginctl.terminate"}
handleTerminate(conn, req, manager)
var resp models.Response[SuccessResult]
var resp models.Response[models.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 := Request{
req := models.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 := Request{
req := models.Request{
ID: 123,
Method: "loginctl.getState",
}
@@ -445,7 +445,7 @@ func TestHandleRequest(t *testing.T) {
manager.sessionObj = mockSessionObj
conn := newMockNetConn()
req := Request{
req := models.Request{
ID: 123,
Method: "loginctl.lock",
}
@@ -470,7 +470,7 @@ func TestHandleSubscribe(t *testing.T) {
}
conn := newMockNetConn()
req := Request{ID: 123, Method: "loginctl.subscribe"}
req := models.Request{ID: 123, Method: "loginctl.subscribe"}
done := make(chan bool)
go func() {

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
package params
import "fmt"
func Get[T any](params map[string]any, key string) (T, error) {
val, ok := params[key].(T)
if !ok {
var zero T
return zero, fmt.Errorf("missing or invalid '%s' parameter", key)
}
return val, nil
}
func GetOpt[T any](params map[string]any, key string, def T) T {
if val, ok := params[key].(T); ok {
return val
}
return def
}
func String(params map[string]any, key string) (string, error) {
return Get[string](params, key)
}
func StringNonEmpty(params map[string]any, key string) (string, error) {
val, err := Get[string](params, key)
if err != nil || val == "" {
return "", fmt.Errorf("missing or invalid '%s' parameter", key)
}
return val, nil
}
func StringOpt(params map[string]any, key string, def string) string {
return GetOpt(params, key, def)
}
func Int(params map[string]any, key string) (int, error) {
val, err := Get[float64](params, key)
if err != nil {
return 0, err
}
return int(val), nil
}
func IntOpt(params map[string]any, key string, def int) int {
if val, ok := params[key].(float64); ok {
return int(val)
}
return def
}
func Float(params map[string]any, key string) (float64, error) {
return Get[float64](params, key)
}
func FloatOpt(params map[string]any, key string, def float64) float64 {
return GetOpt(params, key, def)
}
func Bool(params map[string]any, key string) (bool, error) {
return Get[bool](params, key)
}
func BoolOpt(params map[string]any, key string, def bool) bool {
return GetOpt(params, key, def)
}
func StringMap(params map[string]any, key string) (map[string]string, error) {
rawMap, err := Get[map[string]any](params, key)
if err != nil {
return nil, err
}
result := make(map[string]string)
for k, v := range rawMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result, nil
}
func StringMapOpt(params map[string]any, key string) map[string]string {
rawMap, ok := params[key].(map[string]any)
if !ok {
return nil
}
result := make(map[string]string)
for k, v := range rawMap {
if str, ok := v.(string); ok {
result[k] = str
}
}
return result
}
func Any(params map[string]any, key string) (any, bool) {
val, ok := params[key]
return val, ok
}
func AnyMap(params map[string]any, key string) (map[string]any, bool) {
val, ok := params[key].(map[string]any)
return val, ok
}
func StringAlt(params map[string]any, keys ...string) (string, bool) {
for _, key := range keys {
if val, ok := params[key].(string); ok {
return val, true
}
}
return "", false
}

View File

@@ -0,0 +1,154 @@
package params
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGet(t *testing.T) {
p := map[string]any{"key": "value"}
val, err := Get[string](p, "key")
assert.NoError(t, err)
assert.Equal(t, "value", val)
_, err = Get[string](p, "missing")
assert.Error(t, err)
_, err = Get[int](p, "key")
assert.Error(t, err)
}
func TestGetOpt(t *testing.T) {
p := map[string]any{"key": "value"}
assert.Equal(t, "value", GetOpt(p, "key", "default"))
assert.Equal(t, "default", GetOpt(p, "missing", "default"))
}
func TestString(t *testing.T) {
p := map[string]any{"s": "hello", "n": 123}
val, err := String(p, "s")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
_, err = String(p, "n")
assert.Error(t, err)
}
func TestStringNonEmpty(t *testing.T) {
p := map[string]any{"s": "hello", "empty": ""}
val, err := StringNonEmpty(p, "s")
assert.NoError(t, err)
assert.Equal(t, "hello", val)
_, err = StringNonEmpty(p, "empty")
assert.Error(t, err)
_, err = StringNonEmpty(p, "missing")
assert.Error(t, err)
}
func TestStringOpt(t *testing.T) {
p := map[string]any{"s": "hello"}
assert.Equal(t, "hello", StringOpt(p, "s", "default"))
assert.Equal(t, "default", StringOpt(p, "missing", "default"))
}
func TestInt(t *testing.T) {
p := map[string]any{"n": float64(42), "s": "str"}
val, err := Int(p, "n")
assert.NoError(t, err)
assert.Equal(t, 42, val)
_, err = Int(p, "s")
assert.Error(t, err)
}
func TestIntOpt(t *testing.T) {
p := map[string]any{"n": float64(42)}
assert.Equal(t, 42, IntOpt(p, "n", 0))
assert.Equal(t, 99, IntOpt(p, "missing", 99))
}
func TestFloat(t *testing.T) {
p := map[string]any{"f": 3.14, "s": "str"}
val, err := Float(p, "f")
assert.NoError(t, err)
assert.Equal(t, 3.14, val)
_, err = Float(p, "s")
assert.Error(t, err)
}
func TestFloatOpt(t *testing.T) {
p := map[string]any{"f": 3.14}
assert.Equal(t, 3.14, FloatOpt(p, "f", 0))
assert.Equal(t, 1.0, FloatOpt(p, "missing", 1.0))
}
func TestBool(t *testing.T) {
p := map[string]any{"b": true, "s": "str"}
val, err := Bool(p, "b")
assert.NoError(t, err)
assert.True(t, val)
_, err = Bool(p, "s")
assert.Error(t, err)
}
func TestBoolOpt(t *testing.T) {
p := map[string]any{"b": true}
assert.True(t, BoolOpt(p, "b", false))
assert.True(t, BoolOpt(p, "missing", true))
}
func TestStringMap(t *testing.T) {
p := map[string]any{
"m": map[string]any{"a": "1", "b": "2", "c": 3},
}
val, err := StringMap(p, "m")
assert.NoError(t, err)
assert.Equal(t, map[string]string{"a": "1", "b": "2"}, val)
_, err = StringMap(p, "missing")
assert.Error(t, err)
}
func TestStringMapOpt(t *testing.T) {
p := map[string]any{
"m": map[string]any{"a": "1"},
}
assert.Equal(t, map[string]string{"a": "1"}, StringMapOpt(p, "m"))
assert.Nil(t, StringMapOpt(p, "missing"))
}
func TestAny(t *testing.T) {
p := map[string]any{"k": 123}
val, ok := Any(p, "k")
assert.True(t, ok)
assert.Equal(t, 123, val)
_, ok = Any(p, "missing")
assert.False(t, ok)
}
func TestAnyMap(t *testing.T) {
inner := map[string]any{"nested": true}
p := map[string]any{"m": inner}
val, ok := AnyMap(p, "m")
assert.True(t, ok)
assert.Equal(t, inner, val)
_, ok = AnyMap(p, "missing")
assert.False(t, ok)
}
func TestStringAlt(t *testing.T) {
p := map[string]any{"b": "found"}
val, ok := StringAlt(p, "a", "b", "c")
assert.True(t, ok)
assert.Equal(t, "found", val)
_, ok = StringAlt(p, "x", "y")
assert.False(t, ok)
}

View File

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

View File

@@ -2,8 +2,6 @@ package wayland
import (
"math"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type GammaRamp struct {
@@ -12,6 +10,126 @@ 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),
@@ -19,16 +137,13 @@ 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)
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))
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)
}
return ramp
@@ -50,39 +165,3 @@ 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 TestTemperatureToRGB(t *testing.T) {
func TestCalcWhitepoint(t *testing.T) {
tests := []struct {
name string
temp int
@@ -67,32 +67,32 @@ func TestTemperatureToRGB(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, g, b := temperatureToRGB(tt.temp)
wp := calcWhitepoint(tt.temp)
if r < 0 || r > 1 {
t.Errorf("red out of range: %f", r)
if wp.r < 0 || wp.r > 1 {
t.Errorf("red out of range: %f", wp.r)
}
if g < 0 || g > 1 {
t.Errorf("green out of range: %f", g)
if wp.g < 0 || wp.g > 1 {
t.Errorf("green out of range: %f", wp.g)
}
if b < 0 || b > 1 {
t.Errorf("blue out of range: %f", b)
if wp.b < 0 || wp.b > 1 {
t.Errorf("blue out of range: %f", wp.b)
}
})
}
}
func TestTemperatureProgression(t *testing.T) {
func TestWhitepointProgression(t *testing.T) {
temps := []int{3000, 4000, 5000, 6000, 6500}
var prevBlue float64
for i, temp := range temps {
_, _, b := temperatureToRGB(temp)
if i > 0 && b < prevBlue {
wp := calcWhitepoint(temp)
if i > 0 && wp.b < prevBlue {
t.Errorf("blue should increase with temperature, %d->%d: %f->%f",
temps[i-1], temp, prevBlue, b)
temps[i-1], temp, prevBlue, wp.b)
}
prevBlue = b
prevBlue = wp.b
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -340,38 +340,47 @@ func TestCalculateNextTransition(t *testing.T) {
}
}
func TestTimeOfDayToTime(t *testing.T) {
func TestSunTimesWithTwilight(t *testing.T) {
lat := 40.7128
lon := -74.0060
date := time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local)
times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
if cond != SunNormal {
t.Errorf("expected SunNormal, got %v", cond)
}
if !times.Dawn.Before(times.Sunrise) {
t.Error("dawn should be before sunrise")
}
if !times.Sunrise.Before(times.Sunset) {
t.Error("sunrise should be before sunset")
}
if !times.Sunset.Before(times.Night) {
t.Error("sunset should be before night")
}
}
func TestSunConditions(t *testing.T) {
tests := []struct {
name string
hours float64
expected time.Time
lat float64
date time.Time
expected SunCondition
}{
{
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),
name: "normal_conditions",
lat: 40.0,
date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.UTC),
expected: SunNormal,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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())
_, 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)
}
})
}

View File

@@ -11,18 +11,28 @@ 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
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
}
type State struct {
@@ -31,13 +41,24 @@ 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
@@ -60,10 +81,9 @@ type Manager struct {
updateTrigger chan struct{}
wg sync.WaitGroup
currentTemp int
targetTemp int
transitionMutex sync.RWMutex
transitionChan chan int
schedule sunSchedule
scheduleMutex sync.RWMutex
gammaState GammaState
cachedIPLat *float64
cachedIPLon *float64
@@ -80,7 +100,6 @@ type Manager struct {
type outputState struct {
id uint32
name string
registryName uint32
output *wlclient.Output
gammaControl any
@@ -91,18 +110,15 @@ 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,
Outputs: []string{},
LowTemp: 4000,
HighTemp: 6500,
Gamma: 1.0,
Enabled: false,
ElevationTwilight: -6.0,
ElevationDaylight: 3.0,
}
}
@@ -140,8 +156,7 @@ func (m *Manager) GetState() State {
if m.state == nil {
return State{}
}
stateCopy := *m.state
return stateCopy
return *m.state
}
func (m *Manager) Subscribe(id string) chan State {
@@ -185,5 +200,8 @@ func stateChanged(old, new *State) bool {
if old.Config.Enabled != new.Config.Enabled {
return true
}
if old.SunPosition != new.SunPosition {
return true
}
return false
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,9 @@ 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,6 +13,9 @@ 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

@@ -39,11 +39,12 @@ const KEY_MAP = {
16777329: "XF86AudioMute",
16842808: "XF86AudioMicMute",
16777344: "XF86AudioPlay",
16777345: "XF86AudioPause",
16777346: "XF86AudioStop",
16777345: "XF86AudioStop",
16777346: "XF86AudioPrev",
16777347: "XF86AudioNext",
16777348: "XF86AudioPrev",
16842792: "XF86AudioRecord",
16777348: "XF86AudioPause",
16777349: "XF86AudioMedia",
16777350: "XF86AudioRecord",
16842798: "XF86MonBrightnessUp",
16842797: "XF86MonBrightnessDown",
16842800: "XF86KbdBrightnessUp",
@@ -79,12 +80,49 @@ const KEY_MAP = {
124: "Backslash",
95: "Minus",
43: "Equal",
126: "grave"
126: "grave",
196: "Adiaeresis",
214: "Odiaeresis",
220: "Udiaeresis",
228: "adiaeresis",
246: "odiaeresis",
252: "udiaeresis",
223: "ssharp",
201: "Eacute",
233: "eacute",
200: "Egrave",
232: "egrave",
202: "Ecircumflex",
234: "ecircumflex",
203: "Ediaeresis",
235: "ediaeresis",
192: "Agrave",
224: "agrave",
194: "Acircumflex",
226: "acircumflex",
199: "Ccedilla",
231: "ccedilla",
206: "Icircumflex",
238: "icircumflex",
207: "Idiaeresis",
239: "idiaeresis",
212: "Ocircumflex",
244: "ocircumflex",
217: "Ugrave",
249: "ugrave",
219: "Ucircumflex",
251: "ucircumflex",
209: "Ntilde",
241: "ntilde",
191: "questiondown",
161: "exclamdown"
};
function xkbKeyFromQtKey(qk) {
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)
@@ -94,16 +132,10 @@ function xkbKeyFromQtKey(qk) {
function modsFromEvent(mods) {
var result = [];
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 & 0x10000000)
result.push("Super");
if (mods & 0x08000000)
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", label: "Volume Up" },
{ id: "spawn dms ipc call audio increment 5", label: "Volume Up" },
{ id: "spawn dms ipc call audio increment 1", label: "Volume Up (1%)" },
{ id: "spawn dms ipc call audio increment 5", label: "Volume Up (5%)" },
{ id: "spawn dms ipc call audio increment 10", label: "Volume Up (10%)" },
{ id: "spawn dms ipc call audio decrement", label: "Volume Down" },
{ id: "spawn dms ipc call audio decrement 5", label: "Volume Down" },
{ id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" },
{ id: "spawn dms ipc call audio decrement 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", 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 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 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,19 +223,37 @@ 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: "" }]
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"audio decrement": {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
args: [
{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" },
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
},
"brightness decrement": {
base: "spawn dms ipc call brightness decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
args: [
{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" },
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
},
"brightness toggleExponential": {
base: "spawn dms ipc call brightness toggleExponential",
args: [
{ name: "device", type: "text", label: "Device", placeholder: "leave empty for default", default: "" }
]
},
"dash toggle": {
base: "spawn dms ipc call dash toggle",
args: [
{ name: "tab", type: "text", label: "Tab", placeholder: "overview, media, wallpaper, weather", default: "" }
]
}
};
@@ -243,6 +261,10 @@ function getActionTypes() {
return ACTION_TYPES;
}
function getDmsActionArgs() {
return DMS_ACTION_ARGS;
}
function getDmsActions(isNiri, isHyprland) {
const result = [];
for (let i = 0; i < DMS_ACTIONS.length; i++) {
@@ -495,10 +517,48 @@ function parseDmsActionArgs(action) {
for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key];
if (action.startsWith(config.base)) {
var rest = action.slice(config.base.length).trim();
return { base: key, args: { amount: rest || "" } };
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 (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: {} };
@@ -509,11 +569,24 @@ function buildDmsAction(baseKey, args) {
if (!config)
return "";
var action = config.base;
if (args && args.amount)
action += " " + args.amount;
var parts = [config.base];
return action;
for (var i = 0; i < config.args.length; i++) {
var argDef = config.args[i];
var value = args?.[argDef.name];
if (value === undefined || value === null)
value = argDef.default ?? "";
if (argDef.type === "text" && value === "") {
parts.push('""');
} else if (value !== "") {
parts.push(value);
} else {
break;
}
}
return parts.join(" ");
}
function getScreenshotOptions() {

View File

@@ -255,7 +255,6 @@ 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

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

View File

@@ -96,6 +96,8 @@ 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])
@@ -108,6 +110,9 @@ DankModal {
}
}
if (Object.keys(subcats).length === 0)
continue;
processed[cat] = {
hasSubcats: hasSubcats,
subcats: subcats,

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,7 +42,6 @@ Rectangle {
Row {
id: headerRow
width: parent.width
height: 48
spacing: Theme.spacingM
DankIcon {
@@ -50,10 +49,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
}
@@ -71,12 +70,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
}
@@ -86,12 +85,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
@@ -100,12 +99,13 @@ 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,8 +189,9 @@ 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))
@@ -198,8 +199,9 @@ Rectangle {
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected) return
setProfile(profileModel[index])
if (!selected)
return;
setProfile(profileModel[index]);
}
}
@@ -255,4 +257,4 @@ Rectangle {
}
}
}
}
}

View File

@@ -384,260 +384,269 @@ Item {
}
}
// Controls Group
Column {
id: controlsGroup
Item {
id: seekbarContainer
width: parent.width
spacing: Theme.spacingXS
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.top: songInfo.bottom
anchors.bottom: playbackControls.top
anchors.horizontalCenter: parent.horizontalCenter
DankSeekbar {
width: parent.width * 0.8
height: 20
anchors.horizontalCenter: parent.horizontalCenter
activePlayer: root.activePlayer
isSeeking: root.isSeeking
onIsSeekingChanged: root.isSeeking = isSeeking
}
Item {
width: parent.width * 0.8
height: 20
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: {
if (!activePlayer)
return "0:00";
const rawPos = Math.max(0, activePlayer.position || 0);
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
const minutes = Math.floor(pos / 60);
const seconds = Math.floor(pos % 60);
const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
return timeStr;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: {
if (!activePlayer || !activePlayer.length)
return "0:00";
const dur = Math.max(0, activePlayer.length || 0); // Length is already in seconds
const minutes = Math.floor(dur / 60);
const seconds = Math.floor(dur % 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
Column {
width: parent.width
height: 50
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: parent.height * 0.2
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
height: parent.height
DankSeekbar {
width: parent.width * 0.8
height: 20
anchors.horizontalCenter: parent.horizontalCenter
activePlayer: root.activePlayer
isSeeking: root.isSeeking
onIsSeekingChanged: root.isSeeking = isSeeking
}
Item {
width: 50
height: 50
Item {
width: parent.width * 0.8
height: 16
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.shuffleSupported
text: {
if (!activePlayer)
return "0:00";
const rawPos = Math.max(0, activePlayer.position || 0);
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
const minutes = Math.floor(pos / 60);
const seconds = Math.floor(pos % 60);
const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
return timeStr;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Rectangle {
width: 40
height: 40
radius: 20
StyledText {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: {
if (!activePlayer || !activePlayer.length)
return "0:00";
const dur = Math.max(0, activePlayer.length || 0);
const minutes = Math.floor(dur / 60);
const seconds = Math.floor(dur % 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
Item {
id: playbackControls
width: parent.width
height: 50
anchors.bottom: parent.bottom
Row {
anchors.centerIn: parent
spacing: Theme.spacingM
height: parent.height
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.shuffleSupported
Rectangle {
width: 40
height: 40
radius: 20
anchors.centerIn: parent
color: shuffleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
color: shuffleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
name: "shuffle"
size: 20
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
}
DankIcon {
anchors.centerIn: parent
name: "shuffle"
size: 20
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: shuffleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer && activePlayer.canControl && activePlayer.shuffleSupported) {
activePlayer.shuffle = !activePlayer.shuffle;
}
MouseArea {
id: shuffleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer && activePlayer.canControl && activePlayer.shuffleSupported) {
activePlayer.shuffle = !activePlayer.shuffle;
}
}
}
}
}
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 40
height: 40
radius: 20
Rectangle {
width: 40
height: 40
radius: 20
anchors.centerIn: parent
color: prevBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
DankIcon {
anchors.centerIn: parent
color: prevBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
name: "skip_previous"
size: 24
color: Theme.surfaceText
}
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 24
color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) {
return;
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) {
return;
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
} else {
activePlayer.previous();
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
} else {
activePlayer.previous();
}
}
}
}
}
Item {
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
radius: 25
anchors.centerIn: parent
color: Theme.primary
Rectangle {
width: 50
height: 50
radius: 25
DankIcon {
anchors.centerIn: parent
color: Theme.primary
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28
color: Theme.background
weight: 500
}
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28
color: Theme.background
weight: 500
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
}
}
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 40
height: 40
radius: 20
Rectangle {
width: 40
height: 40
radius: 20
anchors.centerIn: parent
color: nextBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
DankIcon {
anchors.centerIn: parent
color: nextBtnArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
name: "skip_next"
size: 24
color: Theme.surfaceText
}
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 24
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.loopSupported
Item {
width: 50
height: 50
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.loopSupported
Rectangle {
width: 40
height: 40
radius: 20
Rectangle {
width: 40
height: 40
radius: 20
anchors.centerIn: parent
color: repeatArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
color: repeatArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
name: {
if (!activePlayer)
return "repeat";
switch (activePlayer.loopState) {
case MprisLoopState.Track:
return "repeat_one";
case MprisLoopState.Playlist:
return "repeat";
default:
return "repeat";
}
}
size: 20
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
}
DankIcon {
anchors.centerIn: parent
name: {
if (!activePlayer)
return "repeat";
MouseArea {
id: repeatArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer && activePlayer.canControl && activePlayer.loopSupported) {
switch (activePlayer.loopState) {
case MprisLoopState.Track:
return "repeat_one";
case MprisLoopState.None:
activePlayer.loopState = MprisLoopState.Playlist;
break;
case MprisLoopState.Playlist:
return "repeat";
default:
return "repeat";
}
}
size: 20
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: repeatArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer && activePlayer.canControl && activePlayer.loopSupported) {
switch (activePlayer.loopState) {
case MprisLoopState.None:
activePlayer.loopState = MprisLoopState.Playlist;
break;
case MprisLoopState.Playlist:
activePlayer.loopState = MprisLoopState.Track;
break;
case MprisLoopState.Track:
activePlayer.loopState = MprisLoopState.None;
break;
}
activePlayer.loopState = MprisLoopState.Track;
break;
case MprisLoopState.Track:
activePlayer.loopState = MprisLoopState.None;
break;
}
}
}
@@ -648,166 +657,166 @@ Item {
}
}
}
Rectangle {
id: playerSelectorButton
width: 40
height: 40
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185
color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
z: 100
visible: (allPlayers?.length || 0) >= 1
DankIcon {
anchors.centerIn: parent
name: "assistant_device"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: playerSelectorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (playersExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
playersExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onEntered: sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
Rectangle {
id: volumeButton
width: 40
height: 40
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15)
border.width: 1
z: 101
enabled: volumeAvailable
property real previousVolume: 0.0
DankIcon {
anchors.centerIn: parent
name: getVolumeIcon()
size: 18
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, volumeAvailable ? 1.0 : 0.5)
}
MouseArea {
id: volumeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (volumeExpanded)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onExited: {
if (volumeExpanded)
volumeButtonExited();
}
onClicked: {
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
}
onWheel: wheelEvent => {
SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y;
const current = (currentVolume * 100) || 0;
const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5);
if (usePlayerVolume) {
activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100;
}
wheelEvent.accepted = true;
}
}
}
Rectangle {
id: audioDevicesButton
width: 40
height: 40
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240
color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
z: 100
DankIcon {
anchors.centerIn: parent
name: devicesExpanded ? "expand_less" : "speaker"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: audioDevicesArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (devicesExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
}
Rectangle {
id: playerSelectorButton
width: 40
height: 40
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185
color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
z: 100
visible: (allPlayers?.length || 0) >= 1
DankIcon {
anchors.centerIn: parent
name: "assistant_device"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: playerSelectorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (playersExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
playersExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onEntered: sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
Rectangle {
id: volumeButton
width: 40
height: 40
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15)
border.width: 1
z: 101
enabled: volumeAvailable
property real previousVolume: 0.0
DankIcon {
anchors.centerIn: parent
name: getVolumeIcon()
size: 18
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, volumeAvailable ? 1.0 : 0.5)
}
MouseArea {
id: volumeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (volumeExpanded)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onExited: {
if (volumeExpanded)
volumeButtonExited();
}
onClicked: {
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
}
onWheel: wheelEvent => {
SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y;
const current = (currentVolume * 100) || 0;
const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5);
if (usePlayerVolume) {
activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100;
}
wheelEvent.accepted = true;
}
}
}
Rectangle {
id: audioDevicesButton
width: 40
height: 40
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240
color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
z: 100
DankIcon {
anchors.centerIn: parent
name: devicesExpanded ? "expand_less" : "speaker"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: audioDevicesArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (devicesExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
}

View File

@@ -8,6 +8,19 @@ import qs.Widgets
Item {
id: displaysTab
function formatGammaTime(isoString) {
if (!isoString)
return "";
try {
const date = new Date(isoString);
if (isNaN(date.getTime()))
return "";
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
} catch (e) {
return "";
}
}
function getBarComponentsFromSettings() {
const bars = SettingsData.barConfigs || [];
return bars.map(bar => ({
@@ -530,6 +543,233 @@ Item {
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: gammaStatusSection.visible
}
Column {
id: gammaStatusSection
width: parent.width
spacing: Theme.spacingM
visible: DisplayService.nightModeEnabled && DisplayService.gammaCurrentTemp > 0
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: DisplayService.gammaIsDay ? "light_mode" : "dark_mode"
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Current Status")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: tempColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: tempColumn
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "device_thermostat"
size: Theme.iconSize
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: DisplayService.gammaCurrentTemp + "K"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Current Temp")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: periodColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: periodColumn
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: DisplayService.gammaIsDay ? "wb_sunny" : "nightlight"
size: Theme.iconSize
color: DisplayService.gammaIsDay ? "#FFA726" : "#7E57C2"
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: DisplayService.gammaIsDay ? I18n.tr("Daytime") : I18n.tr("Night")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Current Period")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.nightModeAutoMode === "location" && (DisplayService.gammaSunriseTime || DisplayService.gammaSunsetTime)
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: sunriseColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
visible: DisplayService.gammaSunriseTime
Column {
id: sunriseColumn
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "wb_twilight"
size: Theme.iconSize
color: "#FF7043"
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunriseTime)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Sunrise")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: sunsetColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
visible: DisplayService.gammaSunsetTime
Column {
id: sunsetColumn
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "wb_twilight"
size: Theme.iconSize
color: "#5C6BC0"
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunsetTime)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Sunset")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
width: parent.width
height: nextChangeRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
visible: DisplayService.gammaNextTransition
Row {
id: nextChangeRow
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Next Transition")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaNextTransition)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
}
}
}
}
}
}

View File

@@ -61,14 +61,6 @@ Item {
}
}
SettingsToggleRow {
text: I18n.tr("Prevent idle for media")
description: I18n.tr("Inhibit idle timeout when audio or video is playing")
checked: SettingsData.preventIdleForMedia
visible: IdleService.idleMonitorAvailable
onToggled: checked => SettingsData.set("preventIdleForMedia", checked)
}
SettingsToggleRow {
text: I18n.tr("Fade to lock screen")
description: I18n.tr("Gradually fade the screen before locking with a configurable grace period")
@@ -76,6 +68,14 @@ Item {
onToggled: checked => SettingsData.set("fadeToLockEnabled", checked)
}
SettingsToggleRow {
text: I18n.tr("Lock before suspend")
description: I18n.tr("Automatically lock the screen when the system prepares to suspend")
checked: SettingsData.lockBeforeSuspend
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
}
SettingsDropdownRow {
id: fadeGracePeriodDropdown
property var periodOptions: ["1 second", "2 seconds", "3 seconds", "4 seconds", "5 seconds", "10 seconds", "15 seconds", "20 seconds", "30 seconds"]
@@ -114,14 +114,14 @@ Item {
function onCurrentIndexChanged() {
const currentProfile = powerCategory.currentIndex === 0 ? SettingsData.acProfileName : SettingsData.batteryProfileName;
const index = powerProfileDropdown.profileValues.indexOf(currentProfile);
powerProfileDropdown.currentValue = powerProfileDropdown.profileOptions[index]
powerProfileDropdown.currentValue = powerProfileDropdown.profileOptions[index];
}
}
Component.onCompleted: {
const currentProfile = powerCategory.currentIndex === 0 ? SettingsData.acProfileName : SettingsData.batteryProfileName;
const index = profileValues.indexOf(currentProfile);
currentValue = profileOptions[index]
currentValue = profileOptions[index];
}
onValueChanged: value => {

View File

@@ -49,6 +49,7 @@ Singleton {
signal extWorkspaceStateUpdate(var data)
signal wlrOutputStateUpdate(var data)
signal evdevStateUpdate(var data)
signal gammaStateUpdate(var data)
signal openUrlRequested(string url)
signal appPickerRequested(var data)
@@ -267,9 +268,9 @@ Singleton {
function removeSubscription(service) {
if (activeSubscriptions.includes("all")) {
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser"]
const filtered = allServices.filter(s => s !== service)
subscribe(filtered)
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser"];
const filtered = allServices.filter(s => s !== service);
subscribe(filtered);
} else {
const filtered = activeSubscriptions.filter(s => s !== service);
if (filtered.length === 0) {
@@ -289,9 +290,9 @@ Singleton {
excludeServices = [excludeServices];
}
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser"]
const filtered = allServices.filter(s => !excludeServices.includes(s))
subscribe(filtered)
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser"];
const filtered = allServices.filter(s => !excludeServices.includes(s));
subscribe(filtered);
}
function handleSubscriptionEvent(response) {
@@ -355,16 +356,18 @@ Singleton {
if (data.capsLock !== undefined) {
capsLockState = data.capsLock;
}
evdevStateUpdate(data)
evdevStateUpdate(data);
} else if (service === "gamma") {
gammaStateUpdate(data);
} else if (service === "browser.open_requested") {
if (data.target) {
if (data.requestType === "url" || !data.requestType) {
openUrlRequested(data.target)
openUrlRequested(data.target);
} else {
appPickerRequested(data)
appPickerRequested(data);
}
} else if (data.url) {
openUrlRequested(data.url)
openUrlRequested(data.url);
}
}
}

View File

@@ -41,6 +41,18 @@ Singleton {
property bool automationAvailable: false
property bool gammaControlAvailable: false
property var gammaState: ({})
property int gammaCurrentTemp: gammaState?.currentTemp ?? 0
property string gammaNextTransition: gammaState?.nextTransition ?? ""
property string gammaSunriseTime: gammaState?.sunriseTime ?? ""
property string gammaSunsetTime: gammaState?.sunsetTime ?? ""
property string gammaDawnTime: gammaState?.dawnTime ?? ""
property string gammaNightTime: gammaState?.nightTime ?? ""
property bool gammaIsDay: gammaState?.isDay ?? true
property real gammaSunPosition: gammaState?.sunPosition ?? 0
property int gammaLowTemp: gammaState?.config?.LowTemp ?? 0
property int gammaHighTemp: gammaState?.config?.HighTemp ?? 0
function markDeviceUserControlled(deviceId) {
const newControlled = Object.assign({}, userControlledDevices);
newControlled[deviceId] = Date.now();
@@ -809,6 +821,10 @@ Singleton {
osdSuppressTimer.restart();
}
}
function onGammaStateUpdate(data) {
root.gammaState = data;
}
}
// Session Data Connections

View File

@@ -58,43 +58,12 @@ Singleton {
property var monitorOffMonitor: null
property var lockMonitor: null
property var suspendMonitor: null
property var mediaInhibitor: null
property var lockComponent: null
function wake() {
requestMonitorOn();
}
function createMediaInhibitor() {
if (!idleInhibitorAvailable) {
return;
}
if (mediaInhibitor) {
mediaInhibitor.destroy();
mediaInhibitor = null;
}
const inhibitorString = `
import QtQuick
import Quickshell.Wayland
IdleInhibitor {
active: false
}
`;
mediaInhibitor = Qt.createQmlObject(inhibitorString, root, "IdleService.MediaInhibitor");
mediaInhibitor.active = Qt.binding(() => root.mediaPlaying);
}
function destroyMediaInhibitor() {
if (mediaInhibitor) {
mediaInhibitor.destroy();
mediaInhibitor = null;
}
}
function createIdleMonitors() {
if (!idleMonitorAvailable) {
console.info("IdleService: IdleMonitor not available, skipping creation");
@@ -152,10 +121,6 @@ Singleton {
root.requestSuspend();
}
});
if (SettingsData.preventIdleForMedia) {
createMediaInhibitor();
}
} catch (e) {
console.warn("IdleService: Error creating IdleMonitors:", e);
}
@@ -176,17 +141,6 @@ Singleton {
}
}
Connections {
target: SettingsData
function onPreventIdleForMediaChanged() {
if (SettingsData.preventIdleForMedia) {
createMediaInhibitor();
} else {
destroyMediaInhibitor();
}
}
}
Component.onCompleted: {
if (!idleMonitorAvailable) {
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.");

View File

@@ -1,8 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Widgets
@@ -10,7 +8,7 @@ Rectangle {
id: root
property string currentIcon: ""
property string iconType: "icon" // "icon" or "text"
property string iconType: "icon"
signal iconSelected(string iconName, string iconType)
@@ -18,40 +16,73 @@ Rectangle {
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: dropdownLoader.active ? Theme.primary : Theme.outline
border.color: iconPopup.visible ? Theme.primary : Theme.outline
border.width: 1
property var iconCategories: [{
property var iconCategories: [
{
"name": I18n.tr("Numbers"),
"icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"]
}, {
},
{
"name": I18n.tr("Workspace"),
"icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"]
}, {
},
{
"name": I18n.tr("Development"),
"icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"]
}, {
},
{
"name": I18n.tr("Communication"),
"icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"]
}, {
},
{
"name": I18n.tr("Media"),
"icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"]
}, {
},
{
"name": I18n.tr("System"),
"icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"]
}, {
},
{
"name": I18n.tr("Navigation"),
"icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"]
}, {
},
{
"name": I18n.tr("Actions"),
"icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"]
}, {
},
{
"name": I18n.tr("Status"),
"icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"]
}, {
},
{
"name": I18n.tr("Fun"),
"icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"]
}]
}
]
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (iconPopup.visible) {
iconPopup.close();
return;
}
const pos = root.mapToItem(Overlay.overlay, 0, 0);
const popupHeight = 500;
const overlayHeight = Overlay.overlay?.height ?? 800;
iconPopup.x = pos.x;
if (pos.y + root.height + popupHeight + 4 > overlayHeight) {
iconPopup.y = pos.y - popupHeight - 4;
} else {
iconPopup.y = pos.y + root.height + 4;
}
iconPopup.open();
}
}
Row {
anchors.left: parent.left
@@ -73,18 +104,11 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 160
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
onClicked: {
dropdownLoader.active = !dropdownLoader.active
}
}
}
}
DankIcon {
name: dropdownLoader.active ? "expand_less" : "expand_more"
name: iconPopup.visible ? "expand_less" : "expand_more"
size: 16
color: Theme.outline
anchors.right: parent.right
@@ -92,185 +116,126 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
}
Loader {
id: dropdownLoader
active: false
asynchronous: true
Popup {
id: iconPopup
sourceComponent: PanelWindow {
id: dropdownPopup
parent: Overlay.overlay
width: 320
height: Math.min(500, dropdownContent.implicitHeight + 32)
padding: 0
modal: true
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
visible: true
implicitWidth: 320
implicitHeight: Math.min(500, dropdownContent.implicitHeight + 32)
background: Rectangle {
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
}
anchors {
top: true
left: true
right: true
bottom: true
}
contentItem: Rectangle {
color: Theme.surface
radius: Theme.cornerRadius
// Top area - above popup
MouseArea {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: popupContainer.y
onClicked: {
dropdownLoader.active = false
}
}
// Bottom area - below popup
MouseArea {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: popupContainer.bottom
anchors.bottom: parent.bottom
onClicked: {
dropdownLoader.active = false
}
}
// Left area - left of popup
MouseArea {
anchors.left: parent.left
anchors.top: popupContainer.top
anchors.bottom: popupContainer.bottom
width: popupContainer.x
onClicked: {
dropdownLoader.active = false
}
}
// Right area - right of popup
MouseArea {
anchors.right: parent.right
anchors.top: popupContainer.top
anchors.bottom: popupContainer.bottom
anchors.left: popupContainer.right
onClicked: {
dropdownLoader.active = false
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Theme.shadowStrong
shadowBlur: 0.8
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
}
Rectangle {
id: popupContainer
width: 320
height: Math.min(500, dropdownContent.implicitHeight + 32)
x: Math.max(16, Math.min(root.mapToItem(null, 0, 0).x, parent.width - width - 16))
y: Math.max(16, Math.min(root.mapToItem(null, 0, root.height + 4).y, parent.height - height - 16))
radius: Theme.cornerRadius
color: Theme.surface
width: 24
height: 24
radius: 12
color: closeMouseArea.containsMouse ? Theme.errorHover : "transparent"
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
z: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Theme.shadowStrong
shadowBlur: 0.8
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
DankIcon {
name: "close"
size: 16
color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
// Close button
Rectangle {
width: 24
height: 24
radius: 12
color: closeMouseArea.containsMouse ? Theme.errorHover : "transparent"
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
z: 1
DankIcon {
name: "close"
size: 16
color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
dropdownLoader.active = false
}
}
}
DankFlickable {
MouseArea {
id: closeMouseArea
anchors.fill: parent
anchors.margins: Theme.spacingS
contentHeight: dropdownContent.height
clip: true
pressDelay: 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: iconPopup.close()
}
}
Column {
id: dropdownContent
width: parent.width
spacing: Theme.spacingM
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingS
contentHeight: dropdownContent.height
clip: true
pressDelay: 0
// Icon categories
Repeater {
model: root.iconCategories
Column {
id: dropdownContent
width: parent.width
spacing: Theme.spacingM
Column {
Repeater {
model: root.iconCategories
Column {
required property var modelData
width: parent.width
spacing: Theme.spacingS
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
Flow {
width: parent.width
spacing: Theme.spacingS
spacing: 4
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
Repeater {
model: modelData.icons
Flow {
width: parent.width
spacing: 4
Rectangle {
required property string modelData
width: 36
height: 36
radius: Theme.cornerRadius
color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: 2
Repeater {
model: modelData.icons
DankIcon {
name: parent.modelData
size: 20
color: root.currentIcon === parent.modelData ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: 2
DankIcon {
name: modelData
size: 20
color: root.currentIcon === modelData ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
MouseArea {
id: iconMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.iconSelected(parent.modelData, "icon");
iconPopup.close();
}
}
MouseArea {
id: iconMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.iconSelected(modelData, "icon")
dropdownLoader.active = false
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -284,8 +249,8 @@ Rectangle {
}
function setIcon(iconName, type) {
root.iconType = type
root.iconType = "icon"
root.currentIcon = iconName
root.iconType = type;
root.iconType = "icon";
root.currentIcon = iconName;
}
}

View File

@@ -433,7 +433,7 @@ Item {
Loader {
id: contentLoader
anchors.fill: parent
active: contentWindow.visible
active: shouldBeVisible || contentWindow.visible
asynchronous: false
}
}

View File

@@ -880,26 +880,18 @@ Item {
}
RowLayout {
id: dmsArgsRow
Layout.fillWidth: true
spacing: Theme.spacingM
property var dmsArgConfig: {
const action = root.editAction;
if (!action)
return null;
if (action.indexOf("audio increment") !== -1 || action.indexOf("audio decrement") !== -1 || action.indexOf("brightness increment") !== -1 || action.indexOf("brightness decrement") !== -1) {
const parts = action.split(" ");
const lastPart = parts[parts.length - 1];
const hasAmount = /^\d+$/.test(lastPart);
return {
hasAmount: hasAmount,
amount: hasAmount ? lastPart : ""
};
}
return null;
}
readonly property var argConfig: Actions.getActionArgConfig(root.editAction)
readonly property var parsedArgs: argConfig?.type === "dms" ? Actions.parseDmsActionArgs(root.editAction) : null
readonly property var dmsActionArgs: Actions.getDmsActionArgs()
readonly property bool hasAmountArg: parsedArgs?.base ? (dmsActionArgs?.[parsedArgs.base]?.args?.some(a => a.name === "amount") ?? false) : false
readonly property bool hasDeviceArg: parsedArgs?.base ? (dmsActionArgs?.[parsedArgs.base]?.args?.some(a => a.name === "device") ?? false) : false
readonly property bool hasTabArg: parsedArgs?.base ? (dmsActionArgs?.[parsedArgs.base]?.args?.some(a => a.name === "tab") ?? false) : false
visible: root._actionType === "dms" && dmsArgConfig !== null
visible: root._actionType === "dms" && argConfig?.type === "dms"
StyledText {
text: I18n.tr("Amount")
@@ -907,26 +899,36 @@ Item {
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
visible: dmsArgsRow.hasAmountArg
}
DankTextField {
id: dmsAmountField
Layout.preferredWidth: 80
Layout.preferredHeight: 40
placeholderText: "5"
text: parent.dmsArgConfig?.amount || ""
onTextChanged: {
if (!parent.dmsArgConfig)
visible: dmsArgsRow.hasAmountArg
Connections {
target: dmsArgsRow
function onParsedArgsChanged() {
const newText = dmsArgsRow.parsedArgs?.args?.amount || "";
if (dmsAmountField.text !== newText)
dmsAmountField.text = newText;
}
}
Component.onCompleted: {
text = dmsArgsRow.parsedArgs?.args?.amount || "";
}
onEditingFinished: {
if (!dmsArgsRow.parsedArgs)
return;
const action = root.editAction;
const parts = action.split(" ");
const lastPart = parts[parts.length - 1];
const hasOldAmount = /^\d+$/.test(lastPart);
if (hasOldAmount)
parts.pop();
if (text && /^\d+$/.test(text))
parts.push(text);
const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
newArgs.amount = text || "5";
root.updateEdit({
action: parts.join(" ")
action: Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
});
}
}
@@ -935,10 +937,105 @@ Item {
text: "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: dmsArgsRow.hasAmountArg
}
StyledText {
text: I18n.tr("Device")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0
Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : 60
visible: dmsArgsRow.hasDeviceArg
}
DankTextField {
id: dmsDeviceField
Layout.fillWidth: true
Layout.preferredHeight: 40
placeholderText: I18n.tr("leave empty for default")
visible: dmsArgsRow.hasDeviceArg
Connections {
target: dmsArgsRow
function onParsedArgsChanged() {
const newText = dmsArgsRow.parsedArgs?.args?.device || "";
if (dmsDeviceField.text !== newText)
dmsDeviceField.text = newText;
}
}
Component.onCompleted: {
text = dmsArgsRow.parsedArgs?.args?.device || "";
}
onEditingFinished: {
if (!dmsArgsRow.parsedArgs)
return;
const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
newArgs.device = text;
root.updateEdit({
action: Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
});
}
}
Item {
Layout.fillWidth: true
visible: !dmsArgsRow.hasDeviceArg && !dmsArgsRow.hasTabArg
}
StyledText {
text: I18n.tr("Tab")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
visible: dmsArgsRow.hasTabArg
}
DankDropdown {
id: dmsTabDropdown
Layout.fillWidth: true
compactMode: true
visible: dmsArgsRow.hasTabArg
currentValue: {
const tab = dmsArgsRow.parsedArgs?.args?.tab || "";
switch (tab) {
case "media":
return I18n.tr("Media");
case "wallpaper":
return I18n.tr("Wallpaper");
case "weather":
return I18n.tr("Weather");
default:
return I18n.tr("Overview");
}
}
options: [I18n.tr("Overview"), I18n.tr("Media"), I18n.tr("Wallpaper"), I18n.tr("Weather")]
onValueChanged: value => {
if (!dmsArgsRow.parsedArgs)
return;
const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
switch (value) {
case I18n.tr("Media"):
newArgs.tab = "media";
break;
case I18n.tr("Wallpaper"):
newArgs.tab = "wallpaper";
break;
case I18n.tr("Weather"):
newArgs.tab = "weather";
break;
default:
newArgs.tab = "";
break;
}
root.updateEdit({
action: Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
});
}
}
}