1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00

Compare commits

...

52 Commits

Author SHA1 Message Date
Connor Welsh 11287459c3 popout: re-apply surface geometry after shouldBeVisible (#2983)
Popouts sized from their content sometimes open too short and clip what
they render, then come back correct on the next open.

open() snapshots the surface geometry before setting shouldBeVisible,
and the onAligned{X,Y,Width,Height}Changed handlers are all gated on
that flag. Mapping contentWindow happens in between, and that's what
finally runs the layout Qt deferred while the window was hidden.
Content-sized popouts pick up their real height in that gap, so the
correction is dropped and the surface stays short. Re-snapshot once the
flag is set.
2026-08-02 01:30:21 -04:00
bbedward ef191babb7 tailscale: add missing ID
related #2969
port 1.5
2026-07-31 09:37:01 -04:00
bbedward fe64a342f9 network: rescan when network panel is open 2026-07-31 09:33:17 -04:00
bbedward e54be7d12d i18n: sync 2026-07-30 17:11:29 -04:00
bbedward 81c886784b lock: add separate text input for IMEs
related #2950
2026-07-30 17:08:59 -04:00
bbedward 6682bb120c matugen: tweak qt6ct colors
related #2951
2026-07-30 17:08:59 -04:00
bbedward 5c02ec4789 lock: avoid lock at startup firing when using dms-greeter
fixes #2952
2026-07-30 17:08:59 -04:00
bbedward de1e1757c3 keybinds: preserve raw lua actions in parser
fixes #2953
2026-07-30 17:08:59 -04:00
bbedward 43d331d6cf desktop plugins: ensure transparent frame is shown when desktop widgets
disabled

related #2955
2026-07-30 17:08:59 -04:00
Grant Mosha 01832856d4 fix(network): authenticate Fortinet VPNs natively (#2959)
* fix(network): authenticate Fortinet VPNs natively

* cleanup and add test

---------

Co-authored-by: Grant Mosha <grant.mosha@nida.go.tz>
Co-authored-by: bbedward <bbedward@gmail.com>
2026-07-30 16:45:53 -04:00
Huỳnh Thiện Lộc 0033e3f0e0 feat: add QR Generator launcher (#2941)
* feat: add QR Generator modal with auto-generate on input debounce

- QRGeneratorModal with dual-buffer sequential fade animation
- Auto-generate QR code on typing stop (200ms debounce)
- Clear text button on input field
- Escape key closes modal
- Register modal in PopoutService and AppSearchService
- Go backend for text QR code generation

* fix: add QR Generator toggle to Launcher DMS settings

* qr: also add qrg built in launcher plugin

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-07-30 16:33:36 -04:00
bbedward 2d3706321a frame: fix popout IPCs in some situations
related #2956
2026-07-30 15:30:44 -04:00
bbedward 5bb884db57 keybinds: fix labeling of spawn keybinds
fixes #2957
2026-07-30 15:30:44 -04:00
dms-ci[bot] b42763ccbf nix: update vendorHash for go.mod changes 2026-07-30 19:13:28 +00:00
bbedward f2a6d62d65 matugen: add description for vscode template
fixes #2958
2026-07-30 15:10:54 -04:00
bbedward f66693df6b core: bump dankgo 2026-07-30 14:52:14 -04:00
bbedward a710d6d7cc launcher: fix regex escaping launch args
fixes #2961
port 1.5
2026-07-30 14:33:58 -04:00
bbedward 64461c534f qs/socket: improve resilience of socket detection and connection
related #2369
port 1.5
2026-07-27 13:40:34 -04:00
bbedward 73da4879f6 frame: consolidate and simplify inset logic 2026-07-27 13:10:47 -04:00
euletheia 8594a414ec fix(BatteryService): fix typo in notification check (#2944) 2026-07-27 11:33:04 -04:00
euletheia df396bfa43 fix(BatteryService): fix unresolved icons for notifications and dismiss alerts when plugged in (#2943)
* feat(BatteryService): dismiss sticky critical battery notifications

If critical notifications do not timeout, dismiss the battery-related
ones automatically when battery is plugged in.

* fix(BatteryService): use material icons for battery alerts

Since the previously specified icons are not resolved, instead reuse
icons from DMS first-party plugin for critical battery alerts.
2026-07-27 11:13:47 -04:00
euletheia 33677150b1 feat(ipc): add 'dismiss' function to notifications ipc (#2942)
Allow to dismiss the last visible notification only instead of resorting
directly to dismissAllPopups.
2026-07-27 11:13:07 -04:00
bbedward c67b185076 qs: large sweep of dead code removals 2026-07-26 23:22:22 -04:00
bbedward bab2078dfd cups: fix sub/unsub race
port 1.5
2026-07-26 19:43:17 -04:00
bbedward e9bc0169f6 cc: migrate some settings to cache
port 1.5
2026-07-26 17:45:50 -04:00
bbedward 59c83d19e4 battery: auto-apply power profile at startup when DMS managed
fixes #2935
port 1.5
2026-07-26 15:27:07 -04:00
bbedward 9b7d3c64fe dbar: fix click-through mask of center section
fixes #2938
2026-07-26 15:16:48 -04:00
bbedward c367153bac sysupdate: fix gnome-terminal title 2026-07-26 15:00:40 -04:00
bbedward 1f94c3cbd4 dbar/notifications: fix right click context menu position and add middle
click to DnD
2026-07-26 14:51:51 -04:00
bbedward 564583951c theme: stop overwriting user gtk.css and dconf gtk-theme (#2933) 2026-07-26 14:48:23 -04:00
bbedward eed3617a0d ipc/dash: add openAt/toggleAt for opening popouts at specific locations
fixes #2932
2026-07-26 14:37:17 -04:00
bbedward 24cb3d19a0 display config: preserve config when turning off on niri
fixes #2939
port 1.5
2026-07-26 14:26:00 -04:00
feng-yifan 3182c70857 fix(settings): reset AppBrowserPopup model on close to avoid crash (#2925)
Reopening the autostart "Browse" picker crashed in
QQmlIncubatorPrivate::incubate. Drop the ListView model on hide()
and rebind it on show() so each open starts with a fresh
QQmlDelegateModel instead of reusing a stale incubation queue.
2026-07-26 14:24:21 -04:00
dms-ci[bot] 8bb7396362 nix: sync flake.lock to dank-qml-common submodule 2026-07-24 23:36:44 +00:00
purian23 62648ed75a Bump dank-qml-common: revert CachingImage cache change 2026-07-24 19:36:08 -04:00
bbedward fa629fbf99 dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926) 2026-07-24 15:45:57 -04:00
ArijanJ 7222e072af osd: click to hide some osds (#2927) 2026-07-24 15:19:47 -04:00
bbedward f57f254855 notification: clip region on animation exit
related #2917
port 1.5
2026-07-24 14:35:14 -04:00
bbedward fa094db127 lock: add completed guard in fade to dpms window
related #2885
2026-07-24 14:12:26 -04:00
bbedward 77a357109a bluetooth: add soft rfkill unblock, wait for adapter to become available
instead of forfeiting subscription

fixes #2922
fixes #1537
2026-07-24 11:28:24 -04:00
dms-ci[bot] 295657492d nix: update vendorHash for go.mod changes 2026-07-24 14:52:12 +00:00
bbedward 2bafc21229 gamma: replace login1 scheduler re-calculation with suspend-aware
boottimer
2026-07-24 10:49:37 -04:00
bbedward ea03fb2788 cups: fix initialization race
port 1.5
2026-07-24 10:42:27 -04:00
bbedward 0815e48465 dock: limit input mask to body, disable hover headroom unless enabled
fixes #2923
2026-07-24 10:31:51 -04:00
bbedward 42060366cb network: fix networkPreference reverting from eth to wifi
fixes #2919
port 1.5
2026-07-24 10:10:59 -04:00
bbedward 8af71036cc frame: merge bars with frame window and get things on Top layer by
default
2026-07-23 14:10:17 -04:00
bbedward 5854327c28 settings: fix close IPC
fixes #2912
port 1.5
2026-07-23 14:09:59 -04:00
euletheia 99f452c999 feat(Themes): add support for GTK3 dynamic theming (#2523)
* feat(Themes): add support for GTK3 dynamic theming

Instead of overriding named colors of all GTK3 themes via the config
directory file, only override named colors for each variant of the
adw-gtk3 theme (light and dark), and let GTK3 apps change themes
depending on the value of org.gnome.desktop.interface color-scheme.

* fix(Themes): apply requested changes

- remove gtk3 override if it's a symlink
- remove call to gtk.sh with old arg 'assets-only'
- append key to dconf path to respect dconf write signature

* refactor(Themes): separate 'include'/'apply' logic for GTK3 dynamic theming

* revert(Themes): reset timer between color-scheme refresh

- rename DMS_ENABLE_GTK4_REFRESH to DMS_ENABLE_GTK_REFRESH
- revert a3fba8

* feat(Themes): add 'remove' mode to gtk.sh

Allow to remove all inclusions and overrides for GTK.

* fix(Themes): backup adw-gtk3 original css in 'apply' (not 'include')

* style(Themes): rename argument 'include' to 'patch' for clarity
2026-07-23 13:31:39 -04:00
bbedward d818067bee calendar/dcal: fix flatpak socket detection
port 1.5
2026-07-23 10:14:26 -04:00
Huỳnh Thiện Lộc 5538913320 feat(settings): add option to suppress config reload toast (#2730) (#2915)
* feat(settings): add option to suppress config reload toast (#2730)

Add a toggle in Settings → Compositor Layout to disable the 'config reloaded' toast for Niri and MangoWC compositors. Session setting showConfigReloadToast defaults to true.

* chore: update settings search index for config reload toast

* refactor(settings): rename 'Compositor Notifications' to 'Compositor Toasts'

* fix(settings): reuse existing I18n.tr('Notifications') translation
2026-07-23 10:12:39 -04:00
Thomas Kroll 8188ced955 Add option to ignore app-requested notification timeout (#2909)
* Add option to ignore app-requested notification timeout

Notifications may include an expireTimeout hint that currently takes
precedence over the configured Low/Normal/Critical durations, so
popups can disappear earlier (or later) than expected with no
interaction involved. Add notificationIgnoreAppTimeout (default off,
matching the notification spec) to let users force their own
durations regardless of what the app requests.

* Update settings search index for new timeout override toggle
2026-07-23 08:32:14 -04:00
Huỳnh Thiện Lộc 75eac83b47 feat(settings): add Mouse & Touchpad settings tab for Niri (#2905)
* feat(settings): add Mouse & Trackpad settings tab

* style(settings): clarify touchpad natural scroll description

* chore(settings): update settings search index

* refactor(settings): unify Mouse & Trackpad terminology to Mouse & Touchpad

* feat(setup): deploy default dms/input.kdl on setup

* style(niri): restore include section comment in base config template

* refactor(niri): remove redundant input block from base config template

* refactor(niri): restore keyboard and non-mouse input config to base template

* feat(settings): add touchpad disable-on-external-mouse toggle

* feat(settings): add left-handed, scroll-factor and dwt options

* feat(settings): add middle-emulation, scroll-method, tap-and-drag and drag-lock options

* refactor(settings): re-order mouse and touchpad UI options logically
2026-07-22 13:28:40 -04:00
236 changed files with 16575 additions and 11258 deletions
+1
View File
@@ -40,6 +40,7 @@ var resolveIncludeCmd = &cobra.Command{
"layout.kdl", "layout.kdl",
"outputs.kdl", "outputs.kdl",
"binds.kdl", "binds.kdl",
"input.kdl",
"cursor.conf", "cursor.conf",
"layout.conf", "layout.conf",
"outputs.conf", "outputs.conf",
+4
View File
@@ -143,6 +143,10 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
niriFile: "alttab.kdl", niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig }, niriContent: func(_ string) string { return config.NiriAlttabConfig },
}, },
"input": {
niriFile: "input.kdl",
niriContent: func(_ string) string { return config.NiriInputConfig },
},
"outputs": { "outputs": {
niriFile: "outputs.kdl", niriFile: "outputs.kdl",
hyprFile: "outputs.lua", hyprFile: "outputs.lua",
+1 -1
View File
@@ -69,7 +69,7 @@ require (
) )
require ( require (
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
+2 -2
View File
@@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d h1:EZN2x2uAX975Uvw4MByqWaaVTp2fOJrNTl4V4EQbSn8= github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b h1:UwX1H4BkzazL7ips9ljnHzBXGttYARcIi5Njj9aqIt4=
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d/go.mod h1:7p7cfydr4WM1G6eOPFlANXF3IV5du3FoA4CbDPprHAo= github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
+1
View File
@@ -264,6 +264,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
{"layout.kdl", NiriLayoutConfig}, {"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig}, {"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)}, {"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"input.kdl", NiriInputConfig},
{"outputs.kdl", ""}, {"outputs.kdl", ""},
{"cursor.kdl", ""}, {"cursor.kdl", ""},
{"windowrules.kdl", ""}, {"windowrules.kdl", ""},
@@ -0,0 +1,15 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
input {
mouse {
accel-speed 0.0
}
touchpad {
tap
accel-speed 0.0
natural-scroll
}
}
+2 -25
View File
@@ -34,31 +34,6 @@ input {
numlock numlock
} }
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint { trackpoint {
// off // off
// natural-scroll // natural-scroll
@@ -77,6 +52,7 @@ input {
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen. // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%" // focus-follows-mouse max-scroll-amount="0%"
} }
// You can configure outputs by their name, which you can find // You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance. // by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1". // The built-in laptop monitor is usually called "eDP-1".
@@ -271,3 +247,4 @@ include optional=true "dms/alttab.kdl"
include optional=true "dms/binds.kdl" include optional=true "dms/binds.kdl"
include optional=true "dms/outputs.kdl" include optional=true "dms/outputs.kdl"
include optional=true "dms/cursor.kdl" include optional=true "dms/cursor.kdl"
include optional=true "dms/input.kdl"
+3
View File
@@ -16,3 +16,6 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl //go:embed embedded/niri-binds.kdl
var NiriBindsConfig string var NiriBindsConfig string
//go:embed embedded/niri-input.kdl
var NiriInputConfig string
+45 -5
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -224,6 +225,44 @@ func (h *HyprlandProvider) validateAction(action string) error {
return nil return nil
} }
var luaExprActionPattern = regexp.MustCompile(`^(function\s*\(|hl\.)`)
// isRawLuaActionText reports that action is a Lua expression to re-emit
// verbatim rather than freeform dispatcher text to wrap for hyprctl. The
// balance check keeps malformed input from corrupting the generated file.
func isRawLuaActionText(action string) bool {
if !luaExprActionPattern.MatchString(action) {
return false
}
depth := 0
var quote byte
escaped := false
for i := 0; i < len(action); i++ {
c := action[i]
switch {
case escaped:
escaped = false
case quote != 0:
switch c {
case '\\':
escaped = true
case quote:
quote = 0
}
case c == '"' || c == '\'':
quote = c
case c == '(':
depth++
case c == ')':
depth--
if depth < 0 {
return false
}
}
}
return depth == 0 && quote == 0 && !escaped
}
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error { func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.ensureWritableConfig(); err != nil { if err := h.ensureWritableConfig(); err != nil {
return err return err
@@ -254,11 +293,12 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
canonicalKey := canonicalHyprlandOverrideKey(key) canonicalKey := canonicalHyprlandOverrideKey(key)
normalizedKey := hyprlandOverrideMapKey(canonicalKey) normalizedKey := hyprlandOverrideMapKey(canonicalKey)
existingBinds[normalizedKey] = &hyprlandOverrideBind{ existingBinds[normalizedKey] = &hyprlandOverrideBind{
Key: canonicalKey, Key: canonicalKey,
Action: action, Action: action,
Description: description, Description: description,
Flags: flags, Flags: flags,
Options: options, Options: options,
RawLuaAction: isRawLuaActionText(action),
} }
return h.writeOverrideBinds(existingBinds) return h.writeOverrideBinds(existingBinds)
@@ -463,6 +463,56 @@ func TestHyprlandSetBindLeavesConfOnlyInstallReadOnly(t *testing.T) {
} }
} }
func TestIsRawLuaActionText(t *testing.T) {
cases := []struct {
action string
want bool
}{
{`function() hl.plugin.scrolloverview.overview("toggle") end`, true},
{`hl.dsp.exec_cmd("foo")`, true},
{`hl.dsp.no_op()`, true},
{"workspace 3", false},
{"exec zeditor", false},
{`function() hl.foo( end`, false},
{`function() hl.foo("a) end`, false},
{`hl.foo()) hl.bar((`, false},
}
for _, tc := range cases {
if got := isRawLuaActionText(tc.action); got != tc.want {
t.Errorf("isRawLuaActionText(%q) = %v, want %v", tc.action, got, tc.want)
}
}
}
func TestHyprlandSetBindPreservesRawLuaAction(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte("-- DMS user keybind overrides\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
rawAction := `function() hl.plugin.scrolloverview.overview("toggle") end`
if err := provider.SetBind("SUPER + G", rawAction, "Toggle overview", nil); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
if err != nil {
t.Fatal(err)
}
got := string(data)
if !strings.Contains(got, `hl.bind("SUPER + G", `+rawAction) {
t.Fatalf("expected raw Lua action to be written verbatim, got:\n%s", got)
}
if strings.Contains(got, "hyprctl dispatch function") {
t.Fatalf("expected raw Lua action to not be wrapped in hyprctl dispatch, got:\n%s", got)
}
}
func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) { func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms") dmsDir := filepath.Join(tmpDir, "dms")
+53 -26
View File
@@ -386,8 +386,8 @@ func buildOnce(opts *Options) (bool, error) {
default: default:
syncAccentColor(primaryDark) syncAccentColor(primaryDark)
} }
refreshGTK(opts.Mode) refreshGTKTheme(opts.Mode)
refreshGTK4() refreshGTKColorScheme()
} }
if isDMSKDEColorSchemeActive(opts.ConfigDir) { if isDMSKDEColorSchemeActive(opts.ConfigDir) {
@@ -471,12 +471,9 @@ output_path = '%s'
case TemplateKindTerminal: case TemplateKindTerminal:
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
case TemplateKindVSCode: case TemplateKindVSCode:
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir) for _, editor := range vscodeEditors {
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir) }
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs: case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" { if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
@@ -633,6 +630,23 @@ func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []stri
return false return false
} }
type vscodeEditor struct {
name string
dataDir string
}
var vscodeEditors = []vscodeEditor{
{"vscode", ".vscode"},
{"codium", ".vscode-oss"},
{"cursor", ".cursor"},
{"windsurf", ".windsurf"},
{"vscode-insiders", ".vscode-insiders"},
}
func (e vscodeEditor) extensionsDir(homeDir string) string {
return filepath.Join(homeDir, e.dataDir, "extensions")
}
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) { func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*") pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
matches, err := filepath.Glob(pattern) matches, err := filepath.Glob(pattern)
@@ -911,7 +925,7 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
} }
func isDMSGTKActive(configDir string) bool { func isDMSGTKActive(configDir string) bool {
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css") gtkCSS := filepath.Join(configDir, "gtk-4.0", "gtk.css")
info, err := os.Lstat(gtkCSS) info, err := os.Lstat(gtkCSS)
if err != nil { if err != nil {
@@ -968,11 +982,32 @@ func applyKDEColorScheme(mode ColorMode) {
} }
} }
func refreshGTK(mode ColorMode) { func gtkThemeInstalled(theme string) bool {
home, _ := os.UserHomeDir()
candidates := []string{
filepath.Join(home, ".local/share/themes", theme),
filepath.Join(home, ".themes", theme),
filepath.Join("/usr/share/themes", theme),
filepath.Join("/usr/local/share/themes", theme),
}
for _, dir := range candidates {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
return true
}
}
return false
}
func refreshGTKTheme(mode ColorMode) {
theme := mode.GTKTheme()
if !gtkThemeInstalled(theme) {
log.Infof("Skipping gtk-theme refresh: %s is not installed", theme)
return
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err) log.Warnf("Failed to reset gtk-theme: %v", err)
} }
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", theme); err != nil {
log.Warnf("Failed to set gtk-theme: %v", err) log.Warnf("Failed to set gtk-theme: %v", err)
} }
} }
@@ -993,8 +1028,8 @@ func expectColorSchemeEcho(scheme string) {
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps // apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
// following the portal color-scheme (Chromium) can drop the restore signal // following the portal color-scheme (Chromium) can drop the restore signal
// mid-repaint and latch the wrong mode, so this is opt-in. // mid-repaint and latch the wrong mode, so this is opt-in.
func refreshGTK4() { func refreshGTKColorScheme() {
if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" { if os.Getenv("DMS_ENABLE_GTK_REFRESH") != "1" {
return return
} }
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
@@ -1012,13 +1047,13 @@ func refreshGTK4() {
expectColorSchemeEcho(toggle) expectColorSchemeEcho(toggle)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err) log.Warnf("Failed to toggle color-scheme for GTK refresh: %v", err)
return return
} }
time.Sleep(400 * time.Millisecond) time.Sleep(50 * time.Millisecond)
expectColorSchemeEcho(current) expectColorSchemeEcho(current)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err) log.Warnf("Failed to restore color-scheme for GTK refresh: %v", err)
} }
} }
@@ -1147,16 +1182,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
} }
func checkVSCodeExtension(homeDir string) bool { func checkVSCodeExtension(homeDir string) bool {
extDirs := []string{ for _, editor := range vscodeEditors {
filepath.Join(homeDir, ".vscode/extensions"), pattern := filepath.Join(editor.extensionsDir(homeDir), "danklinux.dms-theme-*")
filepath.Join(homeDir, ".vscode-oss/extensions"),
filepath.Join(homeDir, ".config/Code - OSS/extensions"),
filepath.Join(homeDir, ".cursor/extensions"),
filepath.Join(homeDir, ".windsurf/extensions"),
}
for _, extDir := range extDirs {
pattern := filepath.Join(extDir, "danklinux.dms-theme-*")
if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 { if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 {
return true return true
} }
@@ -22,6 +22,16 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") { if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") {
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control") t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
} }
// Wayland IMEs commit unconsumed printable keys as text-input text rather
// than forwarding raw keys, so the lock screen needs an IME commit sink
// alongside raw key handling.
if !strings.Contains(content, "id: imeCommitSink") {
t.Fatalf("passwordField must keep the imeCommitSink TextInput so IME-routed keyboards can type (#2950)")
}
if !strings.Contains(content, "Qt.ImhSensitiveData") {
t.Fatalf("imeCommitSink must advertise hidden-text hints so IMEs treat it as a password field")
}
} }
func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) { func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
+11 -6
View File
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
m.promptBroker = broker m.promptBroker = broker
adapter, err := m.findAdapter() adapter, err := findAdapter(conn)
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, fmt.Errorf("no bluetooth adapter found: %w", err) return nil, err
} }
m.adapterPath = adapter m.adapterPath = adapter
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func (m *Manager) findAdapter() (dbus.ObjectPath, error) { func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/")) obj := conn.Object(bluezService, dbus.ObjectPath("/"))
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
return "", err return "", fmt.Errorf("%w: %v", ErrNoAdapter, err)
} }
for path, interfaces := range objects { for path, interfaces := range objects {
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
} }
} }
return "", fmt.Errorf("no adapter found") return "", ErrNoAdapter
} }
func (m *Manager) initialize() error { func (m *Manager) initialize() error {
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error {
} }
func (m *Manager) SetPowered(powered bool) error { func (m *Manager) SetPowered(powered bool) error {
if powered {
if err := rfkillUnblockBluetooth(); err != nil {
log.Debugf("[BluezManager] rfkill unblock failed: %v", err)
}
}
obj := m.dbusConn.Object(bluezService, m.adapterPath) obj := m.dbusConn.Object(bluezService, m.adapterPath)
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
} }
@@ -0,0 +1,5 @@
package bluez
func rfkillUnblockBluetooth() error {
return nil
}
@@ -0,0 +1,19 @@
package bluez
import "os"
// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; },
// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3
func rfkillUnblockBluetooth() error {
f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()
var event [8]byte
event[4] = 2
event[5] = 3
_, err = f.Write(event[:])
return err
}
+46
View File
@@ -0,0 +1,46 @@
package bluez
import (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
)
var ErrNoAdapter = errors.New("no bluetooth adapter found")
func WaitForAdapter() error {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return err
}
defer conn.Close()
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(objectMgrIface),
dbus.WithMatchMember("InterfacesAdded"),
); err != nil {
return err
}
signals := make(chan *dbus.Signal, 64)
conn.Signal(signals)
if _, err := findAdapter(conn); err == nil {
return nil
}
for sig := range signals {
if sig == nil || sig.Name != objectMgrIface+".InterfacesAdded" || len(sig.Body) < 2 {
continue
}
ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant)
if !ok {
continue
}
if _, ok := ifaces[adapter1Iface]; ok {
return nil
}
}
return fmt.Errorf("dbus signal stream closed")
}
+23 -10
View File
@@ -229,6 +229,9 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
wasEmpty := true wasEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
wasEmpty = false wasEmpty = false
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
m.subscribers.Store(id, ch) m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if !wasEmpty || m.subscription == nil {
if err := m.subscription.Start(); err != nil { return ch
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
} else {
m.eventWG.Add(1)
go m.eventHandler()
}
} }
if err := m.subscription.Start(); err != nil {
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
return ch
}
m.eventWG.Add(1)
go m.eventHandler()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
if val, ok := m.subscribers.LoadAndDelete(id); ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val) close(val)
} }
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
return false return false
}) })
if isEmpty && m.subscription != nil { if !isEmpty || m.subscription == nil {
m.subscription.Stop() return
m.eventWG.Wait()
} }
m.subscription.Stop()
m.eventWG.Wait()
} }
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subLifecycleMu.Lock()
if m.subscription != nil { if m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
} }
m.subLifecycleMu.Unlock()
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
+64
View File
@@ -1,6 +1,9 @@
package cups package cups
import ( import (
"errors"
"fmt"
"sync"
"testing" "testing"
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
assert.Equal(t, 0, count) assert.Equal(t, 0, count)
} }
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
// unsynchronized so overlapping Start/Stop trips the race detector
type stubSubscription struct {
mu sync.Mutex
events chan SubscriptionEvent
conn *int
running bool
}
func (s *stubSubscription) Start() error {
if s.running {
return errors.New("already running")
}
s.running = true
s.mu.Lock()
s.events = make(chan SubscriptionEvent)
s.mu.Unlock()
v := 0
s.conn = &v
*s.conn++
return nil
}
func (s *stubSubscription) Stop() {
if !s.running {
return
}
s.running = false
s.conn = nil
s.mu.Lock()
close(s.events)
s.mu.Unlock()
}
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
s.mu.Lock()
defer s.mu.Unlock()
return s.events
}
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
m.subscription = &stubSubscription{}
var wg sync.WaitGroup
for i := range 8 {
wg.Go(func() {
id := fmt.Sprintf("client-%d", i)
for range 50 {
m.Subscribe(id)
m.Unsubscribe(id)
}
})
}
wg.Wait()
}
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
+1
View File
@@ -79,6 +79,7 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
pkHelper PkHelper pkHelper PkHelper
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
subLifecycleMu sync.Mutex
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan CUPSState] subscribers syncmap.Map[string, chan CUPSState]
stopChan chan struct{} stopChan chan struct{}
@@ -302,17 +302,17 @@ func (a *SecretAgent) GetSecrets(
} }
a.backend.cachedVPNCredsMu.Unlock() a.backend.cachedVPNCredsMu.Unlock()
a.backend.cachedGPSamlMu.Lock() a.backend.cachedOpenConnectMu.Lock()
cachedGPSaml := a.backend.cachedGPSamlCookie cachedOpenConnect := a.backend.cachedOpenConnectAuth
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid { if cachedOpenConnect != nil && cachedOpenConnect.ConnectionUUID == connUuid {
a.backend.cachedGPSamlCookie = nil a.backend.cachedOpenConnectAuth = nil
a.backend.cachedGPSamlMu.Unlock() a.backend.cachedOpenConnectMu.Unlock()
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid) log.Infof("[SecretAgent] Using cached OpenConnect authentication for %s", connUuid)
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil return buildOpenConnectSecretsResponse(settingName, cachedOpenConnect.Cookie, cachedOpenConnect.Host, cachedOpenConnect.Fingerprint), nil
} }
a.backend.cachedGPSamlMu.Unlock() a.backend.cachedOpenConnectMu.Unlock()
if len(fields) == 1 && fields[0] == "gp-saml" { if len(fields) == 1 && fields[0] == "gp-saml" {
gateway := "" gateway := ""
@@ -347,17 +347,17 @@ func (a *SecretAgent) GetSecrets(
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager") log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
a.backend.cachedGPSamlMu.Lock() a.backend.cachedOpenConnectMu.Lock()
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{ a.backend.cachedOpenConnectAuth = &cachedOpenConnectAuth{
ConnectionUUID: connUuid, ConnectionUUID: connUuid,
Cookie: authResult.Cookie, Cookie: authResult.Cookie,
Host: authResult.Host, Host: authResult.Host,
User: authResult.User, User: authResult.User,
Fingerprint: authResult.Fingerprint, Fingerprint: authResult.Fingerprint,
} }
a.backend.cachedGPSamlMu.Unlock() a.backend.cachedOpenConnectMu.Unlock()
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil return buildOpenConnectSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
} }
} }
@@ -987,7 +987,7 @@ func buildWiFiSecretsResponse(settingName string, secrets map[string]string) nmS
return out return out
} }
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap { func buildOpenConnectSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
out := nmSettingMap{} out := nmSettingMap{}
vpnSec := nmVariantMap{} vpnSec := nmVariantMap{}
@@ -122,7 +122,7 @@ func TestNeedsExternalBrowserAuth(t *testing.T) {
} }
} }
func TestBuildGPSamlSecretsResponse(t *testing.T) { func TestBuildOpenConnectSecretsResponse(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
settingName string settingName string
@@ -155,7 +155,7 @@ func TestBuildGPSamlSecretsResponse(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint) result := buildOpenConnectSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
assert.NotNil(t, result) assert.NotNil(t, result)
assert.Contains(t, result, tt.settingName) assert.Contains(t, result, tt.settingName)
@@ -80,16 +80,16 @@ type NetworkManagerBackend struct {
hotspotPendingDevice string hotspotPendingDevice string
pendingVPNSave *pendingVPNCredentials pendingVPNSave *pendingVPNCredentials
pendingVPNSaveMu sync.Mutex pendingVPNSaveMu sync.Mutex
cachedVPNCreds *cachedVPNCredentials cachedVPNCreds *cachedVPNCredentials
cachedVPNCredsMu sync.Mutex cachedVPNCredsMu sync.Mutex
cachedPKCS11PIN *cachedPKCS11PIN cachedPKCS11PIN *cachedPKCS11PIN
cachedPKCS11Mu sync.Mutex cachedPKCS11Mu sync.Mutex
cachedGPSamlCookie *cachedGPSamlCookie cachedOpenConnectAuth *cachedOpenConnectAuth
cachedGPSamlMu sync.Mutex cachedOpenConnectMu sync.Mutex
cachedWiFiSecret *cachedWiFiSecret cachedWiFiSecret *cachedWiFiSecret
cachedWiFiSecretMu sync.Mutex cachedWiFiSecretMu sync.Mutex
onStateChange func() onStateChange func()
} }
@@ -100,8 +100,9 @@ type pendingVPNCredentials struct {
Password string Password string
// Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass"); // Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass");
// falls back to Password under the "password" key when empty. // falls back to Password under the "password" key when empty.
Secrets map[string]string Secrets map[string]string
SavePassword bool PersistentSecrets map[string]string
SavePassword bool
} }
type cachedVPNCredentials struct { type cachedVPNCredentials struct {
@@ -124,7 +125,7 @@ type cachedWiFiSecret struct {
Secrets map[string]string Secrets map[string]string
} }
type cachedGPSamlCookie struct { type cachedOpenConnectAuth struct {
ConnectionUUID string ConnectionUUID string
Cookie string Cookie string
Host string Host string
@@ -3,6 +3,7 @@ package network
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"os/exec" "os/exec"
"strings" "strings"
@@ -10,16 +11,29 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
) )
type gpSamlAuthResult struct { type openConnectAuthResult struct {
Cookie string Cookie string
Host string Host string
User string User string
Fingerprint string Fingerprint string
} }
type openConnectAuthError struct {
cause error
serverCert string
}
func (e *openConnectAuthError) Error() string {
return fmt.Sprintf("openconnect --authenticate failed: %v", e.cause)
}
func (e *openConnectAuthError) Unwrap() error {
return e.cause
}
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui. // runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
// Only supports protocol=gp. Other protocols need their own implementations. // Only supports protocol=gp. Other protocols need their own implementations.
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) { func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*openConnectAuthResult, error) {
if gateway == "" { if gateway == "" {
return nil, fmt.Errorf("GP SAML auth: gateway is empty") return nil, fmt.Errorf("GP SAML auth: gateway is empty")
} }
@@ -63,7 +77,7 @@ func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, ga
} }
}() }()
result := &gpSamlAuthResult{Host: gateway} result := &openConnectAuthResult{Host: gateway}
var allOutput []string var allOutput []string
scanner := bufio.NewScanner(stdout) scanner := bufio.NewScanner(stdout)
@@ -117,13 +131,8 @@ func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, ga
return result, nil return result, nil
} }
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) { func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*openConnectAuthResult, error) {
ocPath, err := exec.LookPath("openconnect") return runOpenConnectAuthenticate(ctx, []string{
if err != nil {
return nil, fmt.Errorf("openconnect not found: %w", err)
}
args := []string{
"--protocol=gp", "--protocol=gp",
"--usergroup=gateway:prelogin-cookie", "--usergroup=gateway:prelogin-cookie",
"--user=" + user, "--user=" + user,
@@ -131,18 +140,83 @@ func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user
"--allow-insecure-crypto", "--allow-insecure-crypto",
"--authenticate", "--authenticate",
gateway, gateway,
}, preloginCookie)
}
func runOpenConnectPasswordAuth(
ctx context.Context,
data map[string]string,
username, password, serverCert string,
) (*openConnectAuthResult, error) {
if data["protocol"] != "fortinet" {
return nil, fmt.Errorf("only Fortinet password authentication is supported")
}
gateway := data["gateway"]
if gateway == "" {
return nil, fmt.Errorf("OpenConnect gateway is empty")
}
if username == "" || password == "" {
return nil, fmt.Errorf("OpenConnect username and password are required")
}
args := []string{
"--protocol=fortinet",
"--user=" + username,
"--passwd-on-stdin",
"--non-inter",
}
if usergroup := data["usergroup"]; usergroup != "" {
args = append(args, "--usergroup="+usergroup)
}
if serverCert != "" {
args = append(args, "--servercert="+serverCert)
}
args = append(args, "--authenticate", gateway)
result, err := runOpenConnectAuthenticate(ctx, args, password)
if err == nil {
result.Host = gateway
if result.Fingerprint == "" {
result.Fingerprint = serverCert
}
}
return result, err
}
func runOpenConnectAuthenticate(ctx context.Context, args []string, secret string) (*openConnectAuthResult, error) {
ocPath, err := exec.LookPath("openconnect")
if err != nil {
return nil, fmt.Errorf("openconnect not found: %w", err)
} }
cmd := exec.CommandContext(ctx, ocPath, args...) cmd := exec.CommandContext(ctx, ocPath, args...)
cmd.Stdin = strings.NewReader(preloginCookie) cmd.Stdin = strings.NewReader(secret + "\n")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
result := parseOpenConnectAuthenticateOutput(string(output))
serverCert := suggestedOpenConnectServerCert(string(output))
if err != nil { if err != nil {
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output)) if ctx.Err() != nil {
return nil, fmt.Errorf("openconnect authentication timed out or was cancelled: %w", ctx.Err())
}
return nil, &openConnectAuthError{cause: err, serverCert: serverCert}
}
if result.Cookie == "" {
return nil, &openConnectAuthError{
cause: errors.New("no COOKIE in command output"),
serverCert: serverCert,
}
} }
result := &gpSamlAuthResult{} log.Infof("[OpenConnect] Authentication successful: cookie_len=%d, host=%s, has_fingerprint=%v",
for _, line := range strings.Split(string(output), "\n") { len(result.Cookie), result.Host, result.Fingerprint != "")
return result, nil
}
func parseOpenConnectAuthenticateOutput(output string) *openConnectAuthResult {
result := &openConnectAuthResult{}
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
switch { switch {
case strings.HasPrefix(line, "COOKIE="): case strings.HasPrefix(line, "COOKIE="):
@@ -158,15 +232,7 @@ func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user
} }
} }
} }
return result
if result.Cookie == "" {
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
}
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
len(result.Cookie), result.Host, result.Fingerprint)
return result, nil
} }
func unshellQuote(s string) string { func unshellQuote(s string) string {
@@ -179,7 +245,7 @@ func unshellQuote(s string) string {
return s return s
} }
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) { func parseGPSamlFromCommandLine(line string, result *openConnectAuthResult) {
if !strings.Contains(line, "openconnect") { if !strings.Contains(line, "openconnect") {
return return
} }
@@ -1,6 +1,10 @@
package network package network
import ( import (
"context"
"errors"
"os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -71,7 +75,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
initialResult *gpSamlAuthResult initialResult *openConnectAuthResult
expectedCookie string expectedCookie string
expectedUser string expectedUser string
expectedFP string expectedFP string
@@ -79,7 +83,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "full openconnect command", name: "full openconnect command",
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john", line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
initialResult: &gpSamlAuthResult{}, initialResult: &openConnectAuthResult{},
expectedCookie: "AUTH123", expectedCookie: "AUTH123",
expectedUser: "john", expectedUser: "john",
expectedFP: "pin-sha256:ABC", expectedFP: "pin-sha256:ABC",
@@ -87,7 +91,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "with equals signs in cookie", name: "with equals signs in cookie",
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane", line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
initialResult: &gpSamlAuthResult{}, initialResult: &openConnectAuthResult{},
expectedCookie: "authcookie=xyz123&portal=GATE", expectedCookie: "authcookie=xyz123&portal=GATE",
expectedUser: "jane", expectedUser: "jane",
expectedFP: "", expectedFP: "",
@@ -95,7 +99,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "non-openconnect line", name: "non-openconnect line",
line: "some other output", line: "some other output",
initialResult: &gpSamlAuthResult{}, initialResult: &openConnectAuthResult{},
expectedCookie: "", expectedCookie: "",
expectedUser: "", expectedUser: "",
expectedFP: "", expectedFP: "",
@@ -103,7 +107,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "preserves existing values", name: "preserves existing values",
line: "openconnect --user=newuser", line: "openconnect --user=newuser",
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"}, initialResult: &openConnectAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
expectedCookie: "existing", expectedCookie: "existing",
expectedUser: "newuser", expectedUser: "newuser",
expectedFP: "existing-fp", expectedFP: "existing-fp",
@@ -111,7 +115,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "only updates empty fields", name: "only updates empty fields",
line: "openconnect --cookie=NEW --user=NEW", line: "openconnect --cookie=NEW --user=NEW",
initialResult: &gpSamlAuthResult{Cookie: "OLD"}, initialResult: &openConnectAuthResult{Cookie: "OLD"},
expectedCookie: "OLD", expectedCookie: "OLD",
expectedUser: "NEW", expectedUser: "NEW",
expectedFP: "", expectedFP: "",
@@ -119,7 +123,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "real gp-saml-gui output", name: "real gp-saml-gui output",
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin", line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
initialResult: &gpSamlAuthResult{}, initialResult: &openConnectAuthResult{},
expectedCookie: "", expectedCookie: "",
expectedUser: "john.doe@example.com", expectedUser: "john.doe@example.com",
expectedFP: "", expectedFP: "",
@@ -127,7 +131,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
{ {
name: "with server cert flag", name: "with server cert flag",
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com", line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
initialResult: &gpSamlAuthResult{}, initialResult: &openConnectAuthResult{},
expectedCookie: "", expectedCookie: "",
expectedUser: "", expectedUser: "",
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=", expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
@@ -158,7 +162,7 @@ func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
"", "",
} }
result := &gpSamlAuthResult{} result := &openConnectAuthResult{}
for _, line := range lines { for _, line := range lines {
parseGPSamlFromCommandLine(line, result) parseGPSamlFromCommandLine(line, result)
} }
@@ -167,3 +171,19 @@ func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line") assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
assert.Empty(t, result.Fingerprint) assert.Empty(t, result.Fingerprint)
} }
func TestRunOpenConnectAuthenticateSanitizesFailure(t *testing.T) {
binDir := t.TempDir()
openConnectPath := filepath.Join(binDir, "openconnect")
script := "#!/bin/sh\nprintf '%s\\n' 'Cookie: should-not-leak' 'Add --servercert pin-sha256:TEST-FINGERPRINT' >&2\nexit 1\n"
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
t.Setenv("PATH", binDir)
_, err := runOpenConnectAuthenticate(context.Background(), []string{"--authenticate", "vpn.example.test"}, "password")
assert.Error(t, err)
assert.NotContains(t, err.Error(), "should-not-leak")
var authErr *openConnectAuthError
assert.True(t, errors.As(err, &authErr))
assert.Equal(t, "pin-sha256:TEST-FINGERPRINT", authErr.serverCert)
}
@@ -326,6 +326,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
} }
authAction := detectVPNAuthAction(vpnServiceType, vpnData) authAction := detectVPNAuthAction(vpnServiceType, vpnData)
var openConnectAuth *openConnectAuthResult
switch authAction { switch authAction {
case "openvpn_username": case "openvpn_username":
@@ -335,6 +336,19 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil { if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
return err return err
} }
case "openconnect_password":
if err := b.ensureOpenConnectAgentFlags(targetConn, vpnData); err != nil {
return fmt.Errorf("failed to prepare OpenConnect connection: %w", err)
}
authCtx, authCancel := context.WithTimeout(context.Background(), 5*time.Minute)
openConnectAuth, err = b.handleOpenConnectPasswordAuth(
authCtx, targetConn, connName, targetUUID, vpnServiceType, vpnData,
)
authCancel()
if err != nil {
return fmt.Errorf("OpenConnect authentication failed: %w", err)
}
case "gp_saml": case "gp_saml":
gateway := vpnData["gateway"] gateway := vpnData["gateway"]
protocol := vpnData["protocol"] protocol := vpnData["protocol"]
@@ -345,7 +359,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway) log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute) samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol) openConnectAuth, err = b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
samlCancel() samlCancel()
if err != nil { if err != nil {
errMsg := err.Error() errMsg := err.Error()
@@ -363,16 +377,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
} }
} }
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: targetUUID,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
b.cachedGPSamlMu.Unlock()
if err := targetConn.ClearSecrets(); err != nil { if err := targetConn.ClearSecrets(); err != nil {
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err) log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
} else { } else {
@@ -382,6 +386,19 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName) log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
} }
if openConnectAuth != nil {
b.cachedOpenConnectMu.Lock()
b.cachedOpenConnectAuth = &cachedOpenConnectAuth{
ConnectionUUID: targetUUID,
Cookie: openConnectAuth.Cookie,
Host: openConnectAuth.Host,
User: openConnectAuth.User,
Fingerprint: openConnectAuth.Fingerprint,
}
b.cachedOpenConnectMu.Unlock()
log.Infof("[ConnectVPN] OpenConnect authentication cached for %s, proceeding with activation", connName)
}
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.IsConnectingVPN = true b.state.IsConnectingVPN = true
b.state.ConnectingVPNUUID = targetUUID b.state.ConnectingVPNUUID = targetUUID
@@ -394,6 +411,13 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
nm := b.nmConn.(gonetworkmanager.NetworkManager) nm := b.nmConn.(gonetworkmanager.NetworkManager)
_, err = nm.ActivateConnection(targetConn, nil, nil) _, err = nm.ActivateConnection(targetConn, nil, nil)
if err != nil { if err != nil {
b.cachedOpenConnectMu.Lock()
b.cachedOpenConnectAuth = nil
b.cachedOpenConnectMu.Unlock()
b.pendingVPNSaveMu.Lock()
b.pendingVPNSave = nil
b.pendingVPNSaveMu.Unlock()
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.IsConnectingVPN = false b.state.IsConnectingVPN = false
b.state.ConnectingVPNUUID = "" b.state.ConnectingVPNUUID = ""
@@ -425,6 +449,9 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol) log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
} }
} }
if protocol == "fortinet" && data["authtype"] == "password" {
return "openconnect_password"
}
case strings.Contains(serviceType, "openvpn"): case strings.Contains(serviceType, "openvpn"):
connType := data["connection-type"] connType := data["connection-type"]
username := data["username"] username := data["username"]
@@ -435,6 +462,200 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
return "" return ""
} }
func setOpenConnectAgentFlags(data map[string]string) bool {
changed := false
for _, field := range []string{"cookie", "gateway", "gwcert"} {
key := field + "-flags"
if data[key] != "2" {
data[key] = "2"
changed = true
}
}
return changed
}
func (b *NetworkManagerBackend) ensureOpenConnectAgentFlags(conn gonetworkmanager.Connection, data map[string]string) error {
if !setOpenConnectAgentFlags(data) {
return nil
}
if b.dbusConn == nil {
return fmt.Errorf("NetworkManager D-Bus connection is unavailable")
}
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", conn.GetPath())
var existingSettings map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
return fmt.Errorf("failed to get connection settings: %w", err)
}
vpn, ok := existingSettings["vpn"]
if !ok {
return fmt.Errorf("VPN settings are missing")
}
vpn["data"] = dbus.MakeVariant(data)
var stored map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSecrets", 0, "vpn").Store(&stored); err != nil {
return fmt.Errorf("failed to preserve VPN secrets: %w", err)
}
if storedVPN, ok := stored["vpn"]; ok {
if secrets, ok := storedVPN["secrets"]; ok {
vpn["secrets"] = secrets
}
}
settings := map[string]map[string]dbus.Variant{"vpn": vpn}
if connection, ok := existingSettings["connection"]; ok {
settings["connection"] = connection
}
var result map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
return fmt.Errorf("failed to set NetworkManager secret-agent flags: %w", err)
}
return nil
}
func (b *NetworkManagerBackend) handleOpenConnectPasswordAuth(
ctx context.Context,
targetConn gonetworkmanager.Connection,
connName, targetUUID, vpnServiceType string,
data map[string]string,
) (*openConnectAuthResult, error) {
username := data["username"]
secrets := map[string]string{}
if stored, err := targetConn.GetSecrets("vpn"); err == nil {
if vpn, ok := stored["vpn"]; ok {
if saved, ok := vpn["secrets"].(map[string]string); ok {
secrets = saved
}
}
}
password := secrets["password"]
serverCert := secrets["certificate:"+data["gateway"]]
if serverCert == "" {
serverCert = secrets["gwcert"]
}
var reply PromptReply
if username == "" || password == "" {
if b.promptBroker == nil {
return nil, fmt.Errorf("password authentication requires an interactive prompt")
}
fields := []string{}
fieldsInfo := []FieldInfo{}
if username == "" {
fields = append(fields, "username")
fieldsInfo = append(fieldsInfo, FieldInfo{Name: "username", Label: "Username", IsSecret: false})
}
if password == "" {
fields = append(fields, "password")
fieldsInfo = append(fieldsInfo, FieldInfo{Name: "password", Label: "Password", IsSecret: true})
}
token, err := b.promptBroker.Ask(ctx, PromptRequest{
Name: connName,
ConnType: "vpn",
VpnService: vpnServiceType,
SettingName: "vpn",
Fields: fields,
FieldsInfo: fieldsInfo,
Reason: "required",
ConnectionId: connName,
ConnectionUuid: targetUUID,
ConnectionPath: string(targetConn.GetPath()),
})
if err != nil {
return nil, fmt.Errorf("failed to request credentials: %w", err)
}
reply, err = b.promptBroker.Wait(ctx, token)
if err != nil {
return nil, fmt.Errorf("credentials prompt failed: %w", err)
}
if username == "" {
username = reply.Secrets["username"]
}
if password == "" {
password = reply.Secrets["password"]
}
}
auth, err := runOpenConnectPasswordAuth(ctx, data, username, password, serverCert)
persistentSecrets := map[string]string{}
var authErr *openConnectAuthError
if err != nil && errors.As(err, &authErr) && authErr.serverCert != "" && authErr.serverCert != serverCert {
if b.promptBroker == nil {
return nil, fmt.Errorf("VPN server certificate is untrusted: %s", authErr.serverCert)
}
reason := "server-certificate"
if serverCert != "" {
reason = "server-certificate-changed"
}
token, promptErr := b.promptBroker.Ask(ctx, PromptRequest{
Name: connName,
ConnType: "vpn",
VpnService: vpnServiceType,
SettingName: "vpn",
Hints: []string{authErr.serverCert},
Reason: reason,
ConnectionId: connName,
ConnectionUuid: targetUUID,
ConnectionPath: string(targetConn.GetPath()),
})
if promptErr != nil {
return nil, fmt.Errorf("failed to request certificate confirmation: %w", promptErr)
}
if _, promptErr = b.promptBroker.Wait(ctx, token); promptErr != nil {
return nil, fmt.Errorf("certificate confirmation failed: %w", promptErr)
}
auth, err = runOpenConnectPasswordAuth(ctx, data, username, password, authErr.serverCert)
if err == nil {
persistentSecrets["certificate:"+data["gateway"]] = authErr.serverCert
}
}
if err != nil {
return nil, err
}
if len(reply.Secrets) > 0 || len(persistentSecrets) > 0 {
creds := &pendingVPNCredentials{
ConnectionPath: string(targetConn.GetPath()),
PersistentSecrets: persistentSecrets,
}
if _, ok := reply.Secrets["username"]; ok {
creds.Username = username
}
if reply.Save {
creds.Username = username
creds.Password = password
creds.Secrets = map[string]string{"password": password}
creds.SavePassword = true
}
b.pendingVPNSaveMu.Lock()
b.pendingVPNSave = creds
b.pendingVPNSaveMu.Unlock()
}
return auth, nil
}
func suggestedOpenConnectServerCert(output string) string {
for _, field := range strings.Fields(output) {
field = strings.Trim(field, "'\".,")
if strings.HasPrefix(field, "pin-sha256:") {
return field
}
}
return ""
}
func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkmanager.Connection, connName, targetUUID, vpnServiceType string) error { func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkmanager.Connection, connName, targetUUID, vpnServiceType string) error {
log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation") log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation")
@@ -758,13 +979,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.VPNErrorUuid = "" b.state.VPNErrorUuid = ""
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie on success // Clear cached one-shot authentication values on success.
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock() b.cachedOpenConnectMu.Lock()
b.cachedGPSamlCookie = nil b.cachedOpenConnectAuth = nil
b.cachedGPSamlMu.Unlock() b.cachedOpenConnectMu.Unlock()
b.pendingVPNSaveMu.Lock() b.pendingVPNSaveMu.Lock()
pending := b.pendingVPNSave pending := b.pendingVPNSave
@@ -787,13 +1008,16 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.VPNErrorUuid = connectingVPNUUID b.state.VPNErrorUuid = connectingVPNUUID
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie on failure // Clear cached one-shot authentication values on failure.
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock() b.cachedOpenConnectMu.Lock()
b.cachedGPSamlCookie = nil b.cachedOpenConnectAuth = nil
b.cachedGPSamlMu.Unlock() b.cachedOpenConnectMu.Unlock()
b.pendingVPNSaveMu.Lock()
b.pendingVPNSave = nil
b.pendingVPNSaveMu.Unlock()
return return
} }
} }
@@ -811,13 +1035,16 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.VPNErrorUuid = connectingVPNUUID b.state.VPNErrorUuid = connectingVPNUUID
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie // Clear cached one-shot authentication values.
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock() b.cachedOpenConnectMu.Lock()
b.cachedGPSamlCookie = nil b.cachedOpenConnectAuth = nil
b.cachedGPSamlMu.Unlock() b.cachedOpenConnectMu.Unlock()
b.pendingVPNSaveMu.Lock()
b.pendingVPNSave = nil
b.pendingVPNSaveMu.Unlock()
} }
} }
@@ -863,17 +1090,40 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials)
log.Infof("[saveVPNCredentials] Saving username") log.Infof("[saveVPNCredentials] Saving username")
} }
// Save secrets if requested secs := map[string]string{}
if creds.SavePassword { if len(creds.PersistentSecrets) > 0 {
secs := creds.Secrets var stored map[string]map[string]dbus.Variant
if len(secs) == 0 { if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSecrets", 0, "vpn").Store(&stored); err != nil {
secs = map[string]string{"password": creds.Password} log.Warnf("[saveVPNCredentials] GetSecrets failed: %v", err)
return
} }
for field := range secs { if storedVPN, ok := stored["vpn"]; ok {
if storedSecrets, ok := storedVPN["secrets"]; ok {
saved, _ := storedSecrets.Value().(map[string]string)
for field, value := range saved {
secs[field] = value
}
}
}
for field, value := range creds.PersistentSecrets {
secs[field] = value
data[field+"-flags"] = "0" data[field+"-flags"] = "0"
} }
}
if creds.SavePassword {
toSave := creds.Secrets
if len(toSave) == 0 {
toSave = map[string]string{"password": creds.Password}
}
for field, value := range toSave {
secs[field] = value
data[field+"-flags"] = "0"
}
}
if len(secs) > 0 {
vpn["secrets"] = dbus.MakeVariant(secs) vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs)) log.Infof("[saveVPNCredentials] Saving %d secret field(s)", len(secs))
} }
vpn["data"] = dbus.MakeVariant(data) vpn["data"] = dbus.MakeVariant(data)
@@ -1,10 +1,14 @@
package network package network
import ( import (
"context"
"os"
"path/filepath"
"testing" "testing"
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
"github.com/Wifx/gonetworkmanager/v2" "github.com/Wifx/gonetworkmanager/v2"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -136,3 +140,137 @@ func TestNetworkManagerBackend_UpdateVPNConnectionState_EmptyUUID(t *testing.T)
backend.updateVPNConnectionState() backend.updateVPNConnectionState()
}) })
} }
func TestDetectVPNAuthAction_FortinetPasswordOnly(t *testing.T) {
service := "org.freedesktop.NetworkManager.openconnect"
assert.Equal(t, "openconnect_password", detectVPNAuthAction(service, map[string]string{
"protocol": "fortinet",
"authtype": "password",
}))
assert.Empty(t, detectVPNAuthAction(service, map[string]string{
"protocol": "anyconnect",
"authtype": "password",
}))
assert.Empty(t, detectVPNAuthAction(service, map[string]string{
"protocol": "fortinet",
"authtype": "saml",
}))
}
func TestEnsureOpenConnectAgentFlags(t *testing.T) {
data := map[string]string{"protocol": "fortinet"}
assert.True(t, setOpenConnectAgentFlags(data))
assert.Equal(t, "2", data["cookie-flags"])
assert.Equal(t, "2", data["gateway-flags"])
assert.Equal(t, "2", data["gwcert-flags"])
assert.False(t, setOpenConnectAgentFlags(data))
}
func TestOpenConnectCertificateConfirmation(t *testing.T) {
binDir := t.TempDir()
openConnectPath := filepath.Join(binDir, "openconnect")
script := `#!/bin/sh
case "$*" in
*--servercert=pin-sha256:TEST-FINGERPRINT*)
printf '%s\n' "COOKIE='SVPNCOOKIE=test'" "HOST='vpn.example.test'" "FINGERPRINT='pin-sha256:TEST-FINGERPRINT'"
exit 0
;;
esac
printf '%s\n' 'Add --servercert pin-sha256:TEST-FINGERPRINT' >&2
exit 1
`
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
t.Setenv("PATH", binDir)
conn := mock_gonetworkmanager.NewMockConnection(t)
connPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings/999")
conn.EXPECT().GetSecrets("vpn").Return(gonetworkmanager.ConnectionSettings{
"vpn": {"secrets": map[string]string{"password": "test-password"}},
}, nil)
conn.EXPECT().GetPath().Return(connPath).Twice()
broker := &fakePromptBroker{
asked: make(chan PromptRequest, 1),
reply: PromptReply{},
}
backend := &NetworkManagerBackend{promptBroker: broker}
data := map[string]string{
"gateway": "vpn.example.test:443",
"protocol": "fortinet",
"authtype": "password",
"username": "test-user",
}
result, err := backend.handleOpenConnectPasswordAuth(
context.Background(), conn, "Test VPN", "test-uuid",
"org.freedesktop.NetworkManager.openconnect", data,
)
assert.NoError(t, err)
assert.Equal(t, "SVPNCOOKIE=test", result.Cookie)
assert.Equal(t, "vpn.example.test:443", result.Host)
prompt := <-broker.asked
assert.Equal(t, "server-certificate", prompt.Reason)
assert.Equal(t, []string{"pin-sha256:TEST-FINGERPRINT"}, prompt.Hints)
assert.Equal(t, map[string]string{
"certificate:vpn.example.test:443": "pin-sha256:TEST-FINGERPRINT",
}, backend.pendingVPNSave.PersistentSecrets)
}
func TestOpenConnectCertificateRotationReprompts(t *testing.T) {
binDir := t.TempDir()
openConnectPath := filepath.Join(binDir, "openconnect")
script := `#!/bin/sh
case "$*" in
*--servercert=pin-sha256:NEW-FINGERPRINT*)
printf '%s\n' "COOKIE='SVPNCOOKIE=test'" "HOST='vpn.example.test'" "FINGERPRINT='pin-sha256:NEW-FINGERPRINT'"
exit 0
;;
esac
printf '%s\n' 'Add --servercert pin-sha256:NEW-FINGERPRINT' >&2
exit 1
`
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
t.Setenv("PATH", binDir)
conn := mock_gonetworkmanager.NewMockConnection(t)
connPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings/999")
conn.EXPECT().GetSecrets("vpn").Return(gonetworkmanager.ConnectionSettings{
"vpn": {"secrets": map[string]string{
"password": "test-password",
"certificate:vpn.example.test:443": "pin-sha256:OLD-FINGERPRINT",
}},
}, nil)
conn.EXPECT().GetPath().Return(connPath).Twice()
broker := &fakePromptBroker{
asked: make(chan PromptRequest, 1),
reply: PromptReply{},
}
backend := &NetworkManagerBackend{promptBroker: broker}
data := map[string]string{
"gateway": "vpn.example.test:443",
"protocol": "fortinet",
"authtype": "password",
"username": "test-user",
}
result, err := backend.handleOpenConnectPasswordAuth(
context.Background(), conn, "Test VPN", "test-uuid",
"org.freedesktop.NetworkManager.openconnect", data,
)
assert.NoError(t, err)
assert.Equal(t, "SVPNCOOKIE=test", result.Cookie)
prompt := <-broker.asked
assert.Equal(t, "server-certificate-changed", prompt.Reason)
assert.Equal(t, []string{"pin-sha256:NEW-FINGERPRINT"}, prompt.Hints)
assert.Equal(t, map[string]string{
"certificate:vpn.example.test:443": "pin-sha256:NEW-FINGERPRINT",
}, backend.pendingVPNSave.PersistentSecrets)
assert.False(t, backend.pendingVPNSave.SavePassword)
assert.Empty(t, backend.pendingVPNSave.Secrets)
}
+18
View File
@@ -43,6 +43,8 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
handleGetNetworkQRCode(conn, req, manager) handleGetNetworkQRCode(conn, req, manager)
case "network.qrcode-content": case "network.qrcode-content":
handleGetNetworkQRCodeContent(conn, req, manager) handleGetNetworkQRCodeContent(conn, req, manager)
case "network.generate-qrcode":
handleGenerateQRCode(conn, req)
case "network.delete-qrcode": case "network.delete-qrcode":
handleDeleteQRCode(conn, req, manager) handleDeleteQRCode(conn, req, manager)
case "network.ethernet.info": case "network.ethernet.info":
@@ -365,6 +367,22 @@ func handleGetNetworkQRCodeContent(conn *models.Conn, req models.Request, manage
models.Respond(conn, req.ID, content) models.Respond(conn, req.ID, content)
} }
func handleGenerateQRCode(conn *models.Conn, req models.Request) {
text, err := params.String(req.Params, "text")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
paths, err := generateTextQRCode(text)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, paths)
}
func handleDeleteQRCode(conn *models.Conn, req models.Request, _ *Manager) { func handleDeleteQRCode(conn *models.Conn, req models.Request, _ *Manager) {
path, err := params.String(req.Params, "path") path, err := params.String(req.Params, "path")
if err != nil { if err != nil {
@@ -0,0 +1,64 @@
package network
import (
"crypto/sha256"
"fmt"
"os"
"time"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
)
const textQRCodeTmpPrefix = "/tmp/dank-text-qrcode-"
func generateTextQRCode(text string) ([2]string, error) {
qrc, err := qrcode.New(text)
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code for text: %w", err)
}
pathThemed, pathNormal := textQRCodePaths(text)
if err := saveQRCodePNG(qrc, pathThemed, standard.WithBgTransparent(), standard.WithFgColorRGBHex("#ffffff")); err != nil {
return [2]string{}, err
}
if err := saveQRCodePNG(qrc, pathNormal); err != nil {
return [2]string{}, err
}
return [2]string{pathThemed, pathNormal}, nil
}
// Write to a temp file and rename into place so the shell's Image never
// observes a partially written PNG.
func saveQRCodePNG(qrc *qrcode.QRCode, path string, opts ...standard.ImageOption) error {
tmpPath := path + ".tmp"
opts = append(opts, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
w, err := standard.New(tmpPath, opts...)
if err != nil {
return fmt.Errorf("failed to create QR code writer: %w", err)
}
if err := qrc.Save(w); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to save QR code: %w", err)
}
if err := os.Rename(tmpPath, path); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to move QR code into place: %w", err)
}
return nil
}
// Paths are unique per generation, not per text: the library's mask selection
// is non-deterministic, so regenerating the same text produces different
// bytes, and reusing a path lets the shell's URL-keyed pixmap cache serve a
// stale pattern over the new file.
func textQRCodePaths(text string) (themed, normal string) {
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(text)))[:8]
nonce := time.Now().UnixNano()
themed = fmt.Sprintf("%s%s-%d-themed.png", textQRCodeTmpPrefix, hash, nonce)
normal = fmt.Sprintf("%s%s-%d-normal.png", textQRCodeTmpPrefix, hash, nonce)
return
}
+1 -1
View File
@@ -24,7 +24,7 @@ func qrCodePaths(ssid string) (themed, normal string) {
func isValidQRCodePath(path string) bool { func isValidQRCodePath(path string) bool {
clean := filepath.Clean(path) clean := filepath.Clean(path)
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png") return (strings.HasPrefix(clean, qrCodeTmpPrefix) || strings.HasPrefix(clean, textQRCodeTmpPrefix)) && strings.HasSuffix(clean, ".png")
} }
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`) var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
+3 -2
View File
@@ -116,11 +116,12 @@ func RouteRequest(conn *models.Conn, req models.Request) {
} }
if strings.HasPrefix(req.Method, "cups.") { if strings.HasPrefix(req.Method, "cups.") {
if cupsManager == nil { mgr, err := ensureCupsManager()
if err != nil {
models.RespondError(conn, req.ID, "CUPS manager not initialized") models.RespondError(conn, req.ID, "CUPS manager not initialized")
return return
} }
cups.HandleRequest(conn, req, cupsManager) cups.HandleRequest(conn, req, mgr)
return return
} }
+80 -36
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"runtime/debug" "runtime/debug"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
@@ -80,8 +79,8 @@ var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
var capabilitySubscribers syncmap.Map[string, chan ServerInfo] var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var cupsSubscribers syncmap.Map[string, bool] var cupsMu sync.Mutex
var cupsSubscriberCount atomic.Int32 var cupsSubscriberCount int
var appPaths = paths.New("danklinux") var appPaths = paths.New("danklinux")
@@ -174,17 +173,55 @@ func InitializeAppPickerManager() error {
return nil return nil
} }
func InitializeCupsManager() error { func initializeCupsManagerLocked() (bool, error) {
if cupsManager != nil {
return false, nil
}
manager, err := cups.NewManager() manager, err := cups.NewManager()
if err != nil { if err != nil {
log.Warnf("Failed to initialize cups manager: %v", err) log.Warnf("Failed to initialize cups manager: %v", err)
return err return false, err
} }
cupsManager = manager cupsManager = manager
log.Info("CUPS manager initialized") log.Info("CUPS manager initialized")
return nil return true, nil
}
func ensureCupsManager() (*cups.Manager, error) {
cupsMu.Lock()
created, err := initializeCupsManagerLocked()
mgr := cupsManager
cupsMu.Unlock()
if err != nil {
return nil, err
}
if created {
notifyCapabilityChange()
}
return mgr, nil
}
func cupsAvailable() bool {
cupsMu.Lock()
defer cupsMu.Unlock()
return cupsManager != nil
}
func releaseCupsSubscriber() {
cupsMu.Lock()
cupsSubscriberCount--
var mgr *cups.Manager
if cupsSubscriberCount == 0 && cupsManager != nil {
mgr = cupsManager
cupsManager = nil
}
cupsMu.Unlock()
if mgr == nil {
return
}
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
mgr.Close()
notifyCapabilityChange()
} }
func InitializeBrightnessManager() error { func InitializeBrightnessManager() error {
@@ -379,7 +416,7 @@ func getCapabilities() Capabilities {
caps = append(caps, "browser") caps = append(caps, "browser")
} }
if cupsManager != nil { if cupsAvailable() {
caps = append(caps, "cups") caps = append(caps, "cups")
} }
@@ -449,7 +486,7 @@ func getServerInfo() ServerInfo {
caps = append(caps, "browser") caps = append(caps, "browser")
} }
if cupsManager != nil { if cupsAvailable() {
caps = append(caps, "cups") caps = append(caps, "cups")
} }
@@ -913,38 +950,31 @@ func handleSubscribe(conn *models.Conn, req models.Request) {
} }
if shouldSubscribe("cups") { if shouldSubscribe("cups") {
cupsSubscribers.Store(clientID+"-cups", true) cupsMu.Lock()
count := cupsSubscriberCount.Add(1) cupsSubscriberCount++
created, err := initializeCupsManagerLocked()
mgr := cupsManager
cupsMu.Unlock()
if count == 1 { if err != nil {
if err := InitializeCupsManager(); err != nil { log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) } else if created {
} else { notifyCapabilityChange()
notifyCapabilityChange()
}
} }
if cupsManager != nil { if mgr == nil {
releaseCupsSubscriber()
} else {
wg.Add(1) wg.Add(1)
cupsChan := cupsManager.Subscribe(clientID + "-cups") cupsChan := mgr.Subscribe(clientID + "-cups")
go func() { go func() {
defer wg.Done() defer wg.Done()
defer func() { defer func() {
cupsManager.Unsubscribe(clientID + "-cups") mgr.Unsubscribe(clientID + "-cups")
cupsSubscribers.Delete(clientID + "-cups") releaseCupsSubscriber()
count := cupsSubscriberCount.Add(-1)
if count == 0 {
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
if cupsManager != nil {
cupsManager.Close()
cupsManager = nil
notifyCapabilityChange()
}
}
}() }()
initialState := cupsManager.GetState() initialState := mgr.GetState()
select { select {
case eventChan <- ServiceEvent{Service: "cups", Data: initialState}: case eventChan <- ServiceEvent{Service: "cups", Data: initialState}:
case <-stopChan: case <-stopChan:
@@ -1254,9 +1284,12 @@ func cleanupManagers() {
if appPickerManager != nil { if appPickerManager != nil {
appPickerManager.Close() appPickerManager.Close()
} }
cupsMu.Lock()
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
cupsManager = nil
} }
cupsMu.Unlock()
if brightnessManager != nil { if brightnessManager != nil {
brightnessManager.Close() brightnessManager.Close()
} }
@@ -1635,10 +1668,21 @@ func (s *Server) Serve(printDocs bool) error {
}() }()
go func() { go func() {
if err := InitializeBluezManager(); err != nil { for {
err := InitializeBluezManager()
if err == nil {
notifyCapabilityChange()
return
}
log.Warnf("Bluez manager unavailable: %v", err) log.Warnf("Bluez manager unavailable: %v", err)
} else { if !errors.Is(err, bluez.ErrNoAdapter) {
notifyCapabilityChange() return
}
if err := bluez.WaitForAdapter(); err != nil {
log.Warnf("Bluetooth adapter watch failed: %v", err)
return
}
log.Info("Bluetooth adapter appeared, initializing bluez manager")
} }
}() }()
+2 -1
View File
@@ -143,7 +143,8 @@ func wrapInTerminal(term, title, shellCmd string, extraArgs []string) []string {
case "konsole": case "konsole":
argv = []string{term, "-p", "tabtitle=" + title} argv = []string{term, "-p", "tabtitle=" + title}
case "gnome-terminal": case "gnome-terminal":
argv = []string{term, "--title=" + title} // --wait: the factory process otherwise returns immediately
argv = []string{term, "--wait", "--title=" + title}
execFlag = "--" execFlag = "--"
default: default:
argv = []string{term} argv = []string{term}
+14 -9
View File
@@ -412,25 +412,30 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, opts UpgradeOptions) {
onLine := func(line string) { m.appendLog(line) } onLine := func(line string) { m.appendLog(line) }
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs) argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil { if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
code := ErrCodeBackendFailed
switch { switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded): case errors.Is(ctx.Err(), context.DeadlineExceeded):
code = ErrCodeTimeout m.failCustomUpgrade(ErrCodeTimeout, err)
return
case errors.Is(ctx.Err(), context.Canceled): case errors.Is(ctx.Err(), context.Canceled):
code = ErrCodeCancelled m.failCustomUpgrade(ErrCodeCancelled, err)
return
} }
m.mu.Lock() // exit status reflects the trailing `read`, not the update command
m.state.Phase = PhaseError m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
return
} }
m.finishSuccessfulUpgrade(false) m.finishSuccessfulUpgrade(false)
m.runRefresh(context.Background(), false) m.runRefresh(context.Background(), false)
} }
func (m *Manager) failCustomUpgrade(code ErrorCode, err error) {
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) { func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
m.appendLog("Upgrade complete.") m.appendLog("Upgrade complete.")
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got) t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
} }
} }
func TestWrapInTerminal(t *testing.T) {
tests := []struct {
term string
wantPrefix []string
}{
{"kitty", []string{"kitty", "--class", "com.danklinux.dms", "-T", "Title"}},
{"gnome-terminal", []string{"gnome-terminal", "--wait", "--title=Title"}},
{"foot", []string{"foot", "--app-id=com.danklinux.dms", "--title=Title"}},
}
for _, tt := range tests {
got := wrapInTerminal(tt.term, "Title", "echo hi", nil)
if len(got) < len(tt.wantPrefix) || !reflect.DeepEqual(got[:len(tt.wantPrefix)], tt.wantPrefix) {
t.Errorf("wrapInTerminal(%q) = %#v, want prefix %#v", tt.term, got, tt.wantPrefix)
}
tail := got[len(got)-3:]
if tail[0] != "sh" || tail[1] != "-c" {
t.Errorf("wrapInTerminal(%q) tail = %#v, want [sh -c <cmd>]", tt.term, tail)
}
if !strings.Contains(tail[2], "echo hi") {
t.Errorf("wrapInTerminal(%q) command %q does not contain shell command", tt.term, tail[2])
}
}
}
+6 -22
View File
@@ -12,6 +12,7 @@ import (
"time" "time"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/dankgo/boottimer"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -23,9 +24,6 @@ import (
const animKelvinStep = 25 const animKelvinStep = 25
// Go timers freeze during suspend; cap sleeps so wall-clock deadlines can't be missed.
const maxScheduleWait = 5 * time.Minute
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) { func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return nil, err return nil, err
@@ -752,7 +750,8 @@ func (m *Manager) schedulerLoop() {
m.post(func() { m.applyCurrentTemp("startup") }) m.post(func() { m.applyCurrentTemp("startup") })
} }
var timer *time.Timer timer := boottimer.New(24 * time.Hour)
defer timer.Stop()
for { for {
m.configMutex.RLock() m.configMutex.RLock()
enabled := m.config.Enabled enabled := m.config.Enabled
@@ -761,31 +760,19 @@ func (m *Manager) schedulerLoop() {
now := time.Now() now := time.Now()
m.recalcSchedule(now) m.recalcSchedule(now)
var waitDur time.Duration waitDur := 24 * time.Hour
if enabled { if enabled {
deadline := m.getNextDeadline(now) deadline := m.getNextDeadline(now)
waitDur = time.Until(deadline) if waitDur = time.Until(deadline); waitDur < time.Second {
switch {
case waitDur < time.Second:
waitDur = time.Second waitDur = time.Second
case waitDur > maxScheduleWait:
waitDur = maxScheduleWait
} }
} else {
waitDur = 24 * time.Hour
} }
timer.Reset(waitDur)
if timer != nil {
timer.Stop()
}
timer = time.NewTimer(waitDur)
select { select {
case <-m.stopChan: case <-m.stopChan:
timer.Stop()
return return
case <-m.updateTrigger: case <-m.updateTrigger:
timer.Stop()
m.scheduleMutex.Lock() m.scheduleMutex.Lock()
m.schedule.calcDay = time.Time{} m.schedule.calcDay = time.Time{}
m.scheduleMutex.Unlock() m.scheduleMutex.Unlock()
@@ -1086,14 +1073,11 @@ func (m *Manager) handleResume() {
} }
// Compositor gamma state is unknown after resume; force a resend (#1235) // Compositor gamma state is unknown after resume; force a resend (#1235)
// and re-arm the scheduler timer, which froze during suspend.
m.outputs.Range(func(_ uint32, out *outputState) bool { m.outputs.Range(func(_ uint32, out *outputState) bool {
out.lastTemp = 0 out.lastTemp = 0
return true return true
}) })
m.recalcSchedule(time.Now())
m.applyCurrentTemp("resume") m.applyCurrentTemp("resume")
m.triggerUpdate()
} }
func (m *Manager) triggerUpdate() { func (m *Manager) triggerUpdate() {
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": { "dank-qml-common": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1784647395, "lastModified": 1785445867,
"narHash": "sha256-kcLSJC8XBzmWoSBc90Zn81qrNsqWMkwQ1m/zZixOw30=", "narHash": "sha256-l9IRsLIVZ7bV+KMuTdCga89tGt4lpdMXhQR7f/2qoe4=",
"owner": "AvengeMedia", "owner": "AvengeMedia",
"repo": "dank-qml-common", "repo": "dank-qml-common",
"rev": "1919595f0dde4f5bf5ac0baa1902ff6d070971d5", "rev": "3b06bd9372e18bc8086cc3958d4f677d57fbfdd8",
"type": "github" "type": "github"
}, },
"original": { "original": {
+1 -1
View File
@@ -111,7 +111,7 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-G0KCUaNwjWfLcSnb68TMXjpB2xxwheXgrBkFEzcSZTc="; vendorHash = "sha256-pjaRyB6E2TZvVd5a4xcdGSRVr9Dg9wEG/5e+HdtZJCg=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
+64 -6
View File
@@ -11,16 +11,27 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("CacheData") readonly property var log: Log.scoped("CacheData")
readonly property int cacheConfigVersion: 1 readonly property int cacheConfigVersion: 2
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation) readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
property bool _loading: false property bool _loading: false
property bool _hasLoaded: false
property int _loadedCacheVersion: 0
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys)
property string wallpaperLastPath: "" property string wallpaperLastPath: ""
property string profileLastPath: "" property string profileLastPath: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property var fileBrowserSettings: ({ property var fileBrowserSettings: ({
"wallpaper": { "wallpaper": {
"lastPath": "", "lastPath": "",
@@ -78,8 +89,46 @@ Singleton {
function loadCache() { function loadCache() {
_loading = true; _loading = true;
parseCache(cacheFile.text()); try {
_loading = false; parseCache(cacheFile.text());
} finally {
_loading = false;
_hasLoaded = true;
}
}
function set(key, value) {
if (_dataKeys.indexOf(key) < 0) {
log.warn("Unknown cache key:", key);
return;
}
root[key] = value;
saveCache();
}
function migratePins(pins) {
if (!pins)
return;
if (!_hasLoaded)
loadCache();
if (_loadedCacheVersion >= cacheConfigVersion)
return;
let migrated = false;
for (const key of _pinKeys) {
const legacy = pins[key];
if (!legacy || Object.keys(legacy).length === 0)
continue;
if (Object.keys(root[key] || {}).length > 0)
continue;
root[key] = legacy;
migrated = true;
}
if (!migrated)
return;
log.info("Migrated device pins from settings.json");
saveCache();
} }
function parseCache(content) { function parseCache(content) {
@@ -87,6 +136,7 @@ Singleton {
try { try {
if (content && content.trim()) { if (content && content.trim()) {
const cache = JSON.parse(content); const cache = JSON.parse(content);
_loadedCacheVersion = cache.configVersion || 0;
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""; wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "";
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""; profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "";
@@ -122,6 +172,10 @@ Singleton {
}; };
} }
for (const key of _pinKeys) {
root[key] = cache[key] !== undefined ? cache[key] : {};
}
if (cache.configVersion === undefined) { if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache); migrateFromUndefinedToV1(cache);
cleanupUnusedKeys(); cleanupUnusedKeys();
@@ -138,12 +192,16 @@ Singleton {
function saveCache() { function saveCache() {
if (_loading) if (_loading)
return; return;
cacheFile.setText(JSON.stringify({ const data = {
"wallpaperLastPath": wallpaperLastPath, "wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath, "profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings, "fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion "configVersion": cacheConfigVersion
}, null, 2)); };
for (const key of _pinKeys) {
data[key] = root[key];
}
cacheFile.setText(JSON.stringify(data, null, 2));
} }
function migrateFromUndefinedToV1(cache) { function migrateFromUndefinedToV1(cache) {
@@ -151,7 +209,7 @@ Singleton {
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
const validKeys = ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "configVersion"]; const validKeys = _dataKeys.concat(["configVersion"]);
try { try {
const content = cacheFile.text(); const content = cacheFile.text();
-29
View File
@@ -1,29 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
Singleton {
id: root
function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
Paths.mkdir(Paths.imagecache);
}
function clearOldCache(ageInMinutes) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
}
function clearCacheForSize(size) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
}
function getCacheSize(callback) {
Proc.runCommand("cache_size", ["du", "-sm", Paths.stringify(Paths.imagecache)], function (output, exitCode) {
const sizeMB = parseInt(output.split("\t")[0]) || 0;
callback(sizeMB);
});
}
}
@@ -114,26 +114,6 @@ function withRevision(descriptor, revision) {
return next; return next;
} }
function withAnimationOffset(descriptor, x, y) {
var next = normalize(descriptor);
next.animationOffset = {
"x": x === undefined ? next.animationOffset.x : _number(x, next.animationOffset.x),
"y": y === undefined ? next.animationOffset.y : _number(y, next.animationOffset.y)
};
return next;
}
function withBodyRect(descriptor, x, y, width, height) {
var next = normalize(descriptor);
next.bodyRect = {
"x": x === undefined ? next.bodyRect.x : _number(x, next.bodyRect.x),
"y": y === undefined ? next.bodyRect.y : _number(y, next.bodyRect.y),
"width": width === undefined ? next.bodyRect.width : Math.max(0, _number(width, next.bodyRect.width)),
"height": height === undefined ? next.bodyRect.height : Math.max(0, _number(height, next.bodyRect.height))
};
return next;
}
function same(a, b, threshold) { function same(a, b, threshold) {
if (!a || !b) if (!a || !b)
return false; return false;
@@ -162,21 +162,6 @@ function fillBounds(rect, side, seamOverlap, dpr) {
}; };
} }
function clipEnvelope(rect, side, radii, seamOverlap, dpr) {
var fill = fillBounds(rect, side, seamOverlap, dpr);
var chrome = chromeBounds(fill, side, radii.start, radii.end, radii.farExtent, dpr);
return {
"x": chrome.x,
"y": chrome.y,
"width": chrome.width,
"height": chrome.height,
"bodyX": snap(fill.x - chrome.x, dpr),
"bodyY": snap(fill.y - chrome.y, dpr),
"bodyWidth": fill.width,
"bodyHeight": fill.height
};
}
function blurRegions(descriptor, rect, radii, dpr) { function blurRegions(descriptor, rect, radii, dpr) {
var side = descriptor.barSide; var side = descriptor.barSide;
var regions = [bodyRect(rect, dpr)]; var regions = [bodyRect(rect, dpr)];
@@ -219,14 +204,3 @@ function unionBounds(rects, padding, dpr) {
"height": Math.max(0, snap(maxY - minY + pad * 2, dpr)) "height": Math.max(0, snap(maxY - minY + pad * 2, dpr))
}; };
} }
function shadowSourceBounds(descriptor, rect, radii, padding, dpr) {
return unionBounds(blurRegions(descriptor, rect, radii, dpr), padding, dpr);
}
function stableEqual(a, b, dpr) {
if (!a || !b)
return false;
var threshold = 0.5 / (dpr || 1);
return Math.abs(a.x - b.x) < threshold && Math.abs(a.y - b.y) < threshold && Math.abs(a.width - b.width) < threshold && Math.abs(a.height - b.height) < threshold;
}
-10
View File
@@ -44,16 +44,6 @@ function connectorX(barSide, baseX, bodyWidth, placement, spacing, radius) {
return barSide === "left" ? s : s - w; return barSide === "left" ? s : s - w;
} }
function connectorY(barSide, baseY, bodyHeight, placement, spacing, radius) {
var s = seamY(barSide, baseY, bodyHeight, placement);
var h = connectorHeight(barSide, spacing, radius);
if (barSide === "top")
return s;
if (barSide === "bottom")
return s - h;
return placement === "left" ? s - h : s;
}
// Which corner of the connector's bounding rect hosts the concave arc that // Which corner of the connector's bounding rect hosts the concave arc that
// carves into the body. Used for arc-sweep orientation. // carves into the body. Used for arc-sweep orientation.
function arcCorner(barSide, placement) { function arcCorner(barSide, placement) {
+72
View File
@@ -0,0 +1,72 @@
.pragma library
function formatRate(bytesPerSec, gbDecimals) {
if (bytesPerSec < 1024)
return bytesPerSec.toFixed(0) + " B/s";
if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(gbDecimals ?? 2) + " GB/s";
}
function formatBytes(bytes) {
if (bytes < 1024)
return bytes.toFixed(0) + "B";
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(0) + "K";
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(1) + "M";
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
}
function formatIsoTime(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 formatRemaining(ms, zeroText, minText, hText, hmText) {
if (ms <= 0)
return zeroText;
const totalMinutes = Math.ceil(ms / 60000);
if (totalMinutes < 60)
return minText.arg(totalMinutes);
const hours = Math.floor(totalMinutes / 60);
const mins = totalMinutes - hours * 60;
if (mins === 0)
return hText.arg(hours);
return hmText.arg(hours).arg(mins);
}
function pad2(n) {
return n < 10 ? "0" + n : "" + n;
}
function formatUntil(ts, use24h) {
if (!ts)
return "";
const d = new Date(ts);
const hours = d.getHours();
const minutes = d.getMinutes();
if (use24h)
return pad2(hours) + ":" + pad2(minutes);
const suffix = hours >= 12 ? "PM" : "AM";
const h12 = ((hours + 11) % 12) + 1;
return h12 + ":" + pad2(minutes) + " " + suffix;
}
function addToHistory(arr, val, max) {
const newArr = arr.slice();
newArr.push(val);
if (newArr.length > max)
newArr.shift();
return newArr;
}
-7
View File
@@ -1370,10 +1370,3 @@ function buildDmsAction(baseKey, args) {
return parts.join(" "); return parts.join(" ");
} }
function getScreenshotOptions() {
return [
{ id: "write-to-disk", label: "Save to disk", type: "bool" },
{ id: "show-pointer", label: "Show pointer", type: "bool" }
];
}
+15 -5
View File
@@ -22,6 +22,10 @@ Singleton {
readonly property url imagecache: `${cache}/imagecache` readonly property url imagecache: `${cache}/imagecache`
property var iconResolver: null
property var desktopIconResolver: null
property var trashHandler: null
Component.onCompleted: mkdir(imagecache) Component.onCompleted: mkdir(imagecache)
function stringify(path: url): string { function stringify(path: url): string {
@@ -88,7 +92,7 @@ Singleton {
function themedIconPath(name: string): string { function themedIconPath(name: string): string {
if (!name) if (!name)
return ""; return "";
const themed = (typeof IconThemeService !== "undefined") ? IconThemeService.resolve(name) : ""; const themed = iconResolver ? iconResolver(name) : "";
if (themed) if (themed)
return themed; return themed;
return Quickshell.iconPath(name, true); return Quickshell.iconPath(name, true);
@@ -105,11 +109,15 @@ Singleton {
return moddedId; return moddedId;
return themedIconPath(moddedId); return themedIconPath(moddedId);
} }
return themedIconPath(iconName) || DesktopService.resolveIconPath(iconName); if (!desktopIconResolver)
return themedIconPath(iconName);
return themedIconPath(iconName) || desktopIconResolver(iconName);
} }
function trashPath(path: string, callback): void { function trashPath(path: string, callback): void {
TrashService.trashPath(path, callback); if (!trashHandler)
return;
trashHandler(path, callback);
} }
function copyPathToClipboard(path: string): void { function copyPathToClipboard(path: string): void {
@@ -125,7 +133,7 @@ Singleton {
return toFileUrl(expandTilde(target)); return toFileUrl(expandTilde(target));
if (target.startsWith("file://")) if (target.startsWith("file://"))
return target; return target;
const themed = (typeof IconThemeService !== "undefined") ? IconThemeService.resolve(target) : ""; const themed = iconResolver ? iconResolver(target) : "";
if (themed) if (themed)
return themed; return themed;
return "image://icon/" + target; return "image://icon/" + target;
@@ -148,7 +156,9 @@ Singleton {
if (icon && icon !== "") if (icon && icon !== "")
return icon; return icon;
return DesktopService.resolveIconPath(appId); if (!desktopIconResolver)
return "";
return desktopIconResolver(appId);
} }
function getAppName(appId: string, desktopEntry: var): string { function getAppName(appId: string, desktopEntry: var): string {
+27
View File
@@ -0,0 +1,27 @@
.pragma library
function findParentFlickable(item) {
while (item) {
if (item.hasOwnProperty("contentY") && item.hasOwnProperty("contentItem"))
return item;
item = item.parent;
}
return null;
}
function findSettings(item) {
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined)
return item;
item = item.parent;
}
return null;
}
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
+10 -200
View File
@@ -16,6 +16,10 @@ Singleton {
readonly property int sessionConfigVersion: 3 readonly property int sessionConfigVersion: 3
signal loaded
signal brightnessDisplayHintChanged(string deviceName)
signal loadErrorOccurred(string file, string message)
property bool _parseError: false property bool _parseError: false
property bool _hasLoaded: false property bool _hasLoaded: false
property bool _isReadOnly: false property bool _isReadOnly: false
@@ -224,6 +228,7 @@ Singleton {
property string niriOverviewLastMode: "apps" property string niriOverviewLastMode: "apps"
property string settingsSidebarExpandedIds: "," property string settingsSidebarExpandedIds: ","
property string settingsSidebarCollapsedIds: "," property string settingsSidebarCollapsedIds: ","
property bool showConfigReloadToast: true
Component.onCompleted: { Component.onCompleted: {
loadSettings(); loadSettings();
@@ -275,15 +280,14 @@ Singleton {
if (typeof Theme !== "undefined") if (typeof Theme !== "undefined")
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
if (typeof WallpaperCyclingService !== "undefined") loaded();
WallpaperCyclingService.updateCyclingState();
_checkSessionWritable(); _checkSessionWritable();
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to parse session.json - file will not be overwritten."); log.error("Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("session.json"), msg)); Qt.callLater(() => loadErrorOccurred("session.json", msg));
} }
} }
@@ -357,13 +361,12 @@ Singleton {
if (typeof Theme !== "undefined") if (typeof Theme !== "undefined")
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
if (typeof WallpaperCyclingService !== "undefined") loaded();
WallpaperCyclingService.updateCyclingState();
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to parse session.json - file will not be overwritten."); log.error("Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("session.json"), msg)); Qt.callLater(() => loadErrorOccurred("session.json", msg));
} }
} }
@@ -464,11 +467,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function setWallpaperPath(path) {
wallpaperPath = path;
saveSettings();
}
function setWallpaper(imagePath) { function setWallpaper(imagePath) {
wallpaperPath = imagePath; wallpaperPath = imagePath;
if (perModeWallpaper) { if (perModeWallpaper) {
@@ -847,11 +845,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function setNightModeLocationProvider(provider) {
nightModeLocationProvider = provider;
saveSettings();
}
function setThemeModeAutoEnabled(enabled) { function setThemeModeAutoEnabled(enabled) {
themeModeAutoEnabled = enabled; themeModeAutoEnabled = enabled;
saveSettings(); saveSettings();
@@ -940,10 +933,6 @@ Singleton {
setBarPinnedApps(currentPinned); setBarPinnedApps(currentPinned);
} }
function isBarPinnedApp(appId) {
return appId && barPinnedApps.indexOf(appId) !== -1;
}
function hideTrayId(trayId) { function hideTrayId(trayId) {
if (!trayId) if (!trayId)
return; return;
@@ -1006,11 +995,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function setLaunchPrefix(prefix) {
launchPrefix = prefix;
saveSettings();
}
function setLastBrightnessDevice(device) { function setLastBrightnessDevice(device) {
lastBrightnessDevice = device; lastBrightnessDevice = device;
saveSettings(); saveSettings();
@@ -1025,10 +1009,7 @@ Singleton {
} }
brightnessExponentialDevices = newSettings; brightnessExponentialDevices = newSettings;
saveSettings(); saveSettings();
brightnessDisplayHintChanged(deviceName);
if (typeof DisplayService !== "undefined") {
DisplayService.updateDeviceBrightnessDisplay(deviceName);
}
} }
function getBrightnessExponential(deviceName) { function getBrightnessExponential(deviceName) {
@@ -1042,10 +1023,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function getBrightnessUserSetValue(deviceName) {
return brightnessUserSetValues[deviceName];
}
function clearBrightnessUserSetValue(deviceName) { function clearBrightnessUserSetValue(deviceName) {
var newValues = Object.assign({}, brightnessUserSetValues); var newValues = Object.assign({}, brightnessUserSetValues);
delete newValues[deviceName]; delete newValues[deviceName];
@@ -1069,21 +1046,6 @@ Singleton {
return value !== undefined ? value : 1.2; return value !== undefined ? value : 1.2;
} }
function setSelectedGpuIndex(index) {
selectedGpuIndex = index;
saveSettings();
}
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled;
saveSettings();
}
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled;
saveSettings();
}
function setEnabledGpuPciIds(pciIds) { function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds; enabledGpuPciIds = pciIds;
saveSettings(); saveSettings();
@@ -1199,15 +1161,6 @@ Singleton {
return deviceMaxVolumes[nodeName] ?? 100; return deviceMaxVolumes[nodeName] ?? 100;
} }
function removeDeviceMaxVolume(nodeName) {
if (!nodeName)
return;
const updated = Object.assign({}, deviceMaxVolumes);
delete updated[nodeName];
deviceMaxVolumes = updated;
saveSettings();
}
function updateLocale() { function updateLocale() {
if (!locale) { if (!locale) {
I18n._pickTranslation(); I18n._pickTranslation();
@@ -1271,12 +1224,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function clearLauncherHistory() {
launcherLastQuery = "";
launcherSearchHistory = [];
saveSettings();
}
function setAppDrawerLastMode(mode) { function setAppDrawerLastMode(mode) {
appDrawerLastMode = mode; appDrawerLastMode = mode;
saveSettings(); saveSettings();
@@ -1416,141 +1363,4 @@ Singleton {
} }
} }
} }
IpcHandler {
target: "wallpaper"
function get(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead.";
}
return root.wallpaperPath || "";
}
function set(path: string): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead.";
}
if (!path) {
return "ERROR: No path provided";
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path;
try {
root.setWallpaper(absolutePath);
return "SUCCESS: Wallpaper set to " + absolutePath;
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString();
}
}
function clear(): string {
root.setWallpaper("");
root.setPerMonitorWallpaper(false);
root.monitorWallpapers = {};
root.saveSettings();
return "SUCCESS: All wallpapers cleared";
}
function next(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead.";
}
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set";
}
try {
WallpaperCyclingService.cycleNextManually();
return "SUCCESS: Cycling to next wallpaper";
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString();
}
}
function prev(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead.";
}
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set";
}
try {
WallpaperCyclingService.cyclePrevManually();
return "SUCCESS: Cycling to previous wallpaper";
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString();
}
}
function getFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided";
}
return root.getMonitorWallpaper(screenName) || "";
}
function setFor(screenName: string, path: string): string {
if (!screenName) {
return "ERROR: No screen name provided";
}
if (!path) {
return "ERROR: No path provided";
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path;
try {
if (!root.perMonitorWallpaper) {
root.setPerMonitorWallpaper(true);
}
root.setMonitorWallpaper(screenName, absolutePath);
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath;
} catch (e) {
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString();
}
}
function nextFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided";
}
var currentWallpaper = root.getMonitorWallpaper(screenName);
if (!currentWallpaper) {
return "ERROR: No wallpaper set for " + screenName;
}
try {
WallpaperCyclingService.cycleNextForMonitor(screenName);
return "SUCCESS: Cycling to next wallpaper for " + screenName;
} catch (e) {
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString();
}
}
function prevFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided";
}
var currentWallpaper = root.getMonitorWallpaper(screenName);
if (!currentWallpaper) {
return "ERROR: No wallpaper set for " + screenName;
}
try {
WallpaperCyclingService.cyclePrevForMonitor(screenName);
return "SUCCESS: Cycling to previous wallpaper for " + screenName;
} catch (e) {
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString();
}
}
}
} }
+76 -460
View File
@@ -15,7 +15,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("SettingsData") readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 12 readonly property int settingsConfigVersion: 13
enum Position { enum Position {
Top, Top,
@@ -184,6 +184,24 @@ Singleton {
property int mangoLayoutRadiusOverride: -1 property int mangoLayoutRadiusOverride: -1
property int mangoLayoutBorderSize: -1 property int mangoLayoutBorderSize: -1
property bool mangoTrackpadNaturalScrolling: true property bool mangoTrackpadNaturalScrolling: true
property string mouseAccelProfile: "default"
property real mouseAccelSpeed: 0.0
property bool mouseLeftHanded: false
property bool mouseMiddleEmulation: false
property bool mouseNaturalScroll: false
property real mouseScrollFactor: 1.0
property string mouseScrollMethod: "default"
property string touchpadAccelProfile: "default"
property real touchpadAccelSpeed: 0.0
property bool touchpadDisableOnExternalMouse: false
property bool touchpadDisableWhileTyping: true
property bool touchpadDragLock: false
property bool touchpadMiddleEmulation: false
property bool touchpadNaturalScroll: true
property real touchpadScrollFactor: 1.0
property string touchpadScrollMethod: "default"
property bool touchpadTapAndDrag: true
property bool touchpadTapToClick: true
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false property bool showWeekNumber: false
@@ -694,16 +712,23 @@ Singleton {
property bool notepadUseCompositorGap: false property bool notepadUseCompositorGap: false
property int notepadEdgeGap: 0 property int notepadEdgeGap: 0
property string activeCompositor: ""
// Compositor layout gap when enabled and available, else the manual value. // Compositor layout gap when enabled and available, else the manual value.
readonly property int notepadEffectiveEdgeGap: { readonly property int notepadEffectiveEdgeGap: {
if (notepadUseCompositorGap) { if (notepadUseCompositorGap) {
var g = -1; var g = -1;
if (CompositorService.isNiri) switch (activeCompositor) {
case "niri":
g = niriLayoutGapsOverride; g = niriLayoutGapsOverride;
else if (CompositorService.isHyprland) break;
case "hyprland":
g = hyprlandLayoutGapsOverride; g = hyprlandLayoutGapsOverride;
else if (CompositorService.isMango) break;
case "mango":
g = mangoLayoutGapsOverride; g = mangoLayoutGapsOverride;
break;
}
if (g >= 0) if (g >= 0)
return g; return g;
} }
@@ -772,11 +797,6 @@ Singleton {
property bool fadeToDpmsEnabled: true property bool fadeToDpmsEnabled: true
property int fadeToDpmsGracePeriod: 5 property int fadeToDpmsGracePeriod: 5
property string launchPrefix: "" property string launchPrefix: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property bool gtkThemingEnabled: false property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false property bool qtThemingEnabled: false
@@ -916,6 +936,7 @@ Singleton {
property int notificationTimeoutLow: 5000 property int notificationTimeoutLow: 5000
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationIgnoreAppTimeout: false
property bool notificationCompactMode: false property bool notificationCompactMode: false
property bool notificationShowTimeoutBar: false property bool notificationShowTimeoutBar: false
property bool notificationDedupeEnabled: true property bool notificationDedupeEnabled: true
@@ -1112,44 +1133,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function getSystemMonitorVariants() {
return systemMonitorVariants || [];
}
function createSystemMonitorVariant(name, config) {
const id = "sysmon_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
const variant = {
id: id,
name: name,
config: config || getDefaultSystemMonitorConfig()
};
const variants = JSON.parse(JSON.stringify(systemMonitorVariants || []));
variants.push(variant);
systemMonitorVariants = variants;
saveSettings();
return variant;
}
function updateSystemMonitorVariant(variantId, updates) {
const variants = JSON.parse(JSON.stringify(systemMonitorVariants || []));
const idx = variants.findIndex(v => v.id === variantId);
if (idx === -1)
return;
Object.assign(variants[idx], updates);
systemMonitorVariants = variants;
saveSettings();
}
function removeSystemMonitorVariant(variantId) {
const variants = (systemMonitorVariants || []).filter(v => v.id !== variantId);
systemMonitorVariants = variants;
saveSettings();
}
function getSystemMonitorVariant(variantId) {
return (systemMonitorVariants || []).find(v => v.id === variantId) || null;
}
function getDefaultSystemMonitorConfig() { function getDefaultSystemMonitorConfig() {
return { return {
showHeader: true, showHeader: true,
@@ -1290,70 +1273,6 @@ Singleton {
return (desktopWidgetInstances || []).find(inst => inst.id === instanceId) || null; return (desktopWidgetInstances || []).find(inst => inst.id === instanceId) || null;
} }
function getDesktopWidgetInstancesOfType(widgetType) {
return (desktopWidgetInstances || []).filter(inst => inst.widgetType === widgetType);
}
function getEnabledDesktopWidgetInstances() {
return (desktopWidgetInstances || []).filter(inst => inst.enabled);
}
function moveDesktopWidgetInstance(instanceId, direction) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1)
return false;
const targetIdx = direction === "up" ? idx - 1 : idx + 1;
if (targetIdx < 0 || targetIdx >= instances.length)
return false;
const temp = instances[idx];
instances[idx] = instances[targetIdx];
instances[targetIdx] = temp;
desktopWidgetInstances = instances;
saveSettings();
return true;
}
function reorderDesktopWidgetInstance(instanceId, newIndex) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1 || newIndex < 0 || newIndex >= instances.length)
return false;
const [item] = instances.splice(idx, 1);
instances.splice(newIndex, 0, item);
desktopWidgetInstances = instances;
saveSettings();
return true;
}
function reorderDesktopWidgetInstanceInGroup(instanceId, groupId, newIndexInGroup) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const groups = desktopWidgetGroups || [];
const groupMatches = inst => {
if (groupId === null)
return !inst.group || !groups.some(g => g.id === inst.group);
return inst.group === groupId;
};
const groupInstances = instances.filter(groupMatches);
const currentGroupIdx = groupInstances.findIndex(inst => inst.id === instanceId);
if (currentGroupIdx === -1 || currentGroupIdx === newIndexInGroup)
return false;
if (newIndexInGroup < 0 || newIndexInGroup >= groupInstances.length)
return false;
const globalIdx = instances.findIndex(inst => inst.id === instanceId);
if (globalIdx === -1)
return false;
const [item] = instances.splice(globalIdx, 1);
const targetInstance = groupInstances[newIndexInGroup];
let targetGlobalIdx = instances.findIndex(inst => inst.id === targetInstance.id);
if (newIndexInGroup > currentGroupIdx)
targetGlobalIdx++;
instances.splice(targetGlobalIdx, 0, item);
desktopWidgetInstances = instances;
saveSettings();
return true;
}
function moveDesktopWidgetInstanceToGroup(instanceId, groupId, newIndexInGroup) { function moveDesktopWidgetInstanceToGroup(instanceId, groupId, newIndexInGroup) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || [])); const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const groups = desktopWidgetGroups || []; const groups = desktopWidgetGroups || [];
@@ -1419,22 +1338,14 @@ Singleton {
saveSettings(); saveSettings();
} }
function getDesktopWidgetGroup(groupId) {
return (desktopWidgetGroups || []).find(g => g.id === groupId) || null;
}
function getDesktopWidgetInstancesByGroup(groupId) {
return (desktopWidgetInstances || []).filter(inst => inst.group === groupId);
}
function getUngroupedDesktopWidgetInstances() {
return (desktopWidgetInstances || []).filter(inst => !inst.group);
}
signal forceDankBarLayoutRefresh signal forceDankBarLayoutRefresh
signal forceDockLayoutRefresh signal forceDockLayoutRefresh
signal widgetDataChanged signal widgetDataChanged
signal workspaceIconsUpdated signal workspaceIconsUpdated
signal compositorLayoutRefreshNeeded(bool frame)
signal compositorInputRefreshNeeded
signal compositorCursorRefreshNeeded
signal notificationPopupsInvalidated
function refreshAuthAvailability() { function refreshAuthAvailability() {
Processes.detectAuthCapabilities(); Processes.detectAuthCapabilities();
@@ -1469,24 +1380,15 @@ Singleton {
} }
function updateCompositorLayout() { function updateCompositorLayout() {
if (typeof CompositorService === "undefined") compositorLayoutRefreshNeeded(false);
return; }
if (CompositorService.isNiri && typeof NiriService !== "undefined")
NiriService.generateNiriLayoutConfig(); function updateCompositorInput() {
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined") compositorInputRefreshNeeded();
HyprlandService.generateLayoutConfig();
if (CompositorService.isMango && typeof MangoService !== "undefined")
MangoService.generateLayoutConfig();
} }
function updateFrameCompositorLayout() { function updateFrameCompositorLayout() {
// Generate before begin() so compositor readiness is already pending at transitionRequested compositorLayoutRefreshNeeded(true);
if (typeof CompositorService !== "undefined") {
if (CompositorService.isNiri && typeof NiriService !== "undefined")
NiriService.generateNiriLayoutConfig(true);
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
HyprlandService.generateLayoutConfig(true);
}
FrameTransitionState.begin(); FrameTransitionState.begin();
} }
@@ -1707,6 +1609,7 @@ Singleton {
"applyStoredTheme": applyStoredTheme, "applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes, "regenSystemThemes": regenSystemThemes,
"updateCompositorLayout": updateCompositorLayout, "updateCompositorLayout": updateCompositorLayout,
"updateCompositorInput": updateCompositorInput,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs, "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor, "updateCompositorCursor": updateCompositorCursor,
@@ -1730,6 +1633,7 @@ Singleton {
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
const oldVersion = obj?.configVersion ?? 0; const oldVersion = obj?.configVersion ?? 0;
const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null;
if (oldVersion < settingsConfigVersion) { if (oldVersion < settingsConfigVersion) {
const migrated = Store.migrateToVersion(obj, settingsConfigVersion); const migrated = Store.migrateToVersion(obj, settingsConfigVersion);
if (migrated) { if (migrated) {
@@ -1737,6 +1641,8 @@ Singleton {
obj = migrated; obj = migrated;
} }
} }
if (legacyPins)
Qt.callLater(() => CacheData.migratePins(legacyPins));
if (obj?.lockScreenActiveMonitor !== undefined) { if (obj?.lockScreenActiveMonitor !== undefined) {
var oldVal = obj.lockScreenActiveMonitor; var oldVal = obj.lockScreenActiveMonitor;
@@ -2135,14 +2041,6 @@ Singleton {
return showSeconds ? "h:mm:ss AP" : "h:mm AP"; return showSeconds ? "h:mm:ss AP" : "h:mm AP";
} }
function getEffectiveClockDateFormat() {
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d";
}
function getEffectiveLockDateFormat() {
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
}
function initializeListModels() { function initializeListModels() {
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
@@ -2155,39 +2053,6 @@ Singleton {
widgetDataChanged(); widgetDataChanged();
} }
function hasNamedWorkspaces() {
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
return false;
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.name && ws.name.trim() !== "")
return true;
}
return false;
}
function getNamedWorkspaces() {
var namedWorkspaces = [];
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
return namedWorkspaces;
for (const ws of NiriService.allWorkspaces) {
if (ws.name && ws.name.trim() !== "") {
namedWorkspaces.push(ws.name);
}
}
return namedWorkspaces;
}
function getPopupYPosition(barHeight) {
const defaultBar = barConfigs[0] || getBarConfig("default");
const gothOffset = defaultBar?.gothCornersEnabled ? Theme.cornerRadius : 0;
const spacing = defaultBar?.spacing ?? 4;
const bottomGap = defaultBar?.bottomGap ?? 0;
return barHeight + spacing + bottomGap - gothOffset + Theme.popupDistance;
}
function getPopupTriggerPosition(pos, screen, barThickness, widgetWidth, barSpacing, barPosition, barConfig) { function getPopupTriggerPosition(pos, screen, barThickness, widgetWidth, barSpacing, barPosition, barConfig) {
const relativeX = pos.x; const relativeX = pos.x;
const relativeY = pos.y; const relativeY = pos.y;
@@ -2481,51 +2346,10 @@ Singleton {
updateBarConfigs(); updateBarConfigs();
if (positionChanged) { if (positionChanged) {
NotificationService.dismissAllPopups(); notificationPopupsInvalidated();
} }
} }
function checkBarCollisions(barId) {
const bar = getBarConfig(barId);
if (!bar || !bar.enabled)
return [];
const conflicts = [];
const enabledBars = getEnabledBarConfigs();
for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i];
if (other.id === barId)
continue;
const samePosition = bar.position === other.position;
if (!samePosition)
continue;
const barScreens = bar.screenPreferences || ["all"];
const otherScreens = other.screenPreferences || ["all"];
const hasAll = barScreens.includes("all") || otherScreens.includes("all");
if (hasAll) {
conflicts.push({
"barId": other.id,
"barName": other.name,
"reason": "Same position on all screens"
});
continue;
}
const overlapping = barScreens.some(screen => otherScreens.includes(screen));
if (overlapping) {
conflicts.push({
"barId": other.id,
"barName": other.name,
"reason": "Same position on overlapping screens"
});
}
}
return conflicts;
}
function deleteBarConfig(barId) { function deleteBarConfig(barId) {
if (barId === "default") if (barId === "default")
return; return;
@@ -2674,38 +2498,6 @@ Singleton {
return filtered; return filtered;
} }
function getFrameFilteredScreens() {
var prefs = frameScreenPreferences || ["all"];
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
return Quickshell.screens;
}
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
}
function getActiveBarEdgeForScreen(screen) {
if (!screen)
return "";
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled)
continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top:
return "top";
case SettingsData.Position.Bottom:
return "bottom";
case SettingsData.Position.Left:
return "left";
case SettingsData.Position.Right:
return "right";
}
}
return "";
}
function getActiveBarEdgesForScreen(screen) { function getActiveBarEdgesForScreen(screen) {
if (!screen) if (!screen)
return []; return [];
@@ -2735,51 +2527,48 @@ Singleton {
return edges; return edges;
} }
function frameEdgeInsetForSide(screen, side) { function getOverlayBarEdgesForScreen(screen) {
if (!frameEnabled || !screen)
return 0;
const edges = getActiveBarEdgesForScreen(screen);
return edges.includes(side) ? frameBarSize : frameThickness;
}
function getActiveBarThicknessForScreen(screen) {
if (frameEnabled)
return frameBarSize;
if (!screen) if (!screen)
return frameThickness; return [];
var edges = [];
for (var i = 0; i < barConfigs.length; i++) { for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i]; var bc = barConfigs[i];
if (!bc.enabled) if (!bc.enabled || !(bc.useOverlayLayer ?? false))
continue; continue;
var prefs = bc.screenPreferences || ["all"]; var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
continue; continue;
const innerPadding = bc.innerPadding ?? 4; switch (bc.position ?? 0) {
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)); case SettingsData.Position.Top:
const spacing = bc.spacing ?? 4; edges.push("top");
const bottomGap = bc.bottomGap ?? 0; break;
return barT + spacing + bottomGap; case SettingsData.Position.Bottom:
edges.push("bottom");
break;
case SettingsData.Position.Left:
edges.push("left");
break;
case SettingsData.Position.Right:
edges.push("right");
break;
}
} }
return frameThickness; return edges;
} }
function sendTestNotifications() { readonly property real frameBarContentGap: frameBarInsetPadding < 0 ? frameThickness : frameBarInsetPadding
NotificationService.dismissAllPopups(); readonly property real frameBarContentGapExtra: Math.max(0, frameBarContentGap - frameThickness)
sendTestNotification(0);
testNotifTimer1.start(); function frameEdgeReservation(screen, edge) {
testNotifTimer2.start(); if (!screen)
return 0;
return getActiveBarEdgesForScreen(screen).includes(edge) ? frameBarSize : frameThickness;
} }
function sendTestNotification(index) { function frameEdgeInsetForSide(screen, side) {
const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]]; if (!frameEnabled)
return 0;
if (index < 0 || index >= notifications.length) { return frameEdgeReservation(screen, side);
return;
}
const notif = notifications[index];
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]];
testNotificationProcess.running = true;
} }
function setMatugenScheme(scheme) { function setMatugenScheme(scheme) {
@@ -2798,15 +2587,6 @@ Singleton {
set("matugenContrast", value); set("matugenContrast", value);
} }
function setRunUserMatugenTemplates(enabled) {
if (runUserMatugenTemplates === enabled)
return;
set("runUserMatugenTemplates", enabled);
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme();
}
}
function setMatugenTargetMonitor(monitorName) { function setMatugenTargetMonitor(monitorName) {
if (matugenTargetMonitor === monitorName) if (matugenTargetMonitor === monitorName)
return; return;
@@ -2825,11 +2605,6 @@ Singleton {
SessionData.setWeatherLocation(displayName, coordinates); SessionData.setWeatherLocation(displayName, coordinates);
} }
function setIconTheme(themeName) {
const light = iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode;
setIconThemeForMode(themeName, light);
}
function setIconThemeForMode(themeName, light) { function setIconThemeForMode(themeName, light) {
if (light) if (light)
iconThemeLight = themeName; iconThemeLight = themeName;
@@ -2875,20 +2650,7 @@ Singleton {
// https://github.com/Supreeeme/xwayland-satellite/issues/104 // https://github.com/Supreeeme/xwayland-satellite/issues/104
// no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher // no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher
function updateCompositorCursor() { function updateCompositorCursor() {
if (typeof CompositorService === "undefined") compositorCursorRefreshNeeded();
return;
if (CompositorService.isNiri && typeof NiriService !== "undefined") {
NiriService.generateNiriCursorConfig();
return;
}
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined") {
HyprlandService.generateCursorConfig();
return;
}
if (CompositorService.isMango && typeof MangoService !== "undefined") {
MangoService.generateCursorConfig();
return;
}
} }
function updateXResources() { function updateXResources() {
@@ -2956,20 +2718,6 @@ Singleton {
return env; return env;
} }
function setGtkThemingEnabled(enabled) {
set("gtkThemingEnabled", enabled);
if (enabled && typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme();
}
}
function setQtThemingEnabled(enabled) {
set("qtThemingEnabled", enabled);
if (enabled && typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme();
}
}
function setShowDock(enabled) { function setShowDock(enabled) {
showDock = enabled; showDock = enabled;
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
@@ -3015,16 +2763,6 @@ Singleton {
Qt.callLater(() => forceDockLayoutRefresh()); Qt.callLater(() => forceDockLayoutRefresh());
} }
function setDankBarSpacing(spacing) {
const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
"spacing": spacing
});
}
updateCompositorLayout();
}
function setDankBarPosition(position) { function setDankBarPosition(position) {
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (!defaultBar) if (!defaultBar)
@@ -3080,39 +2818,6 @@ Singleton {
} }
} }
function resetDankBarWidgetsToDefault() {
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"];
var defaultCenter = ["music", "clock", "weather"];
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"];
const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
"leftWidgets": defaultLeft,
"centerWidgets": defaultCenter,
"rightWidgets": defaultRight
});
}
updateListModel(leftWidgetsModel, defaultLeft);
updateListModel(centerWidgetsModel, defaultCenter);
updateListModel(rightWidgetsModel, defaultRight);
showLauncherButton = true;
showWorkspaceSwitcher = true;
showFocusedWindow = true;
showWeather = true;
showMusic = true;
showClipboard = true;
showCpuUsage = true;
showMemUsage = true;
showCpuTemp = true;
showGpuTemp = true;
showSystemTray = true;
showClock = true;
showNotificationButton = true;
showBattery = true;
showControlCenterButton = true;
showCapsLockIndicator = true;
}
function setWorkspaceNameIcon(workspaceName, iconData) { function setWorkspaceNameIcon(workspaceName, iconData) {
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons)); var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons));
iconMap[workspaceName] = iconData; iconMap[workspaceName] = iconData;
@@ -3394,15 +3099,6 @@ Singleton {
Theme.reloadCustomThemeVariant(); Theme.reloadCustomThemeVariant();
} }
function toggleDankBarVisible() {
const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
"visible": !defaultBar.visible
});
}
}
function toggleShowDock() { function toggleShowDock() {
setShowDock(!showDock); setShowDock(!showDock);
} }
@@ -3424,13 +3120,6 @@ Singleton {
savePluginSettings(); savePluginSettings();
} }
function removePluginSettings(pluginId) {
if (pluginSettings[pluginId]) {
delete pluginSettings[pluginId];
savePluginSettings();
}
}
function getPluginSettingsForPlugin(pluginId) { function getPluginSettingsForPlugin(pluginId) {
const settings = pluginSettings[pluginId]; const settings = pluginSettings[pluginId];
return settings ? JSON.parse(JSON.stringify(settings)) : {}; return settings ? JSON.parse(JSON.stringify(settings)) : {};
@@ -3456,22 +3145,6 @@ Singleton {
return settings ? JSON.parse(JSON.stringify(settings)) : {}; return settings ? JSON.parse(JSON.stringify(settings)) : {};
} }
function setNiriOutputSettings(outputId, settings) {
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
updated[outputId] = settings;
niriOutputSettings = updated;
saveSettings();
}
function removeNiriOutputSettings(outputId) {
if (!niriOutputSettings[outputId])
return;
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
delete updated[outputId];
niriOutputSettings = updated;
saveSettings();
}
function getHyprlandOutputSetting(outputId, key, defaultValue) { function getHyprlandOutputSetting(outputId, key, defaultValue) {
if (!hyprlandOutputSettings[outputId]) if (!hyprlandOutputSettings[outputId])
return defaultValue; return defaultValue;
@@ -3496,40 +3169,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function getHyprlandOutputSettings(outputId) {
const settings = hyprlandOutputSettings[outputId];
return settings ? JSON.parse(JSON.stringify(settings)) : {};
}
function setHyprlandOutputSettings(outputId, settings) {
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
updated[outputId] = settings;
hyprlandOutputSettings = updated;
saveSettings();
}
function removeHyprlandOutputSettings(outputId) {
if (!hyprlandOutputSettings[outputId])
return;
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
delete updated[outputId];
hyprlandOutputSettings = updated;
saveSettings();
}
function getDisplayProfiles(compositor) {
return displayProfiles[compositor] || {};
}
function setDisplayProfile(compositor, profileId, data) {
const updated = JSON.parse(JSON.stringify(displayProfiles));
if (!updated[compositor])
updated[compositor] = {};
updated[compositor][profileId] = data;
displayProfiles = updated;
saveSettings();
}
function removeDisplayProfile(compositor, profileId) { function removeDisplayProfile(compositor, profileId) {
if (!displayProfiles[compositor] || !displayProfiles[compositor][profileId]) if (!displayProfiles[compositor] || !displayProfiles[compositor][profileId])
return; return;
@@ -3583,29 +3222,6 @@ Singleton {
id: rightWidgetsModel id: rightWidgetsModel
} }
property Process testNotificationProcess
testNotificationProcess: Process {
command: []
running: false
}
property Timer testNotifTimer1
testNotifTimer1: Timer {
interval: 400
repeat: false
onTriggered: sendTestNotification(1)
}
property Timer testNotifTimer2
testNotifTimer2: Timer {
interval: 800
repeat: false
onTriggered: sendTestNotification(2)
}
property alias settingsFile: settingsFile property alias settingsFile: settingsFile
Timer { Timer {
-26
View File
@@ -416,24 +416,6 @@ const StockThemes = {
}, },
}; };
const ThemeCategories = {
GENERIC: {
name: "Generic",
variants: [
"blue",
"purple",
"green",
"orange",
"red",
"cyan",
"pink",
"amber",
"coral",
"monochrome",
],
},
};
const ThemeNames = { const ThemeNames = {
BLUE: "blue", BLUE: "blue",
PURPLE: "purple", PURPLE: "purple",
@@ -448,10 +430,6 @@ const ThemeNames = {
DYNAMIC: "dynamic", DYNAMIC: "dynamic",
}; };
function isStockTheme(themeName) {
return Object.keys(StockThemes.DARK).includes(themeName);
}
function getAvailableThemes(isLight = false) { function getAvailableThemes(isLight = false) {
return isLight ? StockThemes.LIGHT : StockThemes.DARK; return isLight ? StockThemes.LIGHT : StockThemes.DARK;
} }
@@ -464,7 +442,3 @@ function getThemeByName(themeName, isLight = false) {
function getAllThemeNames() { function getAllThemeNames() {
return Object.keys(StockThemes.DARK); return Object.keys(StockThemes.DARK);
} }
function getThemeCategories() {
return ThemeCategories;
}
+64 -605
View File
@@ -107,8 +107,11 @@ Singleton {
property int _colorsRetryCount: 0 property int _colorsRetryCount: 0
property double _lastGenerateMs: 0 property double _lastGenerateMs: 0
property bool themeModeAutomationActive: false property bool blurLayersActive: false
property bool dmsServiceWasDisconnected: true property bool matugenToastSuppressed: false
signal screenTransitionNeeded
signal themeGenerationStarting
readonly property var dank16: { readonly property var dank16: {
const raw = matugenColors?.dank16; const raw = matugenColors?.dank16;
@@ -144,7 +147,7 @@ Singleton {
Quickshell.execDetached(["mkdir", "-p", stateDir]); Quickshell.execDetached(["mkdir", "-p", stateDir]);
// shellDir may be an embedded-UI extraction, which is read-only and // shellDir may be an embedded-UI extraction, which is read-only and
// unexecutable (dankgo shellapp/shellfs makeReadOnly chmods 0444) // unexecutable (dankgo shellapp/shellfs makeReadOnly chmods 0444)
Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]); Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "assets", "", shellDir]);
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => { Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
matugenAvailable = (code === 0) && !envDisableMatugen; matugenAvailable = (code === 0) && !envDisableMatugen;
@@ -196,235 +199,6 @@ Singleton {
const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false; const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
SettingsData.updateCosmicThemeMode(currentIsLight); SettingsData.updateCosmicThemeMode(currentIsLight);
} }
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
startThemeModeAutomation();
}
}
Connections {
target: SessionData
enabled: typeof SessionData !== "undefined"
function onThemeModeAutoEnabledChanged() {
if (SessionData.themeModeAutoEnabled) {
root.startThemeModeAutomation();
} else {
root.stopThemeModeAutomation();
}
}
function onThemeModeAutoModeChanged() {
if (root.themeModeAutomationActive) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
}
}
function onThemeModeStartHourChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeStartMinuteChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeEndHourChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeEndMinuteChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeShareGammaSettingsChanged() {
if (root.themeModeAutomationActive) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
}
}
function onNightModeStartHourChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeStartMinuteChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeEndHourChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeEndMinuteChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onLatitudeChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
function onLongitudeChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
function onNightModeUseIPLocationChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": SessionData.nightModeUseIPLocation
}, response => {
if (!response.error && !SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
}
// React to gamma backend's isDay state changes for location-based mode
Connections {
target: DisplayService
enabled: typeof DisplayService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location" && !themeAutoBackendAvailable()
function onGammaIsDayChanged() {
if (root.isLightMode !== DisplayService.gammaIsDay) {
root.setLightMode(DisplayService.gammaIsDay, true, true);
}
}
}
Connections {
target: DMSService
function onThemeAutoStateUpdate(data) {
if (!SessionData.themeModeAutoEnabled) {
return;
}
applyThemeAutoState(data);
}
function onConnectionStateChanged() {
if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") {
root.syncTimeThemeSchedule();
}
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
root.syncLocationThemeSchedule();
}
if (themeAutoBackendAvailable() && SessionData.themeModeAutoEnabled) {
DMSService.sendRequest("theme.auto.getState", null, response => {
if (response && response.result) {
applyThemeAutoState(response.result);
}
});
}
if (!SessionData.themeModeAutoEnabled) {
return;
}
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": true
}, response => {
if (!response.error) {
log.info("Theme automation: IP location enabled after connection");
}
});
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (!response.error) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, locationResponse => {
if (locationResponse?.error) {
log.warn("Theme automation: Failed to set location", locationResponse.error);
}
});
}
});
} else {
log.warn("Theme automation: No location configured");
}
}
}
}
Connections {
target: SessionService
enabled: SessionData.themeModeAutoEnabled
function onSessionUnlocked() {
root.triggerThemeAutomationRefresh();
}
function onSessionResumed() {
root.triggerThemeAutomationRefresh();
}
}
function triggerThemeAutomationRefresh() {
if (!themeAutoBackendAvailable()) {
root.evaluateThemeMode();
return;
}
DMSService.sendRequest("theme.auto.trigger", {});
} }
function getMatugenColor(path, fallback) { function getMatugenColor(path, fallback) {
@@ -571,8 +345,8 @@ Singleton {
property color surfaceVariantAlpha: withAlpha(surfaceVariant, 0.2) property color surfaceVariantAlpha: withAlpha(surfaceVariant, 0.2)
readonly property bool foregroundLayers: typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true) readonly property bool foregroundLayers: typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true)
readonly property bool blurForegroundLayers: BlurService.enabled && foregroundLayers readonly property bool blurForegroundLayers: blurLayersActive && foregroundLayers
readonly property bool transparentBlurLayers: BlurService.enabled && !foregroundLayers readonly property bool transparentBlurLayers: blurLayersActive && !foregroundLayers
readonly property bool notificationForegroundLayers: typeof SettingsData === "undefined" || (SettingsData.notificationForegroundLayers ?? true) readonly property bool notificationForegroundLayers: typeof SettingsData === "undefined" || (SettingsData.notificationForegroundLayers ?? true)
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency) readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency) readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
@@ -658,7 +432,7 @@ Singleton {
} }
} }
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (foregroundLayers ? withAlpha(surfaceContainerHigh, BlurService.enabled ? Math.min(popupTransparency, 0.24) : popupTransparency) : withAlpha(surfaceContainer, 0)) readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (foregroundLayers ? withAlpha(surfaceContainerHigh, blurLayersActive ? Math.min(popupTransparency, 0.24) : popupTransparency) : withAlpha(surfaceContainer, 0))
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
@@ -978,22 +752,6 @@ Singleton {
}; };
} }
function elevationTintOpacity(level) {
if (!level)
return 0;
if (level === elevationLevel1)
return 0.05;
if (level === elevationLevel2)
return 0.08;
if (level === elevationLevel3)
return 0.11;
if (level === elevationLevel4)
return 0.12;
if (level === elevationLevel5)
return 0.14;
return 0.08;
}
readonly property var animationDurations: [ readonly property var animationDurations: [
{ {
"shorter": 0, "shorter": 0,
@@ -1261,9 +1019,7 @@ Singleton {
} }
function screenTransition() { function screenTransition() {
if (CompositorService.isNiri) { screenTransitionNeeded();
NiriService.doScreenTransition();
}
} }
function switchTheme(themeName, savePrefs = true, enableTransition = true) { function switchTheme(themeName, savePrefs = true, enableTransition = true) {
@@ -1315,7 +1071,6 @@ Singleton {
SessionData.setLightMode(light); SessionData.setLightMode(light);
} }
PortalService.setLightMode(light);
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
SettingsData.updateCosmicThemeMode(light); SettingsData.updateCosmicThemeMode(light);
} }
@@ -1326,22 +1081,6 @@ Singleton {
setLightMode(!isLightMode, savePrefs, true); setLightMode(!isLightMode, savePrefs, true);
} }
function forceGenerateSystemThemes() {
if (!matugenAvailable) {
return;
}
generateSystemThemesFromCurrentTheme();
}
function getAvailableThemes() {
return StockThemes.getAllThemeNames();
}
function getThemeDisplayName(themeName) {
const themeData = StockThemes.getThemeByName(themeName, isLightMode);
return themeData.name;
}
function getThemeColors(themeName) { function getThemeColors(themeName) {
if (themeName === "custom" && customThemeData) { if (themeName === "custom" && customThemeData) {
return customThemeData; return customThemeData;
@@ -1457,10 +1196,6 @@ Singleton {
readonly property var _availableThemeNames: StockThemes.getAllThemeNames() readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
property string currentThemeName: currentTheme property string currentThemeName: currentTheme
function panelBackground() {
return withAlpha(surfaceContainer, panelTransparency);
}
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
property bool widgetBackgroundHasAlpha: { property bool widgetBackgroundHasAlpha: {
@@ -1537,10 +1272,6 @@ Singleton {
} }
} }
function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
}
function barIconSize(barThickness, offset, maximizeIcon, iconScale) { function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
const defaultOffset = offset !== undefined ? offset : -6; const defaultOffset = offset !== undefined ? offset : -6;
const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize; const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
@@ -1623,19 +1354,6 @@ Singleton {
} }
} }
function getPowerProfileDescription(profile) {
switch (profile) {
case 0:
return I18n.tr("Extend battery life", "power profile description");
case 1:
return I18n.tr("Balance power and performance", "power profile description");
case 2:
return I18n.tr("Prioritize performance", "power profile description");
default:
return I18n.tr("Custom power profile", "power profile description");
}
}
function onLightModeChanged() { function onLightModeChanged() {
if (currentTheme === "custom" && customThemeFileView.path) { if (currentTheme === "custom" && customThemeFileView.path) {
customThemeFileView.reload(); customThemeFileView.reload();
@@ -1663,9 +1381,7 @@ Singleton {
log.info("Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)"); log.info("Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)");
if (typeof NiriService !== "undefined" && CompositorService.isNiri) { themeGenerationStarting();
NiriService.suppressNextToast();
}
const desired = { const desired = {
"kind": kind, "kind": kind,
@@ -1777,8 +1493,7 @@ Singleton {
if (currentTheme === dynamic) { if (currentTheme === dynamic) {
if (!rawWallpaperPath) { if (!rawWallpaperPath) {
log.warn("Auto theme has no wallpaper - skipping matugen, syncing portal mode only"); log.warn("Auto theme has no wallpaper - skipping matugen");
PortalService.setLightMode(isLight);
return; return;
} }
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"; const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
@@ -1926,6 +1641,47 @@ Singleton {
return colors; return colors;
} }
function refreshGtkTheme() {
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
const theme = isLight ? "adw-gtk3" : "adw-gtk3-dark";
const schema = "org.gnome.desktop.interface";
const key = "gtk-theme";
const makeCmd = (tool, schema, val) => {
if (tool === "gsettings") {
return `gsettings set ${schema} ${key} '' && gsettings set ${schema} ${key} ${val}`;
} else {
const dconfPath = `/${schema.replace(/\./g, "/")}`;
return `dconf write ${dconfPath}/${key} "''" && dconf write ${dconfPath}/${key} "'${val}'"`;
}
};
Proc.runCommand("gtkRefresher", ["sh", "-c", makeCmd("gsettings", schema, theme)], (output, exitCode) => {
if (exitCode !== 0) {
Proc.runCommand("gtkRefreshFallback", ["sh", "-c", makeCmd("dconf", schema, theme)], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to refresh gtk-theme");
}
});
}
});
}
function patchGtk3colors() {
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
Proc.runCommand("gtk3Patcher", ["bash", shellDir + "/scripts/gtk.sh", configDir, "patch", isLight, shellDir], (output, exitCode) => {
switch (exitCode) {
case 0:
refreshGtkTheme();
break;
case 2:
break;
default:
log.warn(`Failed to patch GTK3 colors: ${output}`);
}
});
}
function applyGtkColors() { function applyGtkColors() {
if (!matugenAvailable) { if (!matugenAvailable) {
if (typeof ToastService !== "undefined") { if (typeof ToastService !== "undefined") {
@@ -1935,9 +1691,9 @@ Singleton {
} }
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"; const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir], (output, exitCode) => { Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, "apply", isLight, shellDir], (output, exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) { if (typeof ToastService !== "undefined" && !root.matugenToastSuppressed) {
ToastService.showInfo(I18n.tr("GTK colors applied successfully")); ToastService.showInfo(I18n.tr("GTK colors applied successfully"));
} }
} else { } else {
@@ -1975,18 +1731,17 @@ Singleton {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
} }
function popupLayerColor(baseColor) {
if (isConnectedEffect)
return connectedSurfaceColor;
return withAlpha(baseColor, popupTransparency);
}
function blendAlpha(c, a) { function blendAlpha(c, a) {
if (!c || c.r === undefined) if (!c || c.r === undefined)
return Qt.rgba(0, 0, 0, 0); return Qt.rgba(0, 0, 0, 0);
return Qt.rgba(c.r, c.g, c.b, c.a * a); return Qt.rgba(c.r, c.g, c.b, c.a * a);
} }
function hoverTint(base) {
const factor = 1.2;
return isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
function blend(c1, c2, r) { function blend(c1, c2, r) {
return Qt.rgba(c1.r * (1 - r) + c2.r * r, c1.g * (1 - r) + c2.g * r, c1.b * (1 - r) + c2.b * r, c1.a * (1 - r) + c2.a * r); return Qt.rgba(c1.r * (1 - r) + c2.r * r, c1.g * (1 - r) + c2.g * r, c1.b * (1 - r) + c2.b * r, c1.a * (1 - r) + c2.a * r);
} }
@@ -2131,8 +1886,11 @@ Singleton {
root.matugenCompleted(currentMode, "error"); root.matugenCompleted(currentMode, "error");
} }
if (!pendingThemeRequest) if (!pendingThemeRequest) {
if (SettingsData.matugenTemplateGtk)
patchGtk3colors();
return; return;
}
const req = pendingThemeRequest; const req = pendingThemeRequest;
pendingThemeRequest = null; pendingThemeRequest = null;
@@ -2300,303 +2058,4 @@ Singleton {
root.switchTheme(defaultTheme, true, false); root.switchTheme(defaultTheme, true, false);
} }
} }
// Theme mode automation functions
function themeAutoBackendAvailable() {
return typeof DMSService !== "undefined" && DMSService.isConnected && Array.isArray(DMSService.capabilities) && DMSService.capabilities.includes("theme.auto");
}
function applyThemeAutoState(state) {
if (!state) {
return;
}
if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) {
return;
}
if (typeof SessionData !== "undefined" && state.nextTransition !== undefined) {
SessionData.themeModeNextTransition = state.nextTransition || "";
}
if (state.isLight !== undefined && root.isLightMode !== state.isLight) {
root.setLightMode(state.isLight, true, true);
}
}
function syncTimeThemeSchedule() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return;
}
if (!DMSService.isConnected) {
return;
}
const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time";
if (!timeModeActive) {
return;
}
DMSService.sendRequest("theme.auto.setMode", {
"mode": "time"
});
const shareSettings = SessionData.themeModeShareGammaSettings;
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
DMSService.sendRequest("theme.auto.setSchedule", {
"startHour": startHour,
"startMinute": startMinute,
"endHour": endHour,
"endMinute": endMinute
}, response => {
if (response && response.error) {
log.error("Theme automation: Failed to sync time schedule:", response.error);
}
});
DMSService.sendRequest("theme.auto.setEnabled", {
"enabled": true
});
DMSService.sendRequest("theme.auto.trigger", {});
}
function syncLocationThemeSchedule() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return;
}
if (!DMSService.isConnected) {
return;
}
const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location";
if (!locationModeActive) {
return;
}
DMSService.sendRequest("theme.auto.setMode", {
"mode": "location"
});
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("theme.auto.setUseIPLocation", {
"use": true
});
} else {
DMSService.sendRequest("theme.auto.setUseIPLocation", {
"use": false
});
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("theme.auto.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
}
DMSService.sendRequest("theme.auto.setEnabled", {
"enabled": true
});
DMSService.sendRequest("theme.auto.trigger", {});
}
function evaluateThemeMode() {
if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) {
return;
}
if (themeAutoBackendAvailable()) {
DMSService.sendRequest("theme.auto.getState", null, response => {
if (response && response.result) {
applyThemeAutoState(response.result);
}
});
return;
}
const mode = SessionData.themeModeAutoMode;
if (mode === "location") {
evaluateLocationBasedThemeMode();
} else {
evaluateTimeBasedThemeMode();
}
}
function evaluateLocationBasedThemeMode() {
if (typeof DisplayService !== "undefined") {
const shouldBeLight = DisplayService.gammaIsDay;
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
return;
}
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
const shouldBeLight = calculateIsDaytime(SessionData.latitude, SessionData.longitude);
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
return;
}
if (root.themeModeAutomationActive) {
if (SessionData.nightModeUseIPLocation) {
log.warn("Theme automation: Waiting for IP location from backend");
} else {
log.warn("Theme automation: Location mode requires coordinates");
}
}
}
function evaluateTimeBasedThemeMode() {
const shareSettings = SessionData.themeModeShareGammaSettings;
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const startMinutes = startHour * 60 + startMinute;
const endMinutes = endHour * 60 + endMinute;
let shouldBeLight;
if (startMinutes < endMinutes) {
shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes;
} else {
shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes;
}
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
}
function calculateIsDaytime(lat, lng) {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 0);
const diff = now - start;
const dayOfYear = Math.floor(diff / 86400000);
const latRad = lat * Math.PI / 180;
const declination = 23.45 * Math.sin((360 / 365) * (dayOfYear - 81) * Math.PI / 180);
const declinationRad = declination * Math.PI / 180;
const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad);
if (cosHourAngle > 1) {
return false; // Polar night
}
if (cosHourAngle < -1) {
return true; // Midnight sun
}
const hourAngle = Math.acos(cosHourAngle);
const hourAngleDeg = hourAngle * 180 / Math.PI;
const sunriseHour = 12 - hourAngleDeg / 15;
const sunsetHour = 12 + hourAngleDeg / 15;
const timeZoneOffset = now.getTimezoneOffset() / 60;
const localSunrise = sunriseHour - lng / 15 - timeZoneOffset;
const localSunset = sunsetHour - lng / 15 - timeZoneOffset;
const currentHour = now.getHours() + now.getMinutes() / 60;
const normalizeSunrise = ((localSunrise % 24) + 24) % 24;
const normalizeSunset = ((localSunset % 24) + 24) % 24;
return currentHour >= normalizeSunrise && currentHour < normalizeSunset;
}
// Helper function to send location to backend
function sendLocationToBackend() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return false;
}
if (!DMSService.isConnected) {
return false;
}
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": true
}, response => {
if (response?.error) {
log.warn("Theme automation: Failed to enable IP location", response.error);
}
});
return true;
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (!response.error) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, locResp => {
if (locResp?.error) {
log.warn("Theme automation: Failed to set location", locResp.error);
}
});
}
});
return true;
}
return false;
}
Timer {
id: locationRetryTimer
interval: 1000
repeat: true
running: false
property int retryCount: 0
onTriggered: {
if (root.sendLocationToBackend()) {
stop();
retryCount = 0;
root.evaluateThemeMode();
} else {
retryCount++;
if (retryCount >= 10) {
stop();
retryCount = 0;
}
}
}
}
function startThemeModeAutomation() {
root.themeModeAutomationActive = true;
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
const sent = root.sendLocationToBackend();
if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") {
locationRetryTimer.start();
} else {
root.evaluateThemeMode();
}
}
function stopThemeModeAutomation() {
root.themeModeAutomationActive = false;
if (typeof DMSService !== "undefined" && DMSService.isConnected) {
DMSService.sendRequest("theme.auto.setEnabled", {
"enabled": false
});
}
}
} }
+2 -21
View File
@@ -4,17 +4,13 @@
function markdownToHtml(text) { function markdownToHtml(text) {
if (!text) return ""; if (!text) return "";
// Store code blocks and inline code to protect them from further processing
const codeBlocks = []; const codeBlocks = [];
const inlineCode = []; const inlineCode = [];
let blockIndex = 0; let blockIndex = 0;
let inlineIndex = 0; let inlineIndex = 0;
// First, extract and replace code blocks with placeholders
let html = text.replace(/```([\s\S]*?)```/g, (match, code) => { let html = text.replace(/```([\s\S]*?)```/g, (match, code) => {
// Trim leading and trailing blank lines only
const trimmedCode = code.replace(/^\n+|\n+$/g, ''); const trimmedCode = code.replace(/^\n+|\n+$/g, '');
// Escape HTML entities in code
const escapedCode = trimmedCode.replace(/&/g, '&amp;') const escapedCode = trimmedCode.replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;'); .replace(/>/g, '&gt;');
@@ -22,9 +18,7 @@ function markdownToHtml(text) {
return `\x00CODEBLOCK${blockIndex++}\x00`; return `\x00CODEBLOCK${blockIndex++}\x00`;
}); });
// Extract and replace inline code
html = html.replace(/`([^`]+)`/g, (match, code) => { html = html.replace(/`([^`]+)`/g, (match, code) => {
// Escape HTML entities in code
const escapedCode = code.replace(/&/g, '&amp;') const escapedCode = code.replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;'); .replace(/>/g, '&gt;');
@@ -40,12 +34,10 @@ function markdownToHtml(text) {
return prefix + `\x00URL${urlIndex++}\x00`; return prefix + `\x00URL${urlIndex++}\x00`;
}); });
// Escape HTML entities (but not in code blocks or URLs)
html = html.replace(/&/g, '&amp;') html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;'); .replace(/>/g, '&gt;');
// Headers
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>'); html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>'); html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>'); html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
@@ -58,20 +50,16 @@ function markdownToHtml(text) {
html = html.replace(/__(.*?)__/g, '<b>$1</b>'); html = html.replace(/__(.*?)__/g, '<b>$1</b>');
html = html.replace(/_(.*?)_/g, '<i>$1</i>'); html = html.replace(/_(.*?)_/g, '<i>$1</i>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Lists
html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>'); html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^- (.*?)$/gm, '<li>$1</li>'); html = html.replace(/^- (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>'); html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>');
// Wrap consecutive list items in ul/ol tags
html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) { html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) {
return '<ul>' + match + '</ul>'; return '<ul>' + match + '</ul>';
}); });
// Restore extracted URLs as anchor tags (preserves raw & in href)
html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => { html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => {
const url = urls[parseInt(index)]; const url = urls[parseInt(index)];
const display = url.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); const display = url.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -87,30 +75,23 @@ function markdownToHtml(text) {
return inlineCode[parseInt(index)]; return inlineCode[parseInt(index)];
}); });
// Line breaks (after code blocks are restored)
html = html.replace(/\n\n/g, '</p><p>'); html = html.replace(/\n\n/g, '</p><p>');
html = html.replace(/\n/g, '<br/>'); html = html.replace(/\n/g, '<br/>');
// Wrap in paragraph tags if not already wrapped
if (!html.startsWith('<')) { if (!html.startsWith('<')) {
html = '<p>' + html + '</p>'; html = '<p>' + html + '</p>';
} }
// Clean up the final HTML
// Remove <br/> tags immediately before block elements
html = html.replace(/<br\/>\s*<pre>/g, '<pre>'); html = html.replace(/<br\/>\s*<pre>/g, '<pre>');
html = html.replace(/<br\/>\s*<ul>/g, '<ul>'); html = html.replace(/<br\/>\s*<ul>/g, '<ul>');
html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>'); html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>');
// Remove empty paragraphs
html = html.replace(/<p>\s*<\/p>/g, ''); html = html.replace(/<p>\s*<\/p>/g, '');
html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, ''); html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, '');
// Remove excessive line breaks html = html.replace(/(<br\/>){3,}/g, '<br/><br/>');
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>'); // Max 2 consecutive line breaks html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2');
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2'); // Remove whitespace between paragraphs
// Remove leading/trailing whitespace
html = html.trim(); html = html.trim();
return html; return html;
+15 -13
View File
@@ -5,11 +5,13 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
signal toastRequested(int severity, string title, string body, string command, string category)
signal toastCategoryDismissed(string category)
property var settingsRoot: null property var settingsRoot: null
onSettingsRootChanged: { onSettingsRootChanged: {
@@ -317,7 +319,7 @@ Singleton {
function launchAuthApplyTerminalFallback(fromPrecheck, details) { function launchAuthApplyTerminalFallback(fromPrecheck, details) {
authApplyTerminalFallbackFromPrecheck = fromPrecheck; authApplyTerminalFallbackFromPrecheck = fromPrecheck;
if (details && details !== "") if (details && details !== "")
ToastService.showInfo(I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync"); toastRequested(0, I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync");
authApplyTerminalFallbackStderr = ""; authApplyTerminalFallbackStderr = "";
authApplyTerminalFallbackProcess.running = true; authApplyTerminalFallbackProcess.running = true;
} }
@@ -364,21 +366,21 @@ Singleton {
} }
function deferGreeterAutoLoginSyncToPill(details) { function deferGreeterAutoLoginSyncToPill(details) {
ToastService.dismissCategory("greeter-autologin-sync"); toastCategoryDismissed("greeter-autologin-sync");
if (settingsRoot) if (settingsRoot)
settingsRoot.set("greeterSyncPending", true); settingsRoot.set("greeterSyncPending", true);
ToastService.showWarning(I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync"); toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync");
finishGreeterAutoLoginSync(); finishGreeterAutoLoginSync();
} }
function greeterAutoLoginSyncSuccessToast(details) { function greeterAutoLoginSyncSuccessToast(details) {
const enabling = settingsRoot && settingsRoot.greeterAutoLogin; const enabling = settingsRoot && settingsRoot.greeterAutoLogin;
// Clear the sticky in-progress toast, then confirm with an auto-dismissing toast. // Clear the sticky in-progress toast, then confirm with an auto-dismissing toast.
ToastService.dismissCategory("greeter-autologin-sync"); toastCategoryDismissed("greeter-autologin-sync");
if (enabling) { if (enabling) {
ToastService.showWarning(I18n.tr("Auto-login enabled"), I18n.tr("You'll skip the greeter password after the next reboot. The lock screen and signing out still require your password.") + (details ? "\n\n" + details : "")); toastRequested(1, I18n.tr("Auto-login enabled"), I18n.tr("You'll skip the greeter password after the next reboot. The lock screen and signing out still require your password.") + (details ? "\n\n" + details : ""), "", "");
} else { } else {
ToastService.showInfo(I18n.tr("Auto-login disabled"), I18n.tr("You'll enter your password at the greeter after the next reboot.") + (details ? "\n\n" + details : "")); toastRequested(0, I18n.tr("Auto-login disabled"), I18n.tr("You'll enter your password at the greeter after the next reboot.") + (details ? "\n\n" + details : ""), "", "");
} }
} }
@@ -575,7 +577,7 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin; const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
if (exitCode === 0) { if (exitCode === 0) {
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup...") : I18n.tr("Disabling auto-login on startup..."), "", "dms-greeter sync --autologin", "greeter-autologin-sync"); root.toastRequested(1, enabling ? I18n.tr("Applying auto-login on startup...") : I18n.tr("Disabling auto-login on startup..."), "", "dms-greeter sync --autologin", "greeter-autologin-sync");
root.greeterAutoLoginSyncProcess.running = true; root.greeterAutoLoginSyncProcess.running = true;
return; return;
} }
@@ -604,7 +606,7 @@ Singleton {
let details = out; let details = out;
if (err !== "") if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err; details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
ToastService.showInfo(I18n.tr("Authentication changes applied"), details, "", "auth-sync"); root.toastRequested(0, I18n.tr("Authentication changes applied"), details, "", "auth-sync");
root.detectAuthCapabilities(); root.detectAuthCapabilities();
root.finishAuthApply(); root.finishAuthApply();
return; return;
@@ -615,7 +617,7 @@ Singleton {
details = out; details = out;
if (err !== "") if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err; details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
ToastService.showWarning(I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync"); root.toastRequested(1, I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync");
root.launchAuthApplyTerminalFallback(false, ""); root.launchAuthApplyTerminalFallback(false, "");
} }
} }
@@ -631,7 +633,7 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
const err = (root.authApplySudoProbeStderr || "").trim(); const err = (root.authApplySudoProbeStderr || "").trim();
if (exitCode === 0) { if (exitCode === 0) {
ToastService.showInfo(I18n.tr("Applying authentication changes..."), "", "", "auth-sync"); root.toastRequested(0, I18n.tr("Applying authentication changes..."), "", "", "auth-sync");
root.authApplyProcess.running = true; root.authApplyProcess.running = true;
return; return;
} }
@@ -651,10 +653,10 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication there; it will close automatically when done."); const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication there; it will close automatically when done.");
ToastService.showInfo(message, "", "", "auth-sync"); root.toastRequested(0, message, "", "", "auth-sync");
} else { } else {
let details = (root.authApplyTerminalFallbackStderr || "").trim(); let details = (root.authApplyTerminalFallbackStderr || "").trim();
ToastService.showError(I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync"); root.toastRequested(2, I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
} }
root.finishAuthApply(); root.finishAuthApply();
} }
+2 -1
View File
@@ -98,7 +98,8 @@ var SPEC = {
niriOverviewLastMode: { def: "apps" }, niriOverviewLastMode: { def: "apps" },
settingsSidebarExpandedIds: { def: "," }, settingsSidebarExpandedIds: { def: "," },
settingsSidebarCollapsedIds: { def: "," } settingsSidebarCollapsedIds: { def: "," },
showConfigReloadToast: { def: true }
}; };
function getValidKeys() { function getValidKeys() {
@@ -75,26 +75,3 @@ function migrateToVersion(obj, targetVersion, settingsData) {
return session; return session;
} }
function cleanup(fileText) {
var getValidKeys = SpecModule.getValidKeys;
if (!fileText || !fileText.trim()) return null;
try {
var session = JSON.parse(fileText);
var validKeys = getValidKeys();
var needsSave = false;
for (var key in session) {
if (validKeys.indexOf(key) < 0) {
delete session[key];
needsSave = true;
}
}
return needsSave ? JSON.stringify(session, null, 2) : null;
} catch (e) {
console.warn("SessionData: Failed to cleanup unused keys:", e.message);
return null;
}
}
+19 -5
View File
@@ -38,6 +38,24 @@ var SPEC = {
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" }, mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
mouseAccelProfile: { def: "default", onChange: "updateCompositorInput" },
mouseAccelSpeed: { def: 0.0, onChange: "updateCompositorInput" },
mouseLeftHanded: { def: false, onChange: "updateCompositorInput" },
mouseMiddleEmulation: { def: false, onChange: "updateCompositorInput" },
mouseNaturalScroll: { def: false, onChange: "updateCompositorInput" },
mouseScrollFactor: { def: 1.0, onChange: "updateCompositorInput" },
mouseScrollMethod: { def: "default", onChange: "updateCompositorInput" },
touchpadAccelProfile: { def: "default", onChange: "updateCompositorInput" },
touchpadAccelSpeed: { def: 0.0, onChange: "updateCompositorInput" },
touchpadDisableOnExternalMouse: { def: false, onChange: "updateCompositorInput" },
touchpadDisableWhileTyping: { def: true, onChange: "updateCompositorInput" },
touchpadDragLock: { def: false, onChange: "updateCompositorInput" },
touchpadMiddleEmulation: { def: false, onChange: "updateCompositorInput" },
touchpadNaturalScroll: { def: true, onChange: "updateCompositorInput" },
touchpadScrollFactor: { def: 1.0, onChange: "updateCompositorInput" },
touchpadScrollMethod: { def: "default", onChange: "updateCompositorInput" },
touchpadTapAndDrag: { def: true, onChange: "updateCompositorInput" },
touchpadTapToClick: { def: true, onChange: "updateCompositorInput" },
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false }, showWeekNumber: { def: false },
@@ -355,11 +373,6 @@ var SPEC = {
fadeToDpmsEnabled: { def: true }, fadeToDpmsEnabled: { def: true },
fadeToDpmsGracePeriod: { def: 5 }, fadeToDpmsGracePeriod: { def: 5 },
launchPrefix: { def: "" }, launchPrefix: { def: "" },
brightnessDevicePins: { def: {} },
wifiNetworkPins: { def: {} },
bluetoothDevicePins: { def: {} },
audioInputDevicePins: { def: {} },
audioOutputDevicePins: { def: {} },
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" }, gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" }, qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
@@ -477,6 +490,7 @@ var SPEC = {
notificationTimeoutLow: { def: 5000 }, notificationTimeoutLow: { def: 5000 },
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationIgnoreAppTimeout: { def: false },
notificationCompactMode: { def: false }, notificationCompactMode: { def: false },
notificationShowTimeoutBar: { def: false }, notificationShowTimeoutBar: { def: false },
notificationDedupeEnabled: { def: true }, notificationDedupeEnabled: { def: true },
+23 -20
View File
@@ -2,6 +2,21 @@
.import "./SettingsSpec.js" as SpecModule .import "./SettingsSpec.js" as SpecModule
var PIN_KEYS = ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"];
function extractPins(obj) {
if (!obj) return null;
var pins = null;
for (var i = 0; i < PIN_KEYS.length; i++) {
var value = obj[PIN_KEYS[i]];
if (!value || Object.keys(value).length === 0) continue;
if (!pins) pins = {};
pins[PIN_KEYS[i]] = value;
}
return pins;
}
function parse(root, jsonObj) { function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC; var SPEC = SpecModule.SPEC;
@@ -263,28 +278,16 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 12; settings.configVersion = 12;
} }
return settings; if (currentVersion < 13) {
} console.info("Migrating settings from version", currentVersion, "to version 13");
console.info("Moving device and network pins to cache.json");
function cleanup(fileText) { for (var p = 0; p < PIN_KEYS.length; p++) {
var getValidKeys = SpecModule.getValidKeys; delete settings[PIN_KEYS[p]];
if (!fileText || !fileText.trim()) return;
try {
var settings = JSON.parse(fileText);
var validKeys = getValidKeys();
var needsSave = false;
for (var key in settings) {
if (validKeys.indexOf(key) < 0) {
delete settings[key];
needsSave = true;
}
} }
return needsSave ? JSON.stringify(settings, null, 2) : null; settings.configVersion = 13;
} catch (e) {
console.warn("SettingsData: Failed to cleanup unused keys:", e.message);
return null;
} }
return settings;
} }
-65
View File
@@ -122,11 +122,6 @@ const times = [
// adds a custom time to the times config // adds a custom time to the times config
function addTime(angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// calculations for sun times // calculations for sun times
const J0 = 0.0009; const J0 = 0.0009;
@@ -246,63 +241,3 @@ function getMoonIllumination(date) {
angle angle
}; };
}; };
function hoursLater(date, h) {
return new Date(date.valueOf() + h * dayMs / 24);
}
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
function getMoonTimes(date, lat, lng, inUTC) {
const t = new Date(date);
if (inUTC) t.setUTCHours(0, 0, 0, 0);
else t.setHours(0, 0, 0, 0);
const hc = 0.133 * rad;
let h0 = getMoonPosition(t, lat, lng).altitude - hc,
rise, set, ye;
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
for (let i = 1; i <= 24; i += 2) {
const h1 = getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
const h2 = getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
const a = (h0 + h2) / 2 - h1;
const b = (h2 - h0) / 2;
const xe = -b / (2 * a);
const d = b * b - 4 * a * h1;
let roots = 0, x1 = 0, x2 = 0;
ye = (a * xe + b) * xe + h1;
if (d >= 0) {
const dx = Math.sqrt(d) / (Math.abs(a) * 2);
x1 = xe - dx;
x2 = xe + dx;
if (Math.abs(x1) <= 1) roots++;
if (Math.abs(x2) <= 1) roots++;
if (x1 < -1) x1 = x2;
}
if (roots === 1) {
if (h0 < 0) rise = i + x1;
else set = i + x1;
} else if (roots === 2) {
rise = i + (ye < 0 ? x2 : x1);
set = i + (ye < 0 ? x1 : x2);
}
if (rise && set) break;
h0 = h2;
}
const result = {};
if (rise) result.rise = hoursLater(t, rise);
if (set) result.set = hoursLater(t, set);
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
return result;
};
+33
View File
@@ -233,6 +233,11 @@ Item {
PolkitService.polkitAvailable; PolkitService.polkitAvailable;
DisplayConfigState.hasOutputBackend; DisplayConfigState.hasOutputBackend;
PortalService.systemColorScheme; PortalService.systemColorScheme;
IconThemeService.revision;
DesktopService.isSystemd;
TrashService.count;
WallpaperCyclingService.cyclingActive;
ThemeAutoService.active;
} }
Loader { Loader {
@@ -314,6 +319,17 @@ Item {
} }
} }
Binding {
target: BarWidgetService
property: "dockContextMenu"
value: dockContextMenuLoader.item
}
Binding {
target: BarWidgetService
property: "dockTrashContextMenu"
value: dockTrashContextMenuLoader.item
}
Connections { Connections {
target: SettingsData target: SettingsData
function onDockShowTrashChanged() { function onDockShowTrashChanged() {
@@ -429,6 +445,23 @@ Item {
} }
} }
LazyLoader {
id: qrGeneratorModalLoader
active: false
Component.onCompleted: {
PopoutService.qrGeneratorModalLoader = qrGeneratorModalLoader;
}
QRGeneratorModal {
id: qrGeneratorModalItem
Component.onCompleted: {
PopoutService.qrGeneratorModal = qrGeneratorModalItem;
}
}
}
LazyLoader { LazyLoader {
id: polkitAuthModalLoader id: polkitAuthModalLoader
active: false active: false
+75 -46
View File
@@ -25,32 +25,30 @@ Item {
required property var windowRuleModalLoader required property var windowRuleModalLoader
function getPreferredBar(refPropertyName) { function getPreferredBar(refPropertyName) {
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
return null;
const focusedScreenName = BarWidgetService.getFocusedScreenName(); const focusedScreenName = BarWidgetService.getFocusedScreenName();
const loaders = Array.from({ const bars = [];
length: root.dankBarRepeater.count if (root.dankBarRepeater) {
}, (_, i) => root.dankBarRepeater.itemAt(i)); for (let i = 0; i < root.dankBarRepeater.count; i++)
bars.push(...(root.dankBarRepeater.itemAt(i)?.item?.barVariants?.instances || []));
}
const frameBars = BarWidgetService.frameHostedBars;
for (const screenName in frameBars)
bars.push(frameBars[screenName]);
let currentBar = null; let currentBar = null;
for (const bar of bars) {
if (!bar)
continue;
for (const loader of loaders) { const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
const instances = loader?.item?.barVariants?.instances || []; const hasRef = !refPropertyName || !!bar[refPropertyName];
for (const bar of instances) {
if (!bar)
continue;
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName; if (hasRef) {
const hasRef = !refPropertyName || !!bar[refPropertyName]; currentBar = bar;
if (hasRef) { if (onFocusedScreen)
currentBar = bar; break;
if (onFocusedScreen)
break;
}
} }
} }
@@ -285,28 +283,66 @@ Item {
} }
} }
function _resolvePosition(position) {
switch ((position || "").toLowerCase()) {
case "left":
return "left";
case "center":
return "center";
case "right":
return "right";
default:
return "";
}
}
function _dashBar(position) {
if (position)
return root.getPreferredBar();
return root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
}
function _openDash(tab, position) {
const bar = _dashBar(position);
if (!bar)
return false;
const tabId = _resolveTabId(tab);
const dash = root.dankDashPopoutLoader.item;
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) {
if (position && bar.positionDash)
bar.positionDash(dash, position);
dash.requestTab(tabId);
if (dash.updateSurfacePosition)
dash.updateSurfacePosition();
return true;
}
return bar.triggerDashTab(tabId, position);
}
function _toggleDash(tab, position) {
if (root.dankDashPopoutLoader.item?.dashVisible) {
root.dankDashPopoutLoader.item.dashVisible = false;
return true;
}
const bar = _dashBar(position);
if (!bar)
return false;
return bar.triggerDashTab(_resolveTabId(tab), position);
}
function resolveTabIndex(tab: string): int { function resolveTabIndex(tab: string): int {
return SettingsData.dashTabIndexForId(_resolveTabId(tab)); return SettingsData.dashTabIndexForId(_resolveTabId(tab));
} }
function open(tab: string): string { function open(tab: string): string {
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar(); return _openDash(tab, "") ? "DASH_OPEN_SUCCESS" : "DASH_OPEN_FAILED";
if (!bar) }
return "DASH_OPEN_FAILED";
const tabId = _resolveTabId(tab); function openAt(tab: string, position: string): string {
const dash = root.dankDashPopoutLoader.item; return _openDash(tab, _resolvePosition(position)) ? "DASH_OPEN_SUCCESS" : "DASH_OPEN_FAILED";
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) {
dash.requestTab(tabId);
if (dash.updateSurfacePosition)
dash.updateSurfacePosition();
return "DASH_OPEN_SUCCESS";
}
if (!bar.triggerDashTab(tabId))
return "DASH_OPEN_FAILED";
return "DASH_OPEN_SUCCESS";
} }
function close(): string { function close(): string {
@@ -318,18 +354,11 @@ Item {
} }
function toggle(tab: string): string { function toggle(tab: string): string {
if (root.dankDashPopoutLoader.item?.dashVisible) { return _toggleDash(tab, "") ? "DASH_TOGGLE_SUCCESS" : "DASH_TOGGLE_FAILED";
root.dankDashPopoutLoader.item.dashVisible = false; }
return "DASH_TOGGLE_SUCCESS";
}
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar(); function toggleAt(tab: string, position: string): string {
if (bar) { return _toggleDash(tab, _resolvePosition(position)) ? "DASH_TOGGLE_SUCCESS" : "DASH_TOGGLE_FAILED";
if (!bar.triggerDashTab(_resolveTabId(tab)))
return "DASH_TOGGLE_FAILED";
return "DASH_TOGGLE_SUCCESS";
}
return "DASH_TOGGLE_FAILED";
} }
target: "dash" target: "dash"
@@ -314,26 +314,20 @@ Item {
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
function _frameEdgeInset(side) {
if (!effectiveScreen)
return 0;
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
}
readonly property real _connectedAlignedX: { readonly property real _connectedAlignedX: {
switch (resolvedConnectedBarSide) { switch (resolvedConnectedBarSide) {
case "top": case "top":
case "bottom": case "bottom":
{ {
const insetL = _frameEdgeInset("left"); const insetL = SettingsData.frameEdgeInsetForSide(effectiveScreen, "left");
const insetR = _frameEdgeInset("right"); const insetR = SettingsData.frameEdgeInsetForSide(effectiveScreen, "right");
const usable = Math.max(0, screenWidth - insetL - insetR); const usable = Math.max(0, screenWidth - insetL - insetR);
return insetL + Math.max(0, (usable - alignedWidth) / 2); return insetL + Math.max(0, (usable - alignedWidth) / 2);
} }
case "left": case "left":
return _frameEdgeInset("left"); return SettingsData.frameEdgeInsetForSide(effectiveScreen, "left");
case "right": case "right":
return screenWidth - alignedWidth - _frameEdgeInset("right"); return screenWidth - alignedWidth - SettingsData.frameEdgeInsetForSide(effectiveScreen, "right");
} }
return 0; return 0;
} }
@@ -341,14 +335,14 @@ Item {
readonly property real _connectedAlignedY: { readonly property real _connectedAlignedY: {
switch (resolvedConnectedBarSide) { switch (resolvedConnectedBarSide) {
case "top": case "top":
return _frameEdgeInset("top"); return SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
case "bottom": case "bottom":
return screenHeight - alignedHeight - _frameEdgeInset("bottom"); return screenHeight - alignedHeight - SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
case "left": case "left":
case "right": case "right":
{ {
const insetT = _frameEdgeInset("top"); const insetT = SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
const insetB = _frameEdgeInset("bottom"); const insetB = SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
const usable = Math.max(0, screenHeight - insetT - insetB); const usable = Math.max(0, screenHeight - insetT - insetB);
return insetT + Math.max(0, (usable - alignedHeight) / 2); return insetT + Math.max(0, (usable - alignedHeight) / 2);
} }
@@ -105,12 +105,6 @@ Item {
} }
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide) readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
function _frameEdgeInset(side) {
if (!effectiveScreen)
return 0;
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
}
readonly property var _connectedModalPos: { readonly property var _connectedModalPos: {
const fallback = { const fallback = {
"x": (screenWidth - modalWidth) / 2, "x": (screenWidth - modalWidth) / 2,
@@ -120,10 +114,10 @@ Item {
case "top": case "top":
case "bottom": case "bottom":
{ {
const insetL = _frameEdgeInset("left"); const insetL = SettingsData.frameEdgeInsetForSide(effectiveScreen, "left");
const insetR = _frameEdgeInset("right"); const insetR = SettingsData.frameEdgeInsetForSide(effectiveScreen, "right");
const insetT = _frameEdgeInset("top"); const insetT = SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
const insetB = _frameEdgeInset("bottom"); const insetB = SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
const usable = Math.max(0, screenWidth - insetL - insetR); const usable = Math.max(0, screenWidth - insetL - insetR);
const usableH = Math.max(0, screenHeight - insetT - insetB); const usableH = Math.max(0, screenHeight - insetT - insetB);
return { return {
@@ -134,11 +128,11 @@ Item {
case "left": case "left":
case "right": case "right":
{ {
const insetT = _frameEdgeInset("top"); const insetT = SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
const insetB = _frameEdgeInset("bottom"); const insetB = SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
const usable = Math.max(0, screenHeight - insetT - insetB); const usable = Math.max(0, screenHeight - insetT - insetB);
return { return {
"x": resolvedConnectedBarSide === "left" ? _frameEdgeInset("left") : screenWidth - modalWidth - _frameEdgeInset("right"), "x": resolvedConnectedBarSide === "left" ? SettingsData.frameEdgeInsetForSide(effectiveScreen, "left") : screenWidth - modalWidth - SettingsData.frameEdgeInsetForSide(effectiveScreen, "right"),
"y": insetT + Math.max(0, (usable - modalHeight) / 2) "y": insetT + Math.max(0, (usable - modalHeight) / 2)
}; };
} }
@@ -188,16 +182,16 @@ Item {
readonly property real _connectedChromeY: { readonly property real _connectedChromeY: {
if (!launcherArcExtenderActive) if (!launcherArcExtenderActive)
return alignedY; return alignedY;
return resolvedConnectedBarSide === "top" ? Theme.snap(_frameEdgeInset("top"), dpr) : alignedY; return resolvedConnectedBarSide === "top" ? Theme.snap(SettingsData.frameEdgeInsetForSide(effectiveScreen, "top"), dpr) : alignedY;
} }
readonly property real _connectedChromeWidth: alignedWidth readonly property real _connectedChromeWidth: alignedWidth
readonly property real _connectedChromeHeight: { readonly property real _connectedChromeHeight: {
if (!launcherArcExtenderActive) if (!launcherArcExtenderActive)
return alignedHeight; return alignedHeight;
if (resolvedConnectedBarSide === "top") if (resolvedConnectedBarSide === "top")
return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - _frameEdgeInset("top")), dpr); return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - SettingsData.frameEdgeInsetForSide(effectiveScreen, "top")), dpr);
if (resolvedConnectedBarSide === "bottom") if (resolvedConnectedBarSide === "bottom")
return Theme.snap(Math.max(alignedHeight, screenHeight - _frameEdgeInset("bottom") - alignedY), dpr); return Theme.snap(Math.max(alignedHeight, screenHeight - SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom") - alignedY), dpr);
return alignedHeight; return alignedHeight;
} }
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
@@ -89,15 +89,6 @@ function fuzzyScore(text, query) {
return bestScore return bestScore
} }
function getTimeBucketWeight(daysSinceUsed) {
for (var i = 0; i < TimeBuckets.length; i++) {
if (daysSinceUsed <= TimeBuckets[i].maxDays) {
return TimeBuckets[i].weight
}
}
return 10
}
function calculateTextScore(name, query) { function calculateTextScore(name, query) {
if (name === query) return Weights.exactMatch if (name === query) return Weights.exactMatch
if (name.startsWith(query)) return Weights.prefixMatch if (name.startsWith(query)) return Weights.prefixMatch
+9
View File
@@ -64,6 +64,10 @@ DankModal {
NotificationService.dismissAllPopups(); NotificationService.dismissAllPopups();
} }
function dismissLastNotification() {
NotificationService.dismissLastNotification();
}
modalWidth: Math.min(500, screenWidth - 48) modalWidth: Math.min(500, screenWidth - 48)
modalHeight: Math.min(700, screenHeight * 0.85) modalHeight: Math.min(700, screenHeight * 0.85)
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
@@ -190,6 +194,11 @@ DankModal {
return "NOTIFICATION_MODAL_DISMISS_ALL_POPUPS_SUCCESS"; return "NOTIFICATION_MODAL_DISMISS_ALL_POPUPS_SUCCESS";
} }
function dismiss(): string {
notificationModal.dismissLastNotification();
return "NOTIFICATION_DISMISS_SUCCESS";
}
target: "notifications" target: "notifications"
} }
+14 -1
View File
@@ -18,6 +18,19 @@ DankModal {
open(); open();
} }
function getPowerProfileDescription(profile) {
switch (profile) {
case 0:
return I18n.tr("Extend battery life", "power profile description");
case 1:
return I18n.tr("Balance power and performance", "power profile description");
case 2:
return I18n.tr("Prioritize performance", "power profile description");
default:
return I18n.tr("Custom power profile", "power profile description");
}
}
function hideDialog() { function hideDialog() {
close(); close();
} }
@@ -244,7 +257,7 @@ DankModal {
// Selected power profile description // Selected power profile description
StyledText { StyledText {
text: (root.selectedIndex >= 0 && root.selectedIndex < root.profileModel.length) ? Theme.getPowerProfileDescription(root.profileModel[root.selectedIndex]) : "" text: (root.selectedIndex >= 0 && root.selectedIndex < root.profileModel.length) ? root.getPowerProfileDescription(root.profileModel[root.selectedIndex]) : ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
+3 -12
View File
@@ -6,6 +6,7 @@ import qs.Common
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../Common/Format.js" as Format
FloatingWindow { FloatingWindow {
id: processListModal id: processListModal
@@ -67,16 +68,6 @@ FloatingWindow {
show(); show();
} }
function formatBytes(bytes) {
if (bytes < 1024)
return bytes.toFixed(0) + " B/s";
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(1) + " KB/s";
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
}
function nextTab() { function nextTab() {
currentTab = (currentTab + 1) % 4; currentTab = (currentTab + 1) % 4;
} }
@@ -541,7 +532,7 @@ FloatingWindow {
} }
StyledText { StyledText {
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate) text: "↓" + Format.formatRate(DgopService.networkRxRate) + " ↑" + Format.formatRate(DgopService.networkTxRate)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
color: Theme.surfaceText color: Theme.surfaceText
@@ -559,7 +550,7 @@ FloatingWindow {
} }
StyledText { StyledText {
text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate) text: "↓" + Format.formatRate(DgopService.diskReadRate) + " ↑" + Format.formatRate(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
color: Theme.surfaceText color: Theme.surfaceText
+285
View File
@@ -0,0 +1,285 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
visible: false
layerNamespace: "dms:qr-generator"
property bool disablePopupTransparency: true
property bool generating: false
property string themedQrCodePath: ""
property string normalQrCodePath: ""
property string initialText: ""
property string _pendingPayload: ""
property string _generatingPayload: ""
property string _displayedPayload: ""
modalWidth: 420
modalHeight: 440
onBackgroundClicked: hide()
onOpened: {
Qt.callLater(() => {
modalFocusScope.forceActiveFocus();
const item = contentLoader.item;
if (!item)
return;
item.saveBrowserLoader = saveBrowserLoader;
if (item.textInput) {
item.textInput.text = initialText;
item.textInput.forceActiveFocus();
}
if (initialText.length > 0) {
_pendingPayload = initialText;
generateQR(initialText);
}
});
}
function show(text) {
generating = false;
initialText = text || "";
_pendingPayload = "";
_generatingPayload = "";
_displayedPayload = "";
themedQrCodePath = "";
normalQrCodePath = "";
open();
}
function hide() {
deleteQrCodeFiles(themedQrCodePath, normalQrCodePath);
themedQrCodePath = "";
normalQrCodePath = "";
close();
}
function deleteQrCodeFiles(themed, normal) {
if (themed.length > 0)
DMSService.sendRequest("network.delete-qrcode", {
path: themed
});
if (normal.length > 0)
DMSService.sendRequest("network.delete-qrcode", {
path: normal
});
}
Timer {
id: genTimer
interval: 200
repeat: false
onTriggered: root.generateQR(root._pendingPayload)
}
function generateQR(text) {
const trimmed = (text || "").trim();
if (trimmed.length === 0 || trimmed === _displayedPayload || generating)
return;
_generatingPayload = trimmed;
generating = true;
DMSService.sendRequest("network.generate-qrcode", {
text: trimmed
}, response => {
root.generating = false;
if (response.error) {
ToastService.showError(I18n.tr("Failed to generate QR code: %1").arg(JSON.stringify(response.error)));
return;
}
if (!response.result)
return;
if (root._generatingPayload !== root._pendingPayload.trim()) {
root.deleteQrCodeFiles(response.result[0], response.result[1]);
genTimer.restart();
return;
}
const oldThemed = root.themedQrCodePath;
const oldNormal = root.normalQrCodePath;
root._displayedPayload = root._generatingPayload;
root.themedQrCodePath = response.result[0];
root.normalQrCodePath = response.result[1];
root.deleteQrCodeFiles(oldThemed, oldNormal);
});
}
function onTextChanged(text) {
_pendingPayload = text;
const trimmed = text.trim();
if (trimmed.length === 0 || trimmed === _displayedPayload) {
genTimer.stop();
return;
}
genTimer.restart();
}
LazyLoader {
id: saveBrowserLoader
active: false
FileBrowserSurfaceModal {
id: saveBrowser
browserTitle: I18n.tr("Save QR Code")
browserIcon: "qr_code"
browserType: "default"
fileExtensions: ["*.png"]
allowStacking: true
saveMode: true
defaultFileName: "qrcode.png"
onFileSelected: path => {
const cleanPath = decodeURI(path.toString().replace(/^file:\/\//, ''));
copyQrCodeProcess.exec(["cp", "-f", root.normalQrCodePath, cleanPath]);
}
Process {
id: copyQrCodeProcess
stdout: StdioCollector {
onStreamFinished: {
saveBrowser.close();
}
}
}
}
}
content: Component {
Item {
id: contentItem
property alias textInput: textInput
property var saveBrowserLoader: null
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
RowLayout {
id: modalTitle
width: parent.width
StyledText {
text: I18n.tr("QR Generator")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Bold
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
Layout.alignment: Qt.AlignRight
}
}
DankTextField {
id: textInput
width: parent.width
placeholderText: I18n.tr("Enter text to encode")
showClearButton: true
focus: true
onTextEdited: root.onTextChanged(text)
Keys.onEscapePressed: event => {
event.accepted = true;
root.hide();
}
}
Item {
id: qrContainer
height: Math.min(parent.height - parent.spacing - modalTitle.height - textInput.height - parent.spacing * 4, 260)
width: height
anchors.horizontalCenter: parent.horizontalCenter
opacity: 1
Behavior on opacity {
NumberAnimation {
duration: 80
easing.type: Easing.OutCubic
}
}
Image {
id: qrCodeImg
anchors.fill: parent
source: root.themedQrCodePath
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: false
visible: false
onSourceChanged: qrContainer.opacity = 0
onStatusChanged: {
if (status === Image.Ready)
qrContainer.opacity = 1;
}
}
MultiEffect {
source: qrCodeImg
anchors.fill: qrCodeImg
colorization: 1.0
colorizationColor: Theme.primary
}
}
RowLayout {
width: parent.width
visible: root.themedQrCodePath.length > 0
Layout.alignment: Qt.AlignHCenter
Item {
Layout.fillWidth: true
}
DankButton {
text: I18n.tr("Save")
iconName: "save"
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
onClicked: {
contentItem.saveBrowserLoader.active = true;
if (contentItem.saveBrowserLoader.item) {
contentItem.saveBrowserLoader.item.open();
}
}
}
DankButton {
text: I18n.tr("Copy")
iconName: "content_copy"
backgroundColor: Theme.primary
textColor: Theme.onPrimary
onClicked: {
if (root.normalQrCodePath.length > 0)
DMSService.sendRequest("clipboard.copyFile", {
filePath: root.normalQrCodePath
});
}
}
Item {
Layout.fillWidth: true
}
}
}
}
}
}
@@ -718,5 +718,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: mouseTouchpadLoader
anchors.fill: parent
active: root.currentIndex === 44
visible: active
focus: active
sourceComponent: MouseTouchpadTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }
+11 -5
View File
@@ -41,6 +41,9 @@ FloatingWindow {
signal closingModal signal closingModal
function show() { function show() {
if (visible && !backingWindowVisible) {
visible = false;
}
visible = true; visible = true;
} }
@@ -49,7 +52,11 @@ FloatingWindow {
} }
function toggle() { function toggle() {
visible = !visible; if (visible && backingWindowVisible) {
hide();
return;
}
show();
} }
function setTabIndex(tabIndex: int) { function setTabIndex(tabIndex: int) {
@@ -61,13 +68,12 @@ FloatingWindow {
function showWithTab(tabIndex: int) { function showWithTab(tabIndex: int) {
setTabIndex(tabIndex); setTabIndex(tabIndex);
visible = true; show();
} }
function showWithTabName(tabName: string) { function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName); setTabIndex(sidebar.resolveTabIndex(tabName));
setTabIndex(idx); show();
visible = true;
} }
function resolveTabIndex(tabName: string): int { function resolveTabIndex(tabName: string): int {
@@ -319,6 +319,13 @@ Rectangle {
"icon": "headphones", "icon": "headphones",
"tabIndex": 29 "tabIndex": 29
}, },
{
"id": "mouse_touchpad",
"text": I18n.tr("Mouse & Touchpad"),
"icon": "mouse",
"tabIndex": 44,
"niriOnly": true
},
{ {
"id": "locale", "id": "locale",
"text": I18n.tr("Locale"), "text": I18n.tr("Locale"),
+83 -3
View File
@@ -31,6 +31,7 @@ DankModal {
property string promptToken: "" property string promptToken: ""
property string promptReason: "" property string promptReason: ""
property var promptFields: [] property var promptFields: []
property var promptHints: []
property string promptSetting: "" property string promptSetting: ""
property bool isVpnPrompt: false property bool isVpnPrompt: false
@@ -40,17 +41,21 @@ DankModal {
property var fieldsInfo: [] property var fieldsInfo: []
property var secretValues: ({}) property var secretValues: ({})
readonly property bool isCertificateChangedPrompt: promptReason === "server-certificate-changed"
readonly property bool isCertificatePrompt: promptReason === "server-certificate" || isCertificateChangedPrompt
readonly property string serverCertificateFingerprint: promptHints.length > 0 ? promptHints[0] : ""
readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0 readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
readonly property bool showPasswordField: fieldsInfo.length === 0 readonly property bool showPasswordField: fieldsInfo.length === 0 && !isCertificatePrompt
readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt
readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" && !isCertificatePrompt
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
readonly property int inputFieldWithSpacing: inputFieldHeight + Theme.spacingM readonly property int inputFieldWithSpacing: inputFieldHeight + Theme.spacingM
readonly property int checkboxRowHeight: Theme.fontSizeMedium + Theme.spacingS readonly property int checkboxRowHeight: Theme.fontSizeMedium + Theme.spacingS
readonly property int headerHeight: Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingM * 2 readonly property int headerHeight: Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingM * 2
readonly property int buttonRowHeight: 36 + Theme.spacingM readonly property int buttonRowHeight: 36 + Theme.spacingM
readonly property int certificateWarningHeight: certificateWarningColumn.implicitHeight + Theme.spacingM * 2
property int calculatedHeight: { property int calculatedHeight: {
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2; let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
@@ -67,10 +72,16 @@ DankModal {
h += inputFieldWithSpacing; h += inputFieldWithSpacing;
if (showSavePasswordCheckbox) if (showSavePasswordCheckbox)
h += checkboxRowHeight; h += checkboxRowHeight;
if (isCertificatePrompt)
h += certificateWarningHeight + Theme.spacingM;
return h; return h;
} }
function focusFirstField() { function focusFirstField() {
if (isCertificatePrompt) {
connectButton.forceActiveFocus();
return;
}
if (fieldsInfo.length > 0) { if (fieldsInfo.length > 0) {
if (dynamicFieldsRepeater.count > 0) { if (dynamicFieldsRepeater.count > 0) {
const firstItem = dynamicFieldsRepeater.itemAt(0); const firstItem = dynamicFieldsRepeater.itemAt(0);
@@ -101,6 +112,7 @@ DankModal {
promptToken = ""; promptToken = "";
promptReason = ""; promptReason = "";
promptFields = []; promptFields = [];
promptHints = [];
promptSetting = ""; promptSetting = "";
isVpnPrompt = false; isVpnPrompt = false;
connectionName = ""; connectionName = "";
@@ -127,6 +139,7 @@ DankModal {
promptToken = ""; promptToken = "";
promptReason = ""; promptReason = "";
promptFields = []; promptFields = [];
promptHints = [];
promptSetting = ""; promptSetting = "";
isVpnPrompt = false; isVpnPrompt = false;
connectionName = ""; connectionName = "";
@@ -145,6 +158,7 @@ DankModal {
promptToken = token; promptToken = token;
promptReason = reason; promptReason = reason;
promptFields = fields || []; promptFields = fields || [];
promptHints = hints || [];
promptSetting = setting || "802-11-wireless-security"; promptSetting = setting || "802-11-wireless-security";
connectionType = connType || "802-11-wireless"; connectionType = connType || "802-11-wireless";
connectionName = connName || ssid || ""; connectionName = connName || ssid || "";
@@ -324,6 +338,8 @@ DankModal {
text: { text: {
if (promptReason === "pkcs11") if (promptReason === "pkcs11")
return I18n.tr("Smartcard Authentication"); return I18n.tr("Smartcard Authentication");
if (isCertificatePrompt)
return I18n.tr("Untrusted VPN certificate", "Title for VPN server certificate trust confirmation");
if (isVpnPrompt) if (isVpnPrompt)
return I18n.tr("Connect to VPN"); return I18n.tr("Connect to VPN");
if (isHiddenNetwork) if (isHiddenNetwork)
@@ -343,6 +359,8 @@ DankModal {
text: { text: {
if (promptReason === "pkcs11") if (promptReason === "pkcs11")
return I18n.tr("Enter PIN for ") + wifiPasswordSSID; return I18n.tr("Enter PIN for ") + wifiPasswordSSID;
if (isCertificatePrompt)
return wifiPasswordSSID;
if (fieldsInfo.length > 0) if (fieldsInfo.length > 0)
return I18n.tr("Enter credentials for ") + wifiPasswordSSID; return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
if (isVpnPrompt) if (isVpnPrompt)
@@ -383,6 +401,45 @@ DankModal {
} }
} }
Rectangle {
id: certificateWarningBox
readonly property color warningTone: isCertificateChangedPrompt ? Theme.error : Theme.warning
width: parent.width
height: certificateWarningHeight
radius: Theme.cornerRadius
color: Theme.withAlpha(warningTone, 0.12)
border.color: Theme.withAlpha(warningTone, 0.5)
border.width: 1
visible: isCertificatePrompt
Column {
id: certificateWarningColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
StyledText {
width: parent.width
text: isCertificateChangedPrompt ? I18n.tr("The server certificate has changed since it was last trusted. Only continue if you recognize the new fingerprint.", "Warning shown when a trusted VPN server certificate no longer matches") : I18n.tr("Only continue if you recognize this server certificate fingerprint.", "Warning shown before trusting an unverified VPN server certificate")
wrapMode: Text.Wrap
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: serverCertificateFingerprint
wrapMode: Text.WrapAnywhere
font.family: SettingsData.monoFontFamily
font.pixelSize: Theme.fontSizeSmall
color: certificateWarningBox.warningTone
}
}
}
Rectangle { Rectangle {
width: parent.width width: parent.width
height: inputFieldHeight height: inputFieldHeight
@@ -690,10 +747,15 @@ DankModal {
} }
Rectangle { Rectangle {
id: connectButton
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2) width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
border.color: activeFocus ? Theme.surfaceText : "transparent"
border.width: activeFocus ? 2 : 0
activeFocusOnTab: true
enabled: { enabled: {
if (fieldsInfo.length > 0) { if (fieldsInfo.length > 0) {
for (var i = 0; i < fieldsInfo.length; i++) { for (var i = 0; i < fieldsInfo.length; i++) {
@@ -705,6 +767,8 @@ DankModal {
} }
return true; return true;
} }
if (isCertificatePrompt)
return serverCertificateFingerprint.length > 0;
if (isVpnPrompt) if (isVpnPrompt)
return passwordInput.text.length > 0; return passwordInput.text.length > 0;
if (isHiddenNetwork) if (isHiddenNetwork)
@@ -716,7 +780,7 @@ DankModal {
StyledText { StyledText {
id: connectText id: connectText
anchors.centerIn: parent anchors.centerIn: parent
text: I18n.tr("Connect") text: isCertificatePrompt ? I18n.tr("Trust", "Button that approves a VPN server certificate fingerprint") : I18n.tr("Connect")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.background color: Theme.background
font.weight: Font.Medium font.weight: Font.Medium
@@ -731,6 +795,22 @@ DankModal {
onClicked: submitCredentialsAndClose() onClicked: submitCredentialsAndClose()
} }
Keys.onReturnPressed: event => {
if (enabled)
submitCredentialsAndClose();
event.accepted = true;
}
Keys.onEnterPressed: event => {
if (enabled)
submitCredentialsAndClose();
event.accepted = true;
}
Keys.onSpacePressed: event => {
if (enabled)
submitCredentialsAndClose();
event.accepted = true;
}
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
+2 -25
View File
@@ -25,29 +25,6 @@ Item {
property bool effectActive: false property bool effectActive: false
property bool useNextForEffect: false property bool useNextForEffect: false
function getFillMode(modeName) {
switch (modeName) {
case "Stretch":
return Image.Stretch;
case "Fit":
case "PreserveAspectFit":
return Image.PreserveAspectFit;
case "Fill":
case "PreserveAspectCrop":
return Image.PreserveAspectCrop;
case "Tile":
return Image.Tile;
case "TileVertically":
return Image.TileVertically;
case "TileHorizontally":
return Image.TileHorizontally;
case "Pad":
return Image.Pad;
default:
return Image.PreserveAspectCrop;
}
}
Component.onCompleted: { Component.onCompleted: {
if (initialSource && initialSource !== wallpaperSource && !(CompositorService.isNiri && SessionData.isSwitchingMode)) { if (initialSource && initialSource !== wallpaperSource && !(CompositorService.isNiri && SessionData.isSwitchingMode)) {
currentWallpaper.source = initialSource; currentWallpaper.source = initialSource;
@@ -144,7 +121,7 @@ Item {
smooth: true smooth: true
cache: true cache: true
sourceSize: root.blurTextureSize sourceSize: root.blurTextureSize
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName)) fillMode: Theme.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) { if (status === Image.Error) {
@@ -166,7 +143,7 @@ Item {
smooth: true smooth: true
cache: true cache: true
sourceSize: root.blurTextureSize sourceSize: root.blurTextureSize
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName)) fillMode: Theme.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) { if (status === Image.Error) {
@@ -4,6 +4,7 @@ import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../../Common/Format.js" as Format
Item { Item {
id: root id: root
@@ -188,16 +189,6 @@ Item {
return DgopService.availableGpus.find(g => g.pciId === selectedGpuPciId); return DgopService.availableGpus.find(g => g.pciId === selectedGpuPciId);
} }
function formatBytes(bytes) {
if (bytes < 1024)
return bytes.toFixed(0) + "B";
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(0) + "K";
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(1) + "M";
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
}
function formatMemKB(kb) { function formatMemKB(kb) {
if (kb < 1024) if (kb < 1024)
return kb.toFixed(0) + "K"; return kb.toFixed(0) + "K";
@@ -206,26 +197,18 @@ Item {
return (kb / (1024 * 1024)).toFixed(1) + "G"; return (kb / (1024 * 1024)).toFixed(1) + "G";
} }
function addToHistory(arr, val) {
var newArr = arr.slice();
newArr.push(val);
if (newArr.length > historySize)
newArr.shift();
return newArr;
}
function sampleData() { function sampleData() {
if (showCpuGraph) if (showCpuGraph)
cpuHistory = addToHistory(cpuHistory, DgopService.cpuUsage); cpuHistory = Format.addToHistory(cpuHistory, DgopService.cpuUsage, historySize);
if (showMemoryGraph) if (showMemoryGraph)
memHistory = addToHistory(memHistory, DgopService.memoryUsage); memHistory = Format.addToHistory(memHistory, DgopService.memoryUsage, historySize);
if (showNetworkGraph) { if (showNetworkGraph) {
netRxHistory = addToHistory(netRxHistory, DgopService.networkRxRate); netRxHistory = Format.addToHistory(netRxHistory, DgopService.networkRxRate, historySize);
netTxHistory = addToHistory(netTxHistory, DgopService.networkTxRate); netTxHistory = Format.addToHistory(netTxHistory, DgopService.networkTxRate, historySize);
} }
if (showDisk) { if (showDisk) {
diskReadHistory = addToHistory(diskReadHistory, DgopService.diskReadRate); diskReadHistory = Format.addToHistory(diskReadHistory, DgopService.diskReadRate, historySize);
diskWriteHistory = addToHistory(diskWriteHistory, DgopService.diskWriteRate); diskWriteHistory = Format.addToHistory(diskWriteHistory, DgopService.diskWriteRate, historySize);
} }
} }
@@ -519,7 +502,7 @@ Item {
color: root.accentColor color: root.accentColor
} }
StyledText { StyledText {
text: root.formatBytes(DgopService.networkRxRate) + "/s" text: Format.formatBytes(DgopService.networkRxRate) + "/s"
isMonospace: true isMonospace: true
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: root.textColor color: root.textColor
@@ -533,7 +516,7 @@ Item {
color: root.dimColor color: root.dimColor
} }
StyledText { StyledText {
text: root.formatBytes(DgopService.networkTxRate) + "/s" text: Format.formatBytes(DgopService.networkTxRate) + "/s"
isMonospace: true isMonospace: true
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: root.textColor color: root.textColor
@@ -552,7 +535,7 @@ Item {
color: root.accentColor color: root.accentColor
} }
StyledText { StyledText {
text: root.formatBytes(DgopService.diskReadRate) + "/s" text: Format.formatBytes(DgopService.diskReadRate) + "/s"
isMonospace: true isMonospace: true
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: root.textColor color: root.textColor
@@ -566,7 +549,7 @@ Item {
color: root.dimColor color: root.dimColor
} }
StyledText { StyledText {
text: root.formatBytes(DgopService.diskWriteRate) + "/s" text: Format.formatBytes(DgopService.diskWriteRate) + "/s"
isMonospace: true isMonospace: true
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: root.textColor color: root.textColor
@@ -469,6 +469,8 @@ PluginComponent {
} }
MouseArea { MouseArea {
id: peerMouseArea
z: -1 z: -1
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Modules.ControlCenter.Details
import qs.Modules.Plugins import qs.Modules.Plugins
PluginComponent { PluginComponent {
@@ -1,131 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: ""
property string text: ""
property string secondaryText: ""
property bool isActive: false
property int widgetIndex: 0
property var widgetData: null
property bool editMode: false
signal clicked
width: parent ? parent.width : 200
height: 60
radius: {
if (Theme.cornerRadius === 0)
return 0;
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
}
readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing
color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: isActive ? 1 : Theme.layerOutlineWidth
opacity: enabled ? 1.0 : 0.6
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? hoverTint(root.color) : Theme.withAlpha(hoverTint(root.color), 0)
opacity: mouseArea.containsMouse ? 0.08 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL + 2
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: root.iconName
size: Theme.iconSize
color: isActive ? Theme.ccTileActiveText : Theme.ccTileInactiveIcon
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - Theme.iconSize - parent.spacing
height: parent.height
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
Typography {
width: parent.width
text: root.text
style: Typography.Style.Body
color: isActive ? Theme.ccTileActiveText : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
}
Typography {
width: parent.width
text: root.secondaryText
style: Typography.Style.Caption
color: isActive ? Theme.ccTileActiveText : Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
}
}
}
}
DankRipple {
id: ripple
cornerRadius: root.radius
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onPressed: mouse => ripple.trigger(mouse.x, mouse.y)
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -1,91 +0,0 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Details
Item {
id: root
property string expandedSection: ""
property var expandedWidgetData: null
height: active ? 250 : 0
visible: active
readonly property bool active: expandedSection !== ""
Loader {
anchors.fill: parent
anchors.topMargin: Theme.spacingS
sourceComponent: {
if (!root.active)
return null;
if (expandedSection.startsWith("diskUsage_")) {
return diskUsageDetailComponent;
}
switch (expandedSection) {
case "wifi":
return networkDetailComponent;
case "bluetooth":
return bluetoothDetailComponent;
case "audioOutput":
return audioOutputDetailComponent;
case "audioInput":
return audioInputDetailComponent;
case "battery":
return batteryDetailComponent;
default:
return null;
}
}
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
Component {
id: batteryDetailComponent
BatteryDetail {}
}
Component {
id: diskUsageDetailComponent
DiskUsageDetail {
currentMountPath: root.expandedWidgetData?.mountPath || "/"
instanceId: root.expandedWidgetData?.instanceId || ""
onMountPathChanged: newMountPath => {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || [];
const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w);
updatedWidget.mountPath = newMountPath;
return updatedWidget;
}
return w;
});
SettingsData.set("controlCenterWidgets", newWidgets);
}
}
}
}
}
@@ -1,55 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: ""
property string text: ""
signal pressed
height: 34
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceVariant, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: root.iconName
size: Theme.fontSizeSmall
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: root.text
style: Typography.Style.Button
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankRipple {
id: ripple
cornerRadius: root.radius
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => {
ripple.trigger(mouse.x, mouse.y);
root.pressed();
}
}
}
@@ -4,6 +4,7 @@ import Quickshell.Services.Pipewire
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../../../Common/QmlUtils.js" as QmlUtils
Rectangle { Rectangle {
id: root id: root
@@ -150,17 +151,9 @@ Rectangle {
property int maxPinnedInputs: 3 property int maxPinnedInputs: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedInputs() { function getPinnedInputs() {
const pins = SettingsData.audioInputDevicePins || {}; const pins = CacheData.audioInputDevicePins || {};
return normalizePinList(pins["preferredInput"]); return QmlUtils.normalizePinList(pins["preferredInput"]);
} }
Column { Column {
@@ -315,8 +308,8 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.audioInputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredInput"]); let pinnedList = QmlUtils.normalizePinList(pins["preferredInput"]);
const pinIndex = pinnedList.indexOf(modelData.name); const pinIndex = pinnedList.indexOf(modelData.name);
if (pinIndex !== -1) { if (pinIndex !== -1) {
@@ -332,7 +325,7 @@ Rectangle {
else else
delete pins["preferredInput"]; delete pins["preferredInput"];
SettingsData.set("audioInputDevicePins", pins); CacheData.set("audioInputDevicePins", pins);
} }
} }
} }
@@ -4,6 +4,7 @@ import Quickshell.Services.Pipewire
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../../../Common/QmlUtils.js" as QmlUtils
Rectangle { Rectangle {
id: root id: root
@@ -160,17 +161,9 @@ Rectangle {
property int maxPinnedOutputs: 3 property int maxPinnedOutputs: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedOutputs() { function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {}; const pins = CacheData.audioOutputDevicePins || {};
return normalizePinList(pins["preferredOutput"]); return QmlUtils.normalizePinList(pins["preferredOutput"]);
} }
Column { Column {
@@ -324,8 +317,8 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.audioOutputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]); let pinnedList = QmlUtils.normalizePinList(pins["preferredOutput"]);
const pinIndex = pinnedList.indexOf(modelData.name); const pinIndex = pinnedList.indexOf(modelData.name);
if (pinIndex !== -1) { if (pinIndex !== -1) {
@@ -341,7 +334,7 @@ Rectangle {
else else
delete pins["preferredOutput"]; delete pins["preferredOutput"];
SettingsData.set("audioOutputDevicePins", pins); CacheData.set("audioOutputDevicePins", pins);
} }
} }
} }
@@ -5,6 +5,7 @@ import Quickshell.Bluetooth
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import "../../../Common/QmlUtils.js" as QmlUtils
Rectangle { Rectangle {
id: root id: root
@@ -95,17 +96,9 @@ Rectangle {
BluetoothService.updateDeviceCodec(deviceAddress, codecName); BluetoothService.updateDeviceCodec(deviceAddress, codecName);
} }
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedDevices() { function getPinnedDevices() {
const pins = SettingsData.bluetoothDevicePins || {}; const pins = CacheData.bluetoothDevicePins || {};
return normalizePinList(pins["preferredDevice"]); return QmlUtils.normalizePinList(pins["preferredDevice"]);
} }
Row { Row {
@@ -395,8 +388,8 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.bluetoothDevicePins || {}));
let pinnedList = root.normalizePinList(pins["preferredDevice"]); let pinnedList = QmlUtils.normalizePinList(pins["preferredDevice"]);
const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address); const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
if (pinIndex !== -1) { if (pinIndex !== -1) {
@@ -413,7 +406,7 @@ Rectangle {
delete pins["preferredDevice"]; delete pins["preferredDevice"];
} }
SettingsData.set("bluetoothDevicePins", pins); CacheData.set("bluetoothDevicePins", pins);
} }
} }
} }
@@ -37,7 +37,7 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = devices.find(d => d.name === pinnedDevice); const found = devices.find(d => d.name === pinnedDevice);
@@ -85,12 +85,12 @@ Rectangle {
} }
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const existing = pins[pinKey]; const existing = pins[pinKey];
if (existing && existing !== deviceName) { if (existing && existing !== deviceName) {
const next = JSON.parse(JSON.stringify(pins)); const next = JSON.parse(JSON.stringify(pins));
delete next[pinKey]; delete next[pinKey];
SettingsData.set("brightnessDevicePins", next); CacheData.set("brightnessDevicePins", next);
} }
} }
root.currentDeviceName = deviceName; root.currentDeviceName = deviceName;
@@ -106,7 +106,7 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || !deviceName) if (!pinKey || !deviceName)
return false; return false;
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] === deviceName; return pins[pinKey] === deviceName;
} }
@@ -114,13 +114,13 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || !deviceName) if (!pinKey || !deviceName)
return; return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
if (pins[pinKey] === deviceName) { if (pins[pinKey] === deviceName) {
delete pins[pinKey]; delete pins[pinKey];
} else { } else {
pins[pinKey] = deviceName; pins[pinKey] = deviceName;
} }
SettingsData.set("brightnessDevicePins", pins); CacheData.set("brightnessDevicePins", pins);
} }
implicitHeight: { implicitHeight: {
@@ -269,7 +269,7 @@ Rectangle {
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName) readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
readonly property bool devicePinnedHere: { readonly property bool devicePinnedHere: {
SettingsData.brightnessDevicePins; CacheData.brightnessDevicePins;
return root.isDevicePinnedToScreen(modelData ? modelData.name : ""); return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
} }
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
import "../../../Common/Format.js" as Format
Rectangle { Rectangle {
id: root id: root
@@ -23,33 +24,8 @@ Rectangle {
onTriggered: root.nowMs = Date.now() onTriggered: root.nowMs = Date.now()
} }
function _pad2(n) {
return n < 10 ? "0" + n : "" + n;
}
function formatUntil(ts) {
if (!ts)
return "";
const d = new Date(ts);
const use24h = (typeof SettingsData !== "undefined") ? SettingsData.use24HourClock : true;
if (use24h)
return _pad2(d.getHours()) + ":" + _pad2(d.getMinutes());
const suffix = d.getHours() >= 12 ? "PM" : "AM";
const h12 = ((d.getHours() + 11) % 12) + 1;
return h12 + ":" + _pad2(d.getMinutes()) + " " + suffix;
}
function formatRemaining(ms) { function formatRemaining(ms) {
if (ms <= 0) return Format.formatRemaining(ms, "", I18n.tr("%1 min left"), I18n.tr("%1 h left"), I18n.tr("%1 h %2 m left"));
return "";
const totalMinutes = Math.ceil(ms / 60000);
if (totalMinutes < 60)
return I18n.tr("%1 min left").arg(totalMinutes);
const hours = Math.floor(totalMinutes / 60);
const mins = totalMinutes - hours * 60;
if (mins === 0)
return I18n.tr("%1 h left").arg(hours);
return I18n.tr("%1 h %2 m left").arg(hours).arg(mins);
} }
function minutesUntilTomorrowMorning() { function minutesUntilTomorrowMorning() {
@@ -125,7 +101,7 @@ Rectangle {
if (SessionData.doNotDisturbUntil <= 0) if (SessionData.doNotDisturbUntil <= 0)
return I18n.tr("On indefinitely"); return I18n.tr("On indefinitely");
const remaining = Math.max(0, SessionData.doNotDisturbUntil - root.nowMs); const remaining = Math.max(0, SessionData.doNotDisturbUntil - root.nowMs);
return root.formatRemaining(remaining) + " · " + I18n.tr("until %1").arg(root.formatUntil(SessionData.doNotDisturbUntil)); return root.formatRemaining(remaining) + " · " + I18n.tr("until %1").arg(Format.formatUntil(SessionData.doNotDisturbUntil, SettingsData.use24HourClock));
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -7,6 +7,7 @@ import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modals import qs.Modals
import qs.Modals.Common import qs.Modals.Common
import "../../../Common/QmlUtils.js" as QmlUtils
Rectangle { Rectangle {
id: root id: root
@@ -40,7 +41,11 @@ Rectangle {
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0 property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
property int maxPinnedNetworks: 3 property int maxPinnedNetworks: 3
readonly property int hotspotContentHeight: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable ? 56 + Theme.spacingS : 0 // Hosting on the only wifi adapter with no ethernet uplink just drops connectivity,
// so the hotspot row only shows where sharing can actually work (or is already on).
readonly property bool hotspotRelevant: NetworkService.hotspotEnabled || NetworkService.hotspotActivating || NetworkService.hotspotBusy || NetworkService.ethernetConnected || (NetworkService.wifiDevices?.length ?? 0) > 1
readonly property bool showHotspotRow: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable && hotspotRelevant
readonly property int hotspotContentHeight: showHotspotRow ? 56 + Theme.spacingS : 0
property var hotspotStartConfirm: ConfirmModal {} property var hotspotStartConfirm: ConfirmModal {}
@@ -66,17 +71,9 @@ Rectangle {
PopoutService.openSettingsWithTab("network_wifi"); PopoutService.openSettingsWithTab("network_wifi");
} }
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedNetworks() { function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {}; const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]); return QmlUtils.normalizePinList(pins["preferredWifi"]);
} }
property int currentPreferenceIndex: { property int currentPreferenceIndex: {
@@ -191,7 +188,7 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable visible: root.showHotspotRow
height: visible ? 56 : 0 height: visible ? 56 : 0
Rectangle { Rectangle {
@@ -907,8 +904,8 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = root.normalizePinList(pins["preferredWifi"]); let pinnedList = QmlUtils.normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(modelData.ssid); const pinIndex = pinnedList.indexOf(modelData.ssid);
if (pinIndex !== -1) { if (pinIndex !== -1) {
@@ -924,7 +921,7 @@ Rectangle {
else else
delete pins["preferredWifi"]; delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins); CacheData.set("wifiNetworkPins", pins);
} }
} }
} }
@@ -34,7 +34,7 @@ Row {
if (screenName && screenName.length > 0) { if (screenName && screenName.length > 0) {
const screen = Quickshell.screens.find(s => s.name === screenName); const screen = Quickshell.screens.find(s => s.name === screenName);
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName; const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice); const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
@@ -1,72 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: ""
property color iconColor: Theme.surfaceText
property string labelText: ""
property real value: 0.0
property real maximumValue: 1.0
property real minimumValue: 0.0
signal sliderValueChanged(real value)
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
opacity: enabled ? 1.0 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.right: sliderContainer.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.iconColor
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.labelText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
id: sliderContainer
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingM
width: 120
height: parent.height - Theme.spacingS * 2
DankSlider {
anchors.centerIn: parent
width: parent.width
enabled: root.enabled
minimum: Math.round(root.minimumValue * 100)
maximum: Math.round(root.maximumValue * 100)
value: Math.round(root.value * 100)
trackColor: Theme.ccSliderTrackColor
trackOpacity: Theme.ccSliderTrackOpacity
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
}
}
}
@@ -25,11 +25,6 @@ Rectangle {
height: 60 height: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _containerBg: Theme.ccPillInactiveBg readonly property color _containerBg: Theme.ccPillInactiveBg
color: { color: {
@@ -61,7 +56,7 @@ Rectangle {
radius: root.radius radius: root.radius
z: 0 z: 0
visible: false visible: false
color: hoverTint(_containerBg) color: Theme.hoverTint(_containerBg)
opacity: 0.08 opacity: 0.08
antialiasing: true antialiasing: true
Behavior on opacity { Behavior on opacity {
@@ -98,7 +93,7 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: _tileRadius radius: _tileRadius
color: hoverTint(iconTile.color) color: Theme.hoverTint(iconTile.color)
opacity: tileMouse.pressed ? 0.3 : (tileMouse.containsMouse ? 0.2 : 0.0) opacity: tileMouse.pressed ? 0.3 : (tileMouse.containsMouse ? 0.2 : 0.0)
visible: opacity > 0 visible: opacity > 0
antialiasing: true antialiasing: true
@@ -1,23 +0,0 @@
import QtQuick
Rectangle {
id: root
property string title: ""
property Component content: null
property bool isVisible: true
property int contentHeight: 300
width: parent ? parent.width : 400
implicitHeight: isVisible ? contentHeight : 0
height: implicitHeight
color: "transparent"
clip: true
Loader {
id: contentLoader
anchors.fill: parent
sourceComponent: root.content
asynchronous: true
}
}
@@ -22,11 +22,6 @@ Rectangle {
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4; return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
} }
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.ccPillInactiveBg readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
@@ -47,7 +42,7 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: hoverTint(root.color) color: Theme.hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0 visible: opacity > 0
antialiasing: true antialiasing: true
@@ -16,11 +16,6 @@ Rectangle {
height: 48 height: 48
radius: Theme.cornerRadius === 0 ? 0 : Theme.cornerRadius radius: Theme.cornerRadius === 0 ? 0 : Theme.cornerRadius
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
color: Theme.primary color: Theme.primary
border.color: Theme.ccTileRing border.color: Theme.ccTileRing
border.width: 1 border.width: 1
@@ -29,7 +24,7 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: hoverTint(root.color) color: Theme.hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0 visible: opacity > 0
antialiasing: true antialiasing: true
@@ -25,11 +25,6 @@ Rectangle {
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4; return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
} }
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.ccPillInactiveBg readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
@@ -50,7 +45,7 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: hoverTint(root.color) color: Theme.hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0 visible: opacity > 0
antialiasing: true antialiasing: true
@@ -34,11 +34,6 @@ Rectangle {
height: 48 height: 48
radius: Theme.cornerRadius + 4 radius: Theme.cornerRadius + 4
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _tileBg: Theme.ccPillInactiveBg readonly property color _tileBg: Theme.ccPillInactiveBg
color: mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBg color: mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBg
@@ -50,7 +45,7 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: hoverTint(root.color) color: Theme.hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0 visible: opacity > 0
antialiasing: true antialiasing: true
@@ -20,11 +20,6 @@ Rectangle {
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4; return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
} }
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.ccPillInactiveBg readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
@@ -45,7 +40,7 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: hoverTint(root.color) color: Theme.hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0 visible: opacity > 0
antialiasing: true antialiasing: true
@@ -39,17 +39,12 @@ Rectangle {
border.width: isActive ? 1 : Theme.layerOutlineWidth border.width: isActive ? 1 : Theme.layerOutlineWidth
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _containerBg: Theme.ccPillInactiveBg readonly property color _containerBg: Theme.ccPillInactiveBg
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: mouseArea.containsMouse ? hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0) color: mouseArea.containsMouse ? Theme.hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
opacity: mouseArea.containsMouse ? 0.08 : 0.0 opacity: mouseArea.containsMouse ? 0.08 : 0.0
Behavior on opacity { Behavior on opacity {
@@ -29,6 +29,8 @@ Item {
property var centerWidgets: [] property var centerWidgets: []
property int totalWidgets: 0 property int totalWidgets: 0
property real totalSize: 0 property real totalSize: 0
property real contentStart: 0
property real contentSize: 0
function updateLayout() { function updateLayout() {
if (SettingsData.centeringMode === "geometric") { if (SettingsData.centeringMode === "geometric") {
@@ -36,6 +38,25 @@ Item {
} else { } else {
applyIndexLayout(); applyIndexLayout();
} }
updateContentExtent();
}
function updateContentExtent() {
if (centerWidgets.length === 0) {
contentStart = 0;
contentSize = 0;
return;
}
let start = Infinity;
let end = -Infinity;
for (const widget of centerWidgets) {
const pos = isVertical ? widget.y : widget.x;
const size = isVertical ? widget.height : widget.width;
start = Math.min(start, pos);
end = Math.max(end, pos + size);
}
contentStart = start;
contentSize = end - start;
} }
function applyGeometricLayout() { function applyGeometricLayout() {
+14 -7
View File
@@ -13,6 +13,9 @@ Item {
signal colorPickerRequested signal colorPickerRequested
signal barReady(var barConfig) signal barReady(var barConfig)
Component.onCompleted: BarWidgetService.registerDankBarItem(root.barConfig?.id, root)
Component.onDestruction: BarWidgetService.unregisterDankBarItem(root.barConfig?.id, root)
property alias barVariants: barVariants property alias barVariants: barVariants
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property bool systemTrayMenuOpen: false property bool systemTrayMenuOpen: false
@@ -161,14 +164,18 @@ Item {
id: barVariants id: barVariants
model: { model: {
const prefs = root.barConfig?.screenPreferences || ["all"]; const prefs = root.barConfig?.screenPreferences || ["all"];
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) { const wantsAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
return Quickshell.screens; let base;
if (wantsAll) {
base = Quickshell.screens;
} else {
base = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs));
if (base.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1)
base = Quickshell.screens;
} }
const filtered = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs)); // Connected frame mode renders the bar inside the frame surface; skip the standalone window there
if (filtered.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1) { // unless this bar wants the overlay layer, which the frame surface cannot provide.
return Quickshell.screens; return base.filter(screen => !CompositorService.frameHostsBarForConfig(screen, root.barConfig));
}
return filtered;
} }
delegate: DankBarWindow { delegate: DankBarWindow {
File diff suppressed because it is too large Load Diff
+19 -25
View File
@@ -38,37 +38,43 @@ Item {
readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1) readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1)
readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin
readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw
// Connected-frame Bar Inset Padding: absolute free-end inset (auto < 0 = frameThickness, 0 = edge-to-edge). // Hosted bars span their edge fully; frameBarContentGap is the free-end gap measured from the
// FrameExclusions already moves a free bar end inward by frameThickness so use frame inset to mangage the gap // screen edge (auto = frameThickness, aligning widgets with the interior cutout).
readonly property real _frameInsetResolved: SettingsData.frameBarInsetPadding < 0 ? SettingsData.frameThickness : SettingsData.frameBarInsetPadding readonly property real _frameInsetResolved: SettingsData.frameBarContentGap
readonly property real _frameInsetExtra: Math.max(0, _frameInsetResolved - SettingsData.frameThickness) readonly property real _frameInsetExtra: SettingsData.frameBarContentGapExtra
// Horizontal bars span the full width and own the corners; the perpendicular vertical bar
// tucks in below/above. Where they meet, inset the corner widget so it centres in the
// frameBarSize corner cell, aligning it with the vertical bar's widget column.
readonly property real _widgetThicknessValue: _hasBarWindow ? barWindow.widgetThickness : 30
readonly property real _cornerAlignInset: Math.max(_frameInsetExtra, (SettingsData.frameBarSize - _widgetThicknessValue) / 2)
readonly property real _leftMargin: { readonly property real _leftMargin: {
if (_barIsVertical) if (_barIsVertical)
return _edgeBaseMargin; return _edgeBaseMargin;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentLeftBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentLeftBarLive ? _cornerAlignInset : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
readonly property real _rightMargin: { readonly property real _rightMargin: {
if (_barIsVertical) if (_barIsVertical)
return _edgeBaseMargin; return _edgeBaseMargin;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentRightBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentRightBarLive ? _cornerAlignInset : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
readonly property real _topMargin: { readonly property real _topMargin: {
if (!_barIsVertical) if (!_barIsVertical)
return 0; return 0;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentTopBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentTopBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
readonly property real _bottomMargin: { readonly property real _bottomMargin: {
if (!_barIsVertical) if (!_barIsVertical)
return 0; return 0;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentBottomBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentBottomBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
@@ -267,22 +273,6 @@ Item {
return ws.num !== -1 ? ws.num : ws.name; return ws.num !== -1 ? ws.num : ws.name;
} }
function escapeSwayWorkspaceName(name) {
return String(name ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}
function dispatchSwayWorkspace(ws) {
if (!ws)
return;
try {
if (ws.num !== undefined && ws.num !== -1) {
I3.dispatch(`workspace number ${ws.num}`);
} else if (ws.name) {
I3.dispatch(`workspace "${escapeSwayWorkspaceName(ws.name)}"`);
}
} catch (_) {}
}
function switchWorkspace(direction) { function switchWorkspace(direction) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) { if (realWorkspaces.length < 2) {
@@ -327,7 +317,7 @@ Item {
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
if (nextIndex !== validIndex) { if (nextIndex !== validIndex) {
dispatchSwayWorkspace(realWorkspaces[nextIndex]); CompositorService.dispatchSwayWorkspace(realWorkspaces[nextIndex]);
} }
} }
} }
@@ -677,6 +667,7 @@ Item {
LeftSection { LeftSection {
id: hLeftSection id: hLeftSection
objectName: "leftSection" objectName: "leftSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentLeftBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: false forceVerticalLayout: false
anchors { anchors {
@@ -710,6 +701,7 @@ Item {
RightSection { RightSection {
id: hRightSection id: hRightSection
objectName: "rightSection" objectName: "rightSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentRightBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: false forceVerticalLayout: false
anchors { anchors {
@@ -782,6 +774,7 @@ Item {
LeftSection { LeftSection {
id: vLeftSection id: vLeftSection
objectName: "leftSection" objectName: "leftSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentTopBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: true forceVerticalLayout: true
width: parent.width width: parent.width
@@ -850,6 +843,7 @@ Item {
RightSection { RightSection {
id: vRightSection id: vRightSection
objectName: "rightSection" objectName: "rightSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentBottomBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: true forceVerticalLayout: true
width: parent.width width: parent.width

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