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

Compare commits

..

26 Commits

Author SHA1 Message Date
bbedward bdfd565b72 fix words 2026-07-08 12:53:54 -04:00
bbedward f3b698590c bump VERSION to 1.5.0 2026-07-08 12:13:41 -04:00
bbedward 7826d42ce4 i18n: sync terms and remove term freeze 2026-07-08 12:13:14 -04:00
bbedward 6cc30c289b dankinstall/arch: add dms-greeter-bin package 2026-07-08 12:12:20 -04:00
bbedward 43cc8e69d1 window rules: fix gating of window-rules to supported compositors 2026-07-08 12:10:05 -04:00
bbedward a62ae336e0 media: improve album art handling 2026-07-08 12:01:33 -04:00
bbedward e94af2a7aa dankinstall: add danksearch and dankcalendar as optional deps 2026-07-08 10:01:56 -04:00
bbedward bb9e69e0c1 lock: add support for readline/emacs style modifiers
fixes #2777
2026-07-08 09:16:35 -04:00
purian23 1cc9218ff6 fix(frame): update hairline seam in notification joined surface 2026-07-07 21:54:37 -04:00
Lucas 56712f46fa Add optional=true to niri configs (#2776)
* nix: update flake.lock

* nix/niri: use optional=true in includes

* nix: fix tests

* niri: update embedded config to add optional=true
2026-07-07 21:40:43 -04:00
bbedward 93168ee073 qs: launcher optimizations and weather tab optimizations 2026-07-07 21:16:09 -04:00
purian23 1724aedd49 fix(AudioService): update the default audio sink device handling
- Fixes #2764
2026-07-07 20:17:40 -04:00
bbedward d799175c07 media: tweak seek bar accent color 2026-07-07 18:36:38 -04:00
bbedward ee6f7b4798 qs/media: replace FBOs with ClippingRectangle and rewrite seekbar as a
shader
2026-07-07 18:16:26 -04:00
bbedward 0511cd19df qs: numerous performance optimizations
- ClippingRectangle usages
- Asynchronous loader usages
- Replace cava visualizers with shaders
2026-07-07 16:11:11 -04:00
bbedward 71b1901ab0 notepad: add inline tab renaming and bump tab size 2026-07-07 11:05:14 -04:00
purian23 32e2d96e55 fix(popout): reintroduce texture sizing during animation to prevent content shift 2026-07-07 10:01:05 -04:00
bbedward 647766b9fa dankinstall: xray false for niri in default config 2026-07-07 09:38:45 -04:00
bbedward 0cd8974110 i18n: add Korean 2026-07-07 09:32:15 -04:00
bbedward 0c4c5fc146 changelog: enable for 1.5 2026-07-07 09:16:54 -04:00
bbedward b447e16374 launcher: dont cache clipboard results and fix image previews 2026-07-06 23:57:27 -04:00
purian23 0bb8353a33 refactor(blurSync): reimplement blur sync in popouts & modals 2026-07-06 23:52:57 -04:00
purian23 19a7dcf17d feat: (void-linux): add dankinstall support for auto installs 2026-07-06 23:01:16 -04:00
bbedward 8a1acb63c9 launcher: improve clipboard preview performance
related #2769
2026-07-06 18:15:49 -04:00
bbedward 6f298d3f52 workspace: fix hiding index numbers on sway
fixes #2768
2026-07-06 16:20:31 -04:00
bbedward 3e481a566b wallpaper: more resilience to updatesEnabled missing expose/update
events
2026-07-06 15:57:19 -04:00
139 changed files with 15574 additions and 7642 deletions
-17
View File
@@ -1,17 +0,0 @@
[Unit]
Description=DMS Status Notifier Watcher (early tray)
# The tray watcher starts in parallel with the compositor and owns org.kde.StatusNotifierWatcher before
# graphical-session.target, i.e. before any XDG autostart app launches.
PartOf=graphical-session.target
Before=graphical-session.target
[Service]
Type=dbus
BusName=org.kde.StatusNotifierWatcher
ExecStart=/usr/bin/dms tray-watcher
Restart=on-failure
RestartSec=1
Slice=session.slice
[Install]
WantedBy=graphical-session.target
+5 -2
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control.
**dankinstall**
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
## System Integration
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
**Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
@@ -214,4 +214,7 @@ Most packages available in standard repos. Minimal building required.
**Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
See installer output for distribution-specific details during installation.
+8
View File
@@ -23,6 +23,8 @@ var (
replaceConfigs []string
replaceConfigsAll bool
yes bool
danksearch bool
dankcalendar bool
)
var rootCmd = &cobra.Command{
@@ -49,6 +51,8 @@ func init() {
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
}
func main() {
@@ -74,6 +78,8 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
"replace-configs",
"replace-configs-all",
"yes",
"danksearch",
"dankcalendar",
}
var set []string
for _, name := range headlessOnly {
@@ -109,6 +115,8 @@ func runHeadless() error {
ReplaceConfigs: replaceConfigs,
ReplaceConfigsAll: replaceConfigsAll,
Yes: yes,
DankSearch: danksearch,
DankCalendar: dankcalendar,
}
runner := headless.NewRunner(cfg)
-1
View File
@@ -774,6 +774,5 @@ func getCommonCommands() []*cobra.Command {
trashCmd,
systemCmd,
switchUserCmd,
trayWatcherCmd,
}
}
+1
View File
@@ -886,6 +886,7 @@ func checkOptionalDependencies() []checkResult {
{"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "Calendar events", false},
{"danksearch", "dsearch", "File search", false},
{"dankcalendar", "dcal", "Calendar app", false},
{"fprintd", "fprintd-list", "Fingerprint auth", false},
}
+4 -1
View File
@@ -1534,6 +1534,8 @@ func packageInstallHint() string {
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
case distros.FamilyVoid:
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf)"
default:
return "Run 'dms greeter install' to install greeter"
}
@@ -1572,7 +1574,8 @@ func isPackageOnlyGreeterDistro() bool {
config.Family == distros.FamilySUSE ||
config.Family == distros.FamilyUbuntu ||
config.Family == distros.FamilyFedora ||
config.Family == distros.FamilyArch
config.Family == distros.FamilyArch ||
config.Family == distros.FamilyVoid
}
func promptCompositorChoice(compositors []string) (string, error) {
+13
View File
@@ -10,6 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
@@ -298,6 +299,9 @@ func runSetup() error {
if wmSelected {
if wm == deps.WindowManagerMango {
useSystemd = false
} else if isVoidSetup() {
useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
} else {
useSystemd = promptSystemd()
}
@@ -372,6 +376,15 @@ func runSetup() error {
return nil
}
func isVoidSetup() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
// Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() {
-24
View File
@@ -1,24 +0,0 @@
package main
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/traywatcher"
"github.com/spf13/cobra"
)
var trayWatcherCmd = &cobra.Command{
Use: "tray-watcher",
Short: "Run a minimal StatusNotifierWatcher for early tray registration",
Long: `Run a minimal org.kde.StatusNotifierWatcher daemon.
Started early in the session (Before=graphical-session.target via
dms-tray-watcher.service), it lets tray apps launched by XDG autostart
register their items before the shell finishes loading, and keeps them
registered across shell restarts. The shell's tray host picks items up from
this watcher; if it is not running, the shell's built-in watcher takes over.`,
Run: func(cmd *cobra.Command, args []string) {
if err := traywatcher.Run(); err != nil {
log.Fatalf("%v", err)
}
},
}
+6 -3
View File
@@ -61,6 +61,10 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult
@@ -880,9 +884,8 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
// Watcher spawns first so it owns the SNI name before dms/tray apps start.
spawnDms := "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\""
if !strings.Contains(config, `spawn-at-startup "dms" "run"`) {
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
// Insert spawn-at-startup for dms after the environment block
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
-12
View File
@@ -520,21 +520,9 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandLuaConfig, "input =")
}
// In non-systemd mode dms is launched from the compositor config, so the tray
// watcher must be launched there too, before dms, to own the SNI name first.
func TestNonSystemdLaunchesTrayWatcherBeforeShell(t *testing.T) {
hypr := transformHyprlandLuaForNonSystemd(HyprlandLuaConfig, "ghostty")
assert.Contains(t, hypr, "hl.exec_cmd(\"dms tray-watcher\")\n\thl.exec_cmd(\"dms run\")")
niri := (&ConfigDeployer{}).transformNiriConfigForNonSystemd(NiriConfig, "ghostty")
assert.Contains(t, niri, "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\"")
}
func TestMangoConfigStructure(t *testing.T) {
assert.Contains(t, MangoConfig, "exec-once=dms run")
assert.NotContains(t, MangoConfig, "exec_once=dms run")
// Tray watcher must start before the shell so it owns the SNI name first.
assert.Contains(t, MangoConfig, "exec-once=dms tray-watcher\nexec-once=dms run")
assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
+2 -2
View File
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
copy-on-select = false
confirm-close-surface = false
# Disable annoying copied to clipboard
app-notifications = no-clipboard-copy,no-config-reload
# Disable in-app Ghostty toast notifications
app-notifications = false
# Key bindings for common actions
#keybind = ctrl+c=copy_to_clipboard
+1 -1
View File
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
# Notepad
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
# Browse Wallpapers
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
# Power Menu
bind=SUPER,x,spawn,dms ipc call powermenu toggle
# Cycle Display Profile
+1 -3
View File
@@ -7,9 +7,7 @@ env=XDG_SESSION_TYPE,wayland
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
# every exec= on each config reload, and DMS reloads the config, which would
# spawn a new shell on every reload. The tray watcher starts first so it owns
# the SNI name before dms and the tray apps come up.
exec-once=dms tray-watcher
# spawn a new shell on every reload.
exec-once=dms run
source=./dms/colors.conf
+1 -1
View File
@@ -24,7 +24,7 @@ binds {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
@@ -20,3 +20,10 @@ window-rule {
tiled-state true
draw-border-with-background false
}
layer-rule {
exclude namespace="^dms:bar$"
background-effect {
xray false
}
}
+6 -6
View File
@@ -265,9 +265,9 @@ recent-windows {
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"
include optional=true "dms/colors.kdl"
include optional=true "dms/layout.kdl"
include optional=true "dms/alttab.kdl"
include optional=true "dms/binds.kdl"
include optional=true "dms/outputs.kdl"
include optional=true "dms/cursor.kdl"
-1
View File
@@ -116,7 +116,6 @@ func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string {
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
`hl.on("hyprland.start", function()` + "\n" +
` hl.exec_cmd("dms tray-watcher")` + "\n" +
` hl.exec_cmd("dms run")` + "\n" +
`end)` + "\n" +
hyprlandStartupEnd
+51 -2
View File
@@ -119,10 +119,30 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectDanksearch())
dependencies = append(dependencies, a.detectDankCalendar())
return dependencies, nil
}
func (a *ArchDistribution) detectDanksearch() deps.Dependency {
dep := a.BaseDistribution.detectDanksearch()
dep.CanToggle = true
if a.packageInstalled("dsearch-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) detectDankCalendar() deps.Dependency {
dep := a.BaseDistribution.detectDankCalendar()
dep.CanToggle = true
if a.packageInstalled("dankcalendar-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
}
@@ -132,7 +152,13 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
}
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
installed := a.packageInstalled("greetd-dms-greeter-git") || a.packageInstalled("greetd-dms-greeter-bin")
dep := a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", installed)
dep.CanToggle = true
if a.packageInstalled("greetd-dms-greeter-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) packageInstalled(pkg string) bool {
@@ -191,7 +217,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"dms-greeter": a.getDMSGreeterMapping(variants["dms-greeter"]),
"matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
@@ -199,6 +225,8 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"danksearch": a.getDanksearchMapping(variants["danksearch"]),
"dankcalendar": a.getDankCalendarMapping(variants["dankcalendar"]),
}
switch wm {
@@ -253,6 +281,27 @@ func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) Packag
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) getDanksearchMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dsearch-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dsearch-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDankCalendarMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dankcalendar-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dankcalendar-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDMSGreeterMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "greetd-dms-greeter-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
if forceDMSGit || variant == deps.VariantGit {
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
+10 -4
View File
@@ -107,6 +107,14 @@ func (b *BaseDistribution) detectDgop() deps.Dependency {
return b.detectCommand("dgop", "Desktop portal management tool")
}
func (b *BaseDistribution) detectDanksearch() deps.Dependency {
return b.detectOptionalPackage("danksearch", "File indexing and search service", b.commandExists("dsearch") || b.commandExists("danksearch"))
}
func (b *BaseDistribution) detectDankCalendar() deps.Dependency {
return b.detectOptionalPackage("dankcalendar", "Calendar application", b.commandExists("dcal") || b.commandExists("dankcalendar"))
}
func (b *BaseDistribution) detectDMS() deps.Dependency {
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
@@ -587,15 +595,13 @@ TERMINAL=%s
}
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
// Pull in the tray watcher alongside dms; its Before=graphical-session.target
// ordering makes it claim the SNI name before autostart apps start.
switch wm {
case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms", "dms-tray-watcher").Run(); err != nil {
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service")
}
case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms", "dms-tray-watcher").Run(); err != nil {
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target")
}
}
+4
View File
@@ -71,6 +71,8 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectDanksearch())
dependencies = append(dependencies, d.detectDankCalendar())
return dependencies, nil
}
@@ -135,6 +137,8 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
if wm == deps.WindowManagerNiri {
+34
View File
@@ -0,0 +1,34 @@
package distros
import (
"context"
"fmt"
"os/exec"
)
// SetupDsearchService enables the dsearch.service user unit. Enablement failures
// are returned for the caller to surface as a non-fatal warning.
func SetupDsearchService(ctx context.Context, logf func(string)) error {
if logf == nil {
logf = func(string) {}
}
if err := runSystemctlUser(ctx, "daemon-reload"); err != nil {
return err
}
if err := runSystemctlUser(ctx, "enable", "--now", "dsearch.service"); err != nil {
return err
}
logf("Enabled dsearch.service")
return nil
}
func runSystemctlUser(ctx context.Context, args ...string) error {
cmd := exec.CommandContext(ctx, "systemctl", append([]string{"--user"}, args...)...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("systemctl --user %v failed: %w: %s", args, err, string(output))
}
return nil
}
+4
View File
@@ -104,6 +104,8 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectDanksearch())
dependencies = append(dependencies, f.detectDankCalendar())
return dependencies, nil
}
@@ -138,6 +140,8 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
}
switch wm {
+2
View File
@@ -113,6 +113,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectDanksearch())
return dependencies, nil
}
@@ -171,6 +172,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"dms (DankMaterialShell)": g.getDmsMapping(),
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"danksearch": {Name: "gui-apps/danksearch", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
}
switch wm {
+3
View File
@@ -17,6 +17,7 @@ const (
FamilyDebian DistroFamily = "debian"
FamilyNix DistroFamily = "nix"
FamilyGentoo DistroFamily = "gentoo"
FamilyVoid DistroFamily = "void"
)
// PackageManagerType defines the package manager a distro uses
@@ -29,6 +30,7 @@ const (
PackageManagerZypper PackageManagerType = "zypper"
PackageManagerNix PackageManagerType = "nix"
PackageManagerPortage PackageManagerType = "portage"
PackageManagerXBPS PackageManagerType = "xbps"
)
// RepositoryType defines the type of repository for a package
@@ -42,6 +44,7 @@ const (
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
RepoTypeManual RepositoryType = "manual" // Manual build from source
)
+4
View File
@@ -91,6 +91,8 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectDanksearch())
dependencies = append(dependencies, o.detectDankCalendar())
return dependencies, nil
}
@@ -129,6 +131,8 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
switch wm {
+4
View File
@@ -80,6 +80,8 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectDanksearch())
dependencies = append(dependencies, u.detectDankCalendar())
return dependencies, nil
}
@@ -124,6 +126,8 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
}
switch wm {
+534
View File
@@ -0,0 +1,534 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
const (
VoidDMSRepo = "https://avengemedia.github.io/DankMaterialShell/current"
VoidDankLinuxRepo = "https://avengemedia.github.io/DankLinux/current"
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
voidRunitSvDir = "/etc/sv"
voidRunitServiceDir = "/var/service"
)
func init() {
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
return NewVoidDistribution(config, logChan)
})
}
type VoidDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
return &VoidDistribution{
BaseDistribution: NewBaseDistribution(logChan),
config: config,
}
}
func (v *VoidDistribution) GetID() string {
return v.config.ID
}
func (v *VoidDistribution) GetColorHex() string {
return v.config.ColorHex
}
func (v *VoidDistribution) GetFamily() DistroFamily {
return v.config.Family
}
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
return PackageManagerXBPS
}
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, v.detectDMS())
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
dependencies = append(dependencies, v.detectGit())
dependencies = append(dependencies, v.detectWindowManager(wm))
dependencies = append(dependencies, v.detectQuickshell())
dependencies = append(dependencies, v.detectDMSGreeter())
dependencies = append(dependencies, v.detectXDGPortal())
dependencies = append(dependencies, v.detectAccountsService())
dependencies = append(dependencies, v.detectDBus())
dependencies = append(dependencies, v.detectElogind())
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, v.detectHyprlandTools()...)
}
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
dependencies = append(dependencies, v.detectXwaylandSatellite())
}
dependencies = append(dependencies, v.detectMatugen())
dependencies = append(dependencies, v.detectDgop())
dependencies = append(dependencies, v.detectDanksearch())
dependencies = append(dependencies, v.detectDankCalendar())
return dependencies, nil
}
func (v *VoidDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
version := ""
variant := deps.VariantStable
if v.packageInstalled("dms-git") {
status = deps.StatusInstalled
version = v.packageVersion("dms-git")
variant = deps.VariantGit
} else if v.packageInstalled("dms") {
status = deps.StatusInstalled
version = v.packageVersion("dms")
} else if v.commandExists("dms") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Version: version,
Description: "Desktop Management System package",
Required: true,
Variant: variant,
CanToggle: true,
}
}
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
dep := v.BaseDistribution.detectQuickshell()
dep.CanToggle = false
return dep
}
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
}
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
}
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
}
func (v *VoidDistribution) detectDBus() deps.Dependency {
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
}
func (v *VoidDistribution) detectElogind() deps.Dependency {
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
}
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
}
func (v *VoidDistribution) packageInstalled(pkg string) bool {
return exec.Command("xbps-query", pkg).Run() == nil
}
func (v *VoidDistribution) packageVersion(pkg string) string {
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(output))
}
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
"dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
case deps.WindowManagerMango:
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantStable {
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking XBPS...",
IsComplete: false,
LogOutput: "Checking for xbps-install",
}
if _, err := exec.LookPath("xbps-install"); err != nil {
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
}
return nil
}
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if wm == deps.WindowManagerHyprland {
arch, err := v.xbpsArch(ctx)
if err != nil {
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
}
if arch != "x86_64" {
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
}
}
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if len(xbpsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling DMS XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
LogOutput: "Setting up custom XBPS repositories for DMS packages",
}
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
}
}
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
if len(allPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
}
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install XBPS packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable Void session services: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if !v.isRunitSystem() {
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
return nil
}
for _, service := range []string{"dbus", "elogind"} {
if !v.runitServiceInstalled(service) {
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
continue
}
if v.runitServiceEnabled(service) {
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.92,
Step: fmt.Sprintf("Enabling %s runit service...", service),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
}
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
}
return nil
}
func (v *VoidDistribution) isRunitSystem() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
return err == nil && fi.IsDir()
}
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
return err == nil
}
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
systemPkgs := []string{}
xbpsPkgs := []PackageMapping{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeXBPS:
xbpsPkgs = append(xbpsPkgs, pkgInfo)
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
}
}
return systemPkgs, xbpsPkgs
}
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
enabledRepoURLs := []string{}
for _, pkg := range xbpsPkgs {
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
continue
}
repoName := v.xbpsRepoName(pkg.RepoURL)
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
}
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
}
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
}
if len(enabledRepos) > 0 {
syncArgs := []string{"xbps-install", "-Sy", "-i"}
for _, repoURL := range enabledRepoURLs {
syncArgs = append(syncArgs, "--repository", repoURL)
}
syncCommand := strings.Join(syncArgs, " ")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Synchronizing XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
LogOutput: "Synchronizing XBPS repository indexes",
}
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
}
}
return nil
}
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
switch repoURL {
case VoidDMSRepo:
return "dms"
case VoidDankLinuxRepo:
return "danklinux"
case VoidHyprlandRepo:
return "hyprland"
default:
name := strings.TrimPrefix(repoURL, "https://")
name = strings.TrimPrefix(name, "http://")
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
return name
}
}
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
args := append([]string{"xbps-install", "-Sy"}, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing XBPS packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
}
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
seen := make(map[string]bool)
var unique []string
for _, group := range groups {
for _, pkg := range group {
if pkg == "" || seen[pkg] {
continue
}
seen[pkg] = true
unique = append(unique, pkg)
}
}
return unique
}
+150
View File
@@ -30,6 +30,11 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
const GreeterCacheDir = "/var/cache/dms-greeter"
const (
runitSvDir = "/etc/sv"
runitServiceDir = "/var/service"
)
func DetectDMSPath() (string, error) {
return config.LocateDMSConfig()
}
@@ -41,6 +46,96 @@ func IsNixOS() bool {
return err == nil
}
func IsVoidLinux() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
func isRunit() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(runitSvDir, name))
return err == nil && fi.IsDir()
}
func runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(runitServiceDir, name))
return err == nil
}
func enableRunitService(name, sudoPassword string) error {
if !runitServiceInstalled(name) {
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
}
if runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "ln", "-sf",
filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name))
}
func disableRunitService(name, sudoPassword string) error {
if !runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "rm", "-f",
filepath.Join(runitServiceDir, name))
}
func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) {
if runitServiceInstalled("seatd") {
if err := enableRunitService("seatd", sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err))
} else {
logFunc("✓ seatd enabled")
}
} else {
logFunc("⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
}
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err))
} else {
logFunc(fmt.Sprintf("✓ %s added to seat groups (_seatd, video, input)", greeterUser))
}
}
func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) {
const pamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(pamPath)
if err != nil {
logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err))
return
}
if strings.Contains(string(data), "pam_rundir") {
logFunc("✓ pam_rundir already present in greetd PAM")
return
}
line := "session optional pam_rundir.so"
if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err))
return
}
logFunc("✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
}
func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group")
if err != nil {
@@ -766,6 +861,8 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
case distros.FamilyGentoo:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
case distros.FamilyVoid:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy greetd")
case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
default:
@@ -892,6 +989,14 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
}
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
case distros.FamilyVoid:
failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter"
logFunc("Adding DMS XBPS repository...")
if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil {
logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err))
}
privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run()
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter")
default:
return false
}
@@ -909,6 +1014,20 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
return true
}
func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error {
confPath := filepath.Join("/etc/xbps.d", name+".conf")
repoLine := fmt.Sprintf("repository=%s", repoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
return nil
}
if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil {
return err
}
return privesc.Run(ctx, sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath))
}
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if IsGreeterPackaged() {
@@ -2275,6 +2394,19 @@ func checkSystemdEnabled(service string) (string, error) {
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
for _, dm := range conflictingDMs {
if isRunit() {
if !runitServiceEnabled(dm) {
continue
}
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
if err := disableRunitService(dm, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
} else {
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
}
continue
}
state, err := checkSystemdEnabled(dm)
if err != nil || state == "" || state == "not-found" {
continue
@@ -2294,6 +2426,19 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
if isRunit() {
if !runitServiceInstalled("greetd") {
return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir)
}
ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc)
ensureGreetdPamRundir(sudoPassword, logFunc)
if err := enableRunitService("greetd", sudoPassword); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
logFunc(fmt.Sprintf("✓ greetd enabled (%s)", runitServiceDir))
return nil
}
state, err := checkSystemdEnabled("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
@@ -2317,6 +2462,11 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
}
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
if isRunit() {
logFunc("✓ runit detected; no graphical target is needed")
return nil
}
cmd := exec.Command("systemctl", "get-default")
output, err := cmd.Output()
if err != nil {
+40 -7
View File
@@ -41,6 +41,8 @@ type Config struct {
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
ReplaceConfigsAll bool // deploy/replace all configurations
Yes bool
DankSearch bool // install danksearch and enable its user service
DankCalendar bool // install dankcalendar
}
// Runner orchestrates unattended (headless) installation.
@@ -214,6 +216,11 @@ func (r *Runner) Run() error {
return fmt.Errorf("package installation failed: %w", err)
}
useSystemd := true
if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid {
useSystemd = false
}
// 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri"
@@ -231,18 +238,32 @@ func (r *Runner) Run() error {
}
}
// 9b. danksearch service setup (if danksearch was included)
if useSystemd && !disabledItems["danksearch"] && r.depExists(dependencies, "danksearch") {
fmt.Fprintln(os.Stdout, "Enabling danksearch service...")
logFunc := func(line string) {
r.log(line)
fmt.Fprintf(os.Stdout, " danksearch: %s\n", line)
}
if err := distros.SetupDsearchService(context.Background(), logFunc); err != nil {
// Non-fatal, matching greeter behavior
fmt.Fprintf(os.Stderr, "Warning: danksearch service setup issue (non-fatal): %v\n", err)
}
}
// 10. Deploy configurations
fmt.Fprintln(os.Stdout, "Deploying configurations...")
r.log("Starting configuration deployment")
deployer := config.NewConfigDeployer(r.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(
context.Background(),
wm,
terminal,
dependencies,
replaceConfigs,
reinstallItems,
useSystemd,
)
if err != nil {
return fmt.Errorf("configuration deployment failed: %w", err)
@@ -267,19 +288,31 @@ func (r *Runner) Run() error {
}
// buildDisabledItems computes the set of dependencies that should be skipped
// during installation, applying the --include-deps and --exclude-deps filters.
// dms-greeter is disabled by default (opt-in), matching TUI behavior.
// during installation. Optional components are opt-in (disabled by default),
// then re-enabled by the dedicated flags and --include-deps.
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
disabledItems := make(map[string]bool)
// dms-greeter is opt-in (disabled by default), matching TUI behavior
for i := range dependencies {
if dependencies[i].Name == "dms-greeter" {
disabledItems["dms-greeter"] = true
break
if !dependencies[i].Required {
disabledItems[dependencies[i].Name] = true
}
}
// Dedicated flags resolve before include/exclude
if r.cfg.DankSearch {
if !r.depExists(dependencies, "danksearch") {
return nil, fmt.Errorf("--danksearch: not available on this distribution")
}
delete(disabledItems, "danksearch")
}
if r.cfg.DankCalendar {
if !r.depExists(dependencies, "dankcalendar") {
return nil, fmt.Errorf("--dankcalendar: not available on this distribution")
}
delete(disabledItems, "dankcalendar")
}
// Process --include-deps (enable items that are disabled by default)
for _, name := range r.cfg.IncludeDeps {
name = strings.TrimSpace(name)
+51 -17
View File
@@ -342,17 +342,21 @@ func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
func TestBuildDisabledItems(t *testing.T) {
dependencies := []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled},
{Name: "ghostty", Status: deps.StatusMissing},
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
{Name: "niri", Status: deps.StatusInstalled, Required: true},
{Name: "ghostty", Status: deps.StatusMissing, Required: true},
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled, Required: true},
{Name: "dms-greeter", Status: deps.StatusMissing},
{Name: "waybar", Status: deps.StatusMissing},
{Name: "danksearch", Status: deps.StatusMissing},
{Name: "dankcalendar", Status: deps.StatusMissing},
{Name: "waybar", Status: deps.StatusMissing, Required: true},
}
tests := []struct {
name string
includeDeps []string
excludeDeps []string
dankSearch bool
dankCalendar bool
deps []deps.Dependency // nil means use the shared fixture
wantErr bool
errContains string // substring expected in error message
@@ -360,19 +364,20 @@ func TestBuildDisabledItems(t *testing.T) {
wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
}{
{
name: "no flags set, dms-greeter disabled by default",
wantDisabled: []string{"dms-greeter"},
name: "no flags set, optional deps disabled by default",
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar"},
wantEnabled: []string{"niri", "ghostty", "waybar"},
},
{
name: "include dms-greeter enables it",
includeDeps: []string{"dms-greeter"},
wantEnabled: []string{"dms-greeter"},
name: "include dms-greeter enables it",
includeDeps: []string{"dms-greeter"},
wantEnabled: []string{"dms-greeter"},
wantDisabled: []string{"danksearch", "dankcalendar"},
},
{
name: "exclude a regular dep",
excludeDeps: []string{"waybar"},
wantDisabled: []string{"dms-greeter", "waybar"},
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar", "waybar"},
},
{
name: "include unknown dep returns error",
@@ -399,24 +404,53 @@ func TestBuildDisabledItems(t *testing.T) {
wantDisabled: []string{"dms-greeter"},
},
{
name: "whitespace entries are skipped",
includeDeps: []string{" ", "dms-greeter"},
wantEnabled: []string{"dms-greeter"},
name: "whitespace entries are skipped",
includeDeps: []string{" ", "dms-greeter"},
wantEnabled: []string{"dms-greeter"},
wantDisabled: []string{"danksearch", "dankcalendar"},
},
{
name: "no dms-greeter in deps, nothing disabled by default",
name: "no optional deps present, nothing disabled by default",
deps: []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled},
{Name: "niri", Status: deps.StatusInstalled, Required: true},
},
wantEnabled: []string{"niri"},
},
{
name: "danksearch flag enables it",
dankSearch: true,
wantEnabled: []string{"danksearch"},
wantDisabled: []string{"dms-greeter", "dankcalendar"},
},
{
name: "dankcalendar flag enables it",
dankCalendar: true,
wantEnabled: []string{"dankcalendar"},
wantDisabled: []string{"dms-greeter", "danksearch"},
},
{
name: "danksearch flag when unavailable errors",
dankSearch: true,
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
wantErr: true,
errContains: "--danksearch",
},
{
name: "dankcalendar flag when unavailable errors",
dankCalendar: true,
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
wantErr: true,
errContains: "--dankcalendar",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{
IncludeDeps: tt.includeDeps,
ExcludeDeps: tt.excludeDeps,
IncludeDeps: tt.includeDeps,
ExcludeDeps: tt.excludeDeps,
DankSearch: tt.dankSearch,
DankCalendar: tt.dankCalendar,
})
d := tt.deps
if d == nil {
+5 -3
View File
@@ -68,6 +68,8 @@ func GetQtLoggingRules() string {
level = "info"
}
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
// probes); suppressed except at debug level
var rules []string
switch strings.ToLower(level) {
case "fatal":
@@ -75,13 +77,13 @@ func GetQtLoggingRules() string {
case "error":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
case "warn", "warning":
rules = []string{"*.debug=false", "*.info=false"}
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
case "info":
rules = []string{"*.debug=false"}
rules = []string{"*.debug=false", "scene.warning=false"}
case "debug":
return ""
default:
rules = []string{"*.debug=false"}
rules = []string{"*.debug=false", "scene.warning=false"}
}
return strings.Join(rules, ";")
-205
View File
@@ -1,205 +0,0 @@
// Package traywatcher implements a minimal org.kde.StatusNotifierWatcher that
// claims the SNI name early in the session so autostart tray apps can register
// before the shell finishes loading. See docs/TRAY_WATCHER.md.
package traywatcher
import (
"fmt"
"sort"
"strings"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
"github.com/godbus/dbus/v5/prop"
)
const (
busName = "org.kde.StatusNotifierWatcher"
objPath = dbus.ObjectPath("/StatusNotifierWatcher")
iface = "org.kde.StatusNotifierWatcher"
)
const introXML = `<node>
<interface name="org.kde.StatusNotifierWatcher">
<method name="RegisterStatusNotifierItem">
<arg type="s" direction="in" name="service"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg type="s" direction="in" name="service"/>
</method>
<property name="RegisteredStatusNotifierItems" type="as" access="read"/>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<signal name="StatusNotifierItemRegistered">
<arg type="s" name="service"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s" name="service"/>
</signal>
<signal name="StatusNotifierHostRegistered"/>
<signal name="StatusNotifierHostUnregistered"/>
</interface>
` + introspect.IntrospectDataString + prop.IntrospectDataString + `</node>`
type watcher struct {
conn *dbus.Conn
props *prop.Properties
mu sync.Mutex
items map[string]string // item id ("name/path") -> bus name to watch
hosts map[string]bool
}
func (w *watcher) itemListLocked() []string {
list := make([]string, 0, len(w.items))
for item := range w.items {
list = append(list, item)
}
sort.Strings(list)
return list
}
func (w *watcher) emit(signal string, args ...any) {
if err := w.conn.Emit(objPath, iface+"."+signal, args...); err != nil {
log.Warnf("failed to emit %s: %v", signal, err)
}
}
// Clients pass either an object path (item on the caller) or a bus name.
func (w *watcher) RegisterStatusNotifierItem(sender dbus.Sender, service string) *dbus.Error {
var item, watch string
if strings.HasPrefix(service, "/") {
item, watch = string(sender)+service, string(sender)
} else {
item, watch = service+"/StatusNotifierItem", service
}
w.mu.Lock()
if _, exists := w.items[item]; exists {
w.mu.Unlock()
return nil
}
w.items[item] = watch
list := w.itemListLocked()
w.mu.Unlock()
log.Infof("item registered: %s", item)
w.emit("StatusNotifierItemRegistered", item)
w.props.SetMust(iface, "RegisteredStatusNotifierItems", list)
return nil
}
func (w *watcher) RegisterStatusNotifierHost(sender dbus.Sender, service string) *dbus.Error {
watch := string(sender)
if service != "" && !strings.HasPrefix(service, "/") {
watch = service
}
w.mu.Lock()
if w.hosts[watch] {
w.mu.Unlock()
return nil
}
w.hosts[watch] = true
w.mu.Unlock()
log.Infof("host registered: %s", watch)
w.emit("StatusNotifierHostRegistered")
return nil
}
func (w *watcher) pruneOwner(name string) {
w.mu.Lock()
var removed []string
for item, watch := range w.items {
if watch == name {
delete(w.items, item)
removed = append(removed, item)
}
}
list := w.itemListLocked()
hostGone := w.hosts[name]
delete(w.hosts, name)
w.mu.Unlock()
for _, item := range removed {
log.Infof("item gone: %s", item)
w.emit("StatusNotifierItemUnregistered", item)
}
if len(removed) > 0 {
w.props.SetMust(iface, "RegisteredStatusNotifierItems", list)
}
if hostGone {
log.Infof("host gone: %s", name)
w.emit("StatusNotifierHostUnregistered")
}
}
// Run serves the watcher until the session bus connection closes.
func Run() error {
conn, err := dbus.ConnectSessionBus()
if err != nil {
return fmt.Errorf("connect to session bus: %w", err)
}
defer conn.Close()
w := &watcher{conn: conn, items: map[string]string{}, hosts: map[string]bool{}}
if err := conn.Export(w, objPath, iface); err != nil {
return fmt.Errorf("export watcher: %w", err)
}
w.props, err = prop.Export(conn, objPath, map[string]map[string]*prop.Prop{
iface: {
"RegisteredStatusNotifierItems": {Value: []string{}, Emit: prop.EmitTrue},
// Always true: libappindicator clients drop the icon if it is false.
"IsStatusNotifierHostRegistered": {Value: true, Emit: prop.EmitFalse},
"ProtocolVersion": {Value: int32(0), Emit: prop.EmitFalse},
},
})
if err != nil {
return fmt.Errorf("export properties: %w", err)
}
if err := conn.Export(introspect.Introspectable(introXML), objPath, "org.freedesktop.DBus.Introspectable"); err != nil {
return fmt.Errorf("export introspection: %w", err)
}
if err := conn.AddMatchSignal(
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchInterface("org.freedesktop.DBus"),
dbus.WithMatchMember("NameOwnerChanged"),
dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
); err != nil {
return fmt.Errorf("match NameOwnerChanged: %w", err)
}
sigs := make(chan *dbus.Signal, 128)
conn.Signal(sigs)
// No flags: queue for the name and take it when the current owner exits.
reply, err := conn.RequestName(busName, 0)
if err != nil {
return fmt.Errorf("request %s: %w", busName, err)
}
if reply == dbus.RequestNameReplyInQueue {
log.Infof("name busy, queued for %s", busName)
}
for sig := range sigs {
switch sig.Name {
case "org.freedesktop.DBus.NameAcquired":
if len(sig.Body) == 1 && sig.Body[0] == busName {
log.Infof("acquired %s", busName)
}
case "org.freedesktop.DBus.NameOwnerChanged":
if len(sig.Body) != 3 {
continue
}
name, _ := sig.Body[0].(string)
newOwner, _ := sig.Body[2].(string)
if name != busName && newOwner == "" {
w.pruneOwner(name)
}
}
}
return fmt.Errorf("session bus connection closed")
}
+63 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea"
)
@@ -129,7 +130,7 @@ func (m Model) deployConfigurations() tea.Cmd {
deployer := config.NewConfigDeployer(m.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig())
return configDeploymentResult{
results: results,
@@ -138,6 +139,29 @@ func (m Model) deployConfigurations() tea.Cmd {
}
}
func (m Model) optionalDepSelected(name string) bool {
if m.disabledItems[name] {
return false
}
for _, dep := range m.dependencies {
if dep.Name == name {
return true
}
}
return false
}
func (m Model) useSystemdConfig() bool {
if m.osInfo == nil {
return true
}
distroConfig, exists := distros.Registry[m.osInfo.Distribution.ID]
if !exists {
return true
}
return distroConfig.Family != distros.FamilyVoid
}
func (m Model) viewConfigConfirmation() string {
var b strings.Builder
@@ -195,12 +219,50 @@ func (m Model) viewConfigConfirmation() string {
b.WriteString(backup)
b.WriteString("\n\n")
if note := m.configReplacementNote(); note != "" {
b.WriteString(m.styles.Subtle.Render(note))
b.WriteString("\n\n")
}
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
b.WriteString(help)
return b.String()
}
func (m Model) configReplacementNote() string {
if m.selectedConfig < 0 || m.selectedConfig >= len(m.existingConfigs) {
return ""
}
configInfo := m.existingConfigs[m.selectedConfig]
if !configInfo.Exists {
return ""
}
switch configInfo.ConfigType {
case "Niri":
if m.useSystemdConfig() {
return "Replacing Niri writes the DMS Niri template and uses the user systemd dms service for shell autostart."
}
return `Replacing Niri writes the DMS Niri template and starts DMS from Niri with spawn-at-startup "dms" "run".`
case "Hyprland":
if m.useSystemdConfig() {
return "Replacing Hyprland writes the DMS Lua template and uses the user systemd dms service for shell autostart."
}
return `Replacing Hyprland writes the DMS Lua template and starts DMS from Hyprland with hl.exec_cmd("dms run").`
case "Mango":
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec-once=dms run."
case "Ghostty":
return "Replacing Ghostty writes the DMS terminal defaults and theme include."
case "Kitty":
return "Replacing Kitty writes the DMS terminal defaults, theme include, and tab styling."
case "Alacritty":
return "Replacing Alacritty writes the DMS terminal defaults and theme import."
default:
return ""
}
}
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
if result, ok := msg.(configCheckResult); ok {
if result.error != nil {
+58 -15
View File
@@ -28,6 +28,21 @@ func (m Model) viewDetectingDeps() string {
return b.String()
}
func partitionOptionalLast(dependencies []deps.Dependency) []deps.Dependency {
ordered := make([]deps.Dependency, 0, len(dependencies))
for _, dep := range dependencies {
if dep.Required {
ordered = append(ordered, dep)
}
}
for _, dep := range dependencies {
if !dep.Required {
ordered = append(ordered, dep)
}
}
return ordered
}
func (m Model) viewDependencyReview() string {
var b strings.Builder
@@ -39,7 +54,15 @@ func (m Model) viewDependencyReview() string {
b.WriteString("\n\n")
if len(m.dependencies) > 0 {
optionalHeaderShown := false
for i, dep := range m.dependencies {
if !dep.Required && !optionalHeaderShown {
b.WriteString("\n")
b.WriteString(m.styles.Subtle.Render("Optional (space to enable)"))
b.WriteString("\n")
optionalHeaderShown = true
}
var status string
var reinstallMarker string
var variantMarker string
@@ -82,8 +105,13 @@ func (m Model) viewDependencyReview() string {
}
note := ""
if dep.Name == "dms-greeter" {
switch dep.Name {
case "dms-greeter":
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
case "danksearch":
note = m.styles.Subtle.Render(" (file search; enables dsearch.service)")
case "dankcalendar":
note = m.styles.Subtle.Render(" (autostart managed in dankcalendar settings)")
}
var line string
@@ -120,13 +148,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = depsMsg.err
m.state = StateError
} else {
m.dependencies = depsMsg.deps
// dms-greeter is opt-in skipped by default
for _, dep := range depsMsg.deps {
if dep.Name == "dms-greeter" {
m.disabledItems["dms-greeter"] = true
break
m.dependencies = partitionOptionalLast(depsMsg.deps)
// Optional components are opt-in, skipped by default
for _, dep := range m.dependencies {
if dep.Required {
continue
}
m.disabledItems[dep.Name] = true
}
m.state = StateDependencyReview
}
@@ -231,14 +259,7 @@ func (m Model) installPackages() tea.Cmd {
for msg := range installerProgressChan {
// Run optional greeter setup
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
greeterSelected := false
for _, dep := range m.dependencies {
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
greeterSelected = true
break
}
}
if greeterSelected {
if m.optionalDepSelected("dms-greeter") {
compositorName := "niri"
switch m.selectedWindowManager() {
case deps.WindowManagerHyprland:
@@ -265,6 +286,28 @@ func (m Model) installPackages() tea.Cmd {
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
}
}
if m.useSystemdConfig() && m.optionalDepSelected("danksearch") {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.97,
step: "Enabling danksearch service...",
logOutput: "Setting up dsearch.service...",
}
dsearchLogFunc := func(line string) {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.97,
step: "Enabling danksearch service...",
logOutput: line,
}
}
if err := distros.SetupDsearchService(context.Background(), dsearchLogFunc); err != nil {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.98,
step: "danksearch service warning",
logOutput: fmt.Sprintf("danksearch service setup warning (non-fatal): %v", err),
}
}
}
}
}
tuiMsg := packageInstallProgressMsg{
+33 -10
View File
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
return "dms-shell-git"
}
return "dms-shell"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid:
if isGit {
return "dms-git"
}
@@ -168,6 +168,8 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
return "sudo apt remove " + pkg
case distros.FamilySUSE:
return "sudo zypper remove " + pkg
case distros.FamilyVoid:
return "sudo xbps-remove -R " + pkg
default:
return ""
}
@@ -201,15 +203,26 @@ func (m Model) viewInstallComplete() string {
wm := m.selectedWindowManager()
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, login with \"niri-session\""
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, login with \"Hyprland\""
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, login with \"mango\""
if !m.useSystemdConfig() {
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri"
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland"
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session mango"
}
} else {
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, login with \"niri-session\""
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, login with \"Hyprland\""
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, login with \"mango\""
}
}
b.WriteString("\n")
@@ -222,7 +235,17 @@ func (m Model) viewInstallComplete() string {
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
if wm == deps.WindowManagerMango {
if !m.useSystemdConfig() {
switch wm {
case deps.WindowManagerNiri:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`) + "\n")
case deps.WindowManagerHyprland:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`) + "\n")
case deps.WindowManagerMango:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
}
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("quickshell --path ~/.config/quickshell/dms log") + "\n")
} else if wm == deps.WindowManagerMango {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
} else {
-1
View File
@@ -72,7 +72,6 @@ override_dh_auto_install:
if [ -d quickshell ]; then \
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
install -Dm644 assets/systemd/dms-tray-watcher.service debian/dms-git/usr/lib/systemd/user/dms-tray-watcher.service; \
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \
-1
View File
@@ -65,7 +65,6 @@ override_dh_auto_install:
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms-tray-watcher.service debian/dms/usr/lib/systemd/user/dms-tray-watcher.service; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \
-2
View File
@@ -119,7 +119,6 @@ core/bin/${DMS_BINARY} completion fish > %{buildroot}%{_datadir}/fish/vendor_com
# Install systemd user service
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -146,7 +145,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%doc quickshell/README.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
-2
View File
@@ -87,7 +87,6 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -113,7 +112,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
-18
View File
@@ -101,24 +101,6 @@ in
Install.WantedBy = [ cfg.systemd.target ];
};
# Early SNI watcher so autostart tray apps register before the shell loads.
systemd.user.services.dms-tray-watcher = lib.mkIf cfg.systemd.enable {
Unit = {
Description = "DMS Status Notifier Watcher (early tray)";
PartOf = [ cfg.systemd.target ];
Before = [ cfg.systemd.target ];
};
Service = {
Type = "dbus";
BusName = "org.kde.StatusNotifierWatcher";
ExecStart = lib.getExe cfg.package + " tray-watcher";
Restart = "on-failure";
};
Install.WantedBy = [ cfg.systemd.target ];
};
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
source = jsonFormat.generate "session.json" cfg.session;
};
+1 -1
View File
@@ -93,7 +93,7 @@ in {
text = lib.pipe cfg'.filesToInclude [
(map (filename: "dms/${filename}"))
withOriginalConfig
(map (filename: "include \"${filename}.kdl\""))
(map (filename: "include optional=true \"${filename}.kdl\""))
(files: files ++ fixes)
(builtins.concatStringsSep "\n")
];
-18
View File
@@ -39,24 +39,6 @@ in
};
};
# Early SNI watcher so autostart tray apps register before the shell loads.
systemd.user.services.dms-tray-watcher = lib.mkIf cfg.systemd.enable {
description = "DMS Status Notifier Watcher (early tray)";
path = lib.mkForce [ ];
partOf = [ cfg.systemd.target ];
before = [ cfg.systemd.target ];
wantedBy = [ cfg.systemd.target ];
restartIfChanged = cfg.systemd.restartIfChanged;
serviceConfig = {
Type = "dbus";
BusName = "org.kde.StatusNotifierWatcher";
ExecStart = lib.getExe cfg.package + " tray-watcher";
Restart = "on-failure";
};
};
environment.systemPackages = [ cfg.quickshell.package ] ++ common.packages;
environment.etc = lib.mapAttrs' (name: value: {
+2 -2
View File
@@ -6,8 +6,8 @@
let
homeManagerNixosModule =
(fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
})
+ "/nixos";
in
+4 -4
View File
@@ -6,8 +6,8 @@
let
homeManagerNixosModule =
(fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
})
+ "/nixos";
@@ -76,8 +76,8 @@ pkgs.testers.runNixOSTest {
machine.wait_for_unit("multi-user.target")
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
'';
-2
View File
@@ -114,7 +114,6 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -153,7 +152,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
-2
View File
@@ -63,7 +63,6 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -100,7 +99,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
-2
View File
@@ -69,8 +69,6 @@ override_dh_auto_install:
# Install systemd user service
install -Dm644 dms-git-repo/assets/systemd/dms.service \
debian/dms-git/usr/lib/systemd/user/dms.service
install -Dm644 dms-git-repo/assets/systemd/dms-tray-watcher.service \
debian/dms-git/usr/lib/systemd/user/dms-tray-watcher.service
# Install desktop file and icon
install -Dm644 dms-git-repo/assets/dms-open.desktop \
-2
View File
@@ -50,8 +50,6 @@ override_dh_auto_install:
# Install systemd user service (before copying everything else)
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms.service \
debian/dms/usr/lib/systemd/user/dms.service
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms-tray-watcher.service \
debian/dms/usr/lib/systemd/user/dms-tray-watcher.service
# Install desktop file and icon
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/dms-open.desktop \
+23 -2
View File
@@ -52,8 +52,8 @@ checkout at `srcpkgs/<pkg>/template` to build or submit it.
## Dependencies
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
and `matugen` (which drives the Material You theming). The rest are optional —
install whichever features you want:
`matugen` (which drives the Material You theming), `dbus`, and `elogind`.
The rest are optional, install whichever features you want:
| Package | Enables |
| --- | --- |
@@ -98,6 +98,27 @@ spawn-at-startup "dms" "run"
or Hyprland: `exec-once = dms run`.
From a TTY on Void without a greeter, start your compositor through a D-Bus
session:
```sh
dbus-run-session niri
dbus-run-session Hyprland
dbus-run-session mango
```
The `mangowc` package provides the `mango` command.
For power menu actions to work on runit systems, make sure the system D-Bus and
elogind services are enabled:
```sh
sudo ln -sf /etc/sv/dbus /var/service/dbus
sudo ln -sf /etc/sv/elogind /var/service/elogind
```
The `dankinstall` Void path does this automatically after installing packages.
## Greeter (optional)
Install `dms-greeter`, then let the CLI do the setup:
+1 -2
View File
@@ -32,13 +32,12 @@ conflicts="dms"
provides="dms-${version}_${revision}"
# Optional feature deps are listed in distro/void/README.md.
depends="quickshell accountsservice dgop matugen dbus"
depends="quickshell accountsservice dgop matugen dbus elogind"
post_install() {
# QML shell tree (build_style=go already installed the dms binary)
vmkdir usr/share/quickshell/dms
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
# Desktop entry + icon
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
+1 -2
View File
@@ -22,13 +22,12 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
# Optional feature deps are listed in distro/void/README.md.
depends="quickshell accountsservice dgop matugen dbus"
depends="quickshell accountsservice dgop matugen dbus elogind"
post_install() {
# QML shell tree (build_style=go already installed the dms binary)
vmkdir usr/share/quickshell/dms
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
# Desktop entry + icon
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
Generated
+3 -3
View File
@@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1778443072,
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
"lastModified": 1783224372,
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
"type": "github"
},
"original": {
+17 -2
View File
@@ -120,12 +120,18 @@ Singleton {
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
var newPositions = Object.assign({}, monitorScrollPositions);
newPositions[screenName] = { scrollX: scrollX, scrollY: scrollY };
newPositions[screenName] = {
scrollX: scrollX,
scrollY: scrollY
};
monitorScrollPositions = newPositions;
}
function getMonitorScrollPosition(screenName) {
return monitorScrollPositions[screenName] || { scrollX: 50, scrollY: 50 };
return monitorScrollPositions[screenName] || {
scrollX: 50,
scrollY: 50
};
}
function clearMonitorScrollPosition(screenName) {
@@ -209,6 +215,8 @@ Singleton {
property string locale: ""
property string timeLocale: ""
property string notepadLastMode: ""
property string launcherLastMode: "all"
property string launcherLastFileSearchType: "all"
property string launcherLastQuery: ""
@@ -1216,6 +1224,13 @@ Singleton {
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
}
function setNotepadLastMode(mode) {
if (notepadLastMode === mode)
return;
notepadLastMode = mode;
saveSettings();
}
function setLauncherLastMode(mode) {
launcherLastMode = mode;
saveSettings();
@@ -88,6 +88,8 @@ var SPEC = {
locale: { def: "", onChange: "updateLocale" },
timeLocale: { def: "" },
notepadLastMode: { def: "" },
launcherLastMode: { def: "all" },
launcherLastFileSearchType: { def: "all" },
launcherLastQuery: { def: "" },
+10 -9
View File
@@ -373,7 +373,7 @@ Item {
}
function open(): string {
if (SettingsData.notepadDefaultMode === "popout") {
if (PopoutService.notepadResolvedMode === "popout") {
PopoutService.openNotepadPopout();
return "NOTEPAD_OPEN_SUCCESS";
}
@@ -388,7 +388,7 @@ Item {
function openFile(path: string): string {
if (!path)
return open();
if (SettingsData.notepadDefaultMode === "popout") {
if (PopoutService.notepadResolvedMode === "popout") {
PopoutService.openNotepadPopoutWithFile(path);
return "NOTEPAD_OPEN_FILE_SUCCESS";
}
@@ -402,7 +402,7 @@ Item {
}
function close(): string {
if (SettingsData.notepadDefaultMode === "popout") {
if (PopoutService.notepadResolvedMode === "popout") {
PopoutService.notepadPopout?.hide();
return "NOTEPAD_CLOSE_SUCCESS";
}
@@ -415,7 +415,7 @@ Item {
}
function toggle(): string {
if (SettingsData.notepadDefaultMode === "popout") {
if (PopoutService.notepadResolvedMode === "popout") {
PopoutService.toggleNotepadPopout();
return "NOTEPAD_TOGGLE_SUCCESS";
}
@@ -712,12 +712,13 @@ Item {
target: "hypr"
}
// ! TODO - remove for v1.6
IpcHandler {
function wallpaper(): string {
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
if (bar) {
bar.triggerWallpaperBrowser();
return "SUCCESS: Toggled wallpaper browser";
return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead";
}
return "ERROR: Failed to toggle wallpaper browser";
}
@@ -1792,8 +1793,8 @@ Item {
}
function open(): string {
if (!CompositorService.isNiri)
return "WINDOW_RULES_NIRI_ONLY";
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return "WINDOW_RULES_UNSUPPORTED_COMPOSITOR";
root.windowRuleModalLoader.active = true;
if (root.windowRuleModalLoader.item) {
root.windowRuleModalLoader.item.show(getFocusedWindow());
@@ -1811,8 +1812,8 @@ Item {
}
function toggle(): string {
if (!CompositorService.isNiri)
return "WINDOW_RULES_NIRI_ONLY";
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return "WINDOW_RULES_UNSUPPORTED_COMPOSITOR";
root.windowRuleModalLoader.active = true;
if (root.windowRuleModalLoader.item) {
if (root.windowRuleModalLoader.item.visible) {
@@ -65,7 +65,7 @@ Column {
StyledText {
id: codenameText
anchors.centerIn: parent
text: "Saffron Bloom"
text: "The Wolverine"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
@@ -74,7 +74,7 @@ Column {
}
StyledText {
text: "New launcher, enhanced plugin system, KDE Connect, & more"
text: "Frame Mode, DankCalendar, Spotlight, & more"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
@@ -108,77 +108,99 @@ Column {
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "space_dashboard"
title: "Dank Launcher V2"
description: "New capabilities & plugins"
onClicked: PopoutService.openDankLauncherV2()
iconName: "border_outer"
title: "Frame Mode"
description: "Connected shell surfaces"
onClicked: PopoutService.openSettingsWithTab("frame")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "smartphone"
title: "Phone Connect"
description: "KDE Connect & Valent"
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
iconName: "calendar_month"
title: "DankCalendar"
description: "Native calendar & events"
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dankcalendar")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "monitor_heart"
title: "System Monitor"
description: "Redesigned process list"
onClicked: PopoutService.showProcessListModal()
iconName: "search"
title: "Spotlight"
description: "Lightweight launcher"
onClicked: PopoutService.openSpotlightBar()
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "window"
title: "Window Rules"
description: "niri window rule manager"
visible: CompositorService.isNiri
description: "Rules for many compositors"
onClicked: PopoutService.openSettingsWithTab("window_rules")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "notifications_active"
title: "Enhanced Notifications"
description: "Configurable rules & styling"
visible: !CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("notifications")
iconName: "display_settings"
title: "Display Profiles"
description: "Auto-switch monitor layouts"
onClicked: PopoutService.openSettingsWithTab("display_config")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "dock_to_bottom"
title: "Dock Enhancements"
description: "Bar dock widget & more"
onClicked: PopoutService.openSettingsWithTab("dock")
iconName: "dvr"
title: "Multiplexer Launcher"
description: "Attach to tmux sessions"
onClicked: PopoutService.openSettingsWithTab("multiplexers")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "volume_up"
title: "Audio Aliases"
description: "Custom device names"
onClicked: PopoutService.openSettingsWithTab("audio")
iconName: "edit_note"
title: "Notepad Rewrite"
description: "Popout & tiling support"
onClicked: PopoutService.openNotepad()
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "extension"
title: "Enhanced Plugin System"
description: "Enables new types of plugins"
onClicked: PopoutService.openSettingsWithTab("plugins")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "light_mode"
title: "Auto Light/Dark"
description: "Automatic mode switching"
iconName: "gradient"
title: "M3 Shadows"
description: "Reworked elevation system"
onClicked: PopoutService.openSettingsWithTab("theme")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "login"
title: "Greeter Enhancements"
description: "Settings GUI & multi-user"
onClicked: PopoutService.openSettingsWithTab("greeter")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "content_paste"
title: "Clipboard Filtering"
description: "Text, image & pinned filters"
onClicked: PopoutService.openSettingsWithTab("clipboard")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "restart_alt"
title: "XDG Autostart"
description: "Manage apps at login"
onClicked: PopoutService.openSettingsWithTab("autostart")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "apps"
title: "Default Apps"
description: "Set preferred applications"
onClicked: PopoutService.openSettingsWithTab("default_apps")
}
}
}
@@ -230,12 +252,7 @@ Column {
ChangelogUpgradeNote {
width: parent.width
text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
}
ChangelogUpgradeNote {
width: parent.width
text: "Plugin API updated — third-party plugins may need updates"
text: "App ID changed to com.danklinux.dms — update any compositor window rules targeting the old ID"
}
}
}
@@ -131,7 +131,7 @@ FloatingWindow {
iconName: "open_in_new"
backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-5-release")
}
DankButton {
@@ -1,10 +1,10 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
ClippingRectangle {
id: thumbnail
readonly property var log: Log.scoped("ClipboardThumbnail")
@@ -15,6 +15,10 @@ Item {
required property int itemIndex
property bool disposed: false
radius: Theme.cornerRadius / 2
color: "transparent"
antialiasing: true
Image {
id: thumbnailImage
@@ -34,7 +38,7 @@ Item {
fillMode: Image.PreserveAspectCrop
smooth: true
cache: false
visible: false
visible: entryType === "image" && status === Image.Ready && source != ""
asynchronous: true
sourceSize.width: 128
sourceSize.height: 128
@@ -236,33 +240,6 @@ Item {
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: thumbnailImage
maskEnabled: true
maskSource: clipboardRoundedRectangularMask
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardRoundedRectangularMask
width: ClipboardConstants.thumbnailSize
height: ClipboardConstants.itemHeight - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
color: "black"
antialiasing: true
}
}
DankIcon {
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
name: {
@@ -62,6 +62,11 @@ Item {
property bool animationsEnabled: true
function _kickBlurCommit() {
if (typeof contentWindow.update === "function")
contentWindow.update();
}
function open() {
closeTimer.stop();
isClosing = false;
@@ -201,6 +206,12 @@ Item {
}
})(), dpr)
onAlignedXChanged: _kickBlurCommit()
onAlignedYChanged: _kickBlurCommit()
onAlignedWidthChanged: _kickBlurCommit()
onAlignedHeightChanged: _kickBlurCommit()
onShouldBeVisibleChanged: _kickBlurCommit()
PanelWindow {
id: clickCatcher
visible: false
@@ -244,11 +255,13 @@ Item {
WindowBlur {
targetWindow: contentWindow
readonly property real s: Math.min(1, modalContainer.scaleValue)
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.06) * 2))
readonly property real visibleScale: s * op
// Blur tracks the surface's scaled rect, matching the connected backend.
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0
blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: root.shouldBeVisible ? modalContainer.width * visibleScale : 0
blurHeight: root.shouldBeVisible ? modalContainer.height * visibleScale : 0
blurRadius: root.cornerRadius
}
@@ -338,6 +351,7 @@ Item {
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
onOpenProgressChanged: root._kickBlurCommit()
Behavior on openProgress {
enabled: root.animationsEnabled
DankAnim {
@@ -0,0 +1,324 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
FocusScope {
id: root
property var editingApp: null
property string editAppId: ""
signal closeRequested
function loadOverride() {
var existing = SessionData.getAppOverride(editAppId);
editNameField.text = existing?.name || "";
editIconField.text = existing?.icon || "";
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
Qt.callLater(() => editNameField.forceActiveFocus());
}
function saveAppOverride() {
var override = {};
if (editNameField.text.trim())
override.name = editNameField.text.trim();
if (editIconField.text.trim())
override.icon = editIconField.text.trim();
if (editCommentField.text.trim())
override.comment = editCommentField.text.trim();
if (editEnvVarsField.text.trim())
override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim();
SessionData.setAppOverride(editAppId, override);
closeRequested();
}
function resetAppOverride() {
SessionData.clearAppOverride(editAppId);
closeRequested();
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
closeRequested();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: backButtonArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceHover, 0)
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: 20
color: Theme.surfaceText
}
MouseArea {
id: backButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.closeRequested()
}
}
Image {
width: 40
height: 40
source: Paths.resolveIconUrl(root.editingApp?.icon || "application-x-executable")
sourceSize.width: 40
sourceSize.height: 40
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
StyledText {
text: I18n.tr("Edit App")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: root.editingApp?.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outlineMedium
}
Flickable {
width: parent.width
height: parent.height - y - buttonsRow.height - Theme.spacingM
contentHeight: editFieldsColumn.height
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: editFieldsColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Name")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editNameField
width: parent.width
placeholderText: root.editingApp?.name || ""
keyNavigationTab: editIconField
keyNavigationBacktab: editExtraFlagsField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editIconField
width: parent.width
placeholderText: root.editingApp?.icon || ""
keyNavigationTab: editCommentField
keyNavigationBacktab: editNameField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Description")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editCommentField
width: parent.width
placeholderText: root.editingApp?.comment || ""
keyNavigationTab: editEnvVarsField
keyNavigationBacktab: editIconField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Environment Variables")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "KEY=value KEY2=value2"
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankTextField {
id: editEnvVarsField
width: parent.width
placeholderText: "VAR=value"
keyNavigationTab: editExtraFlagsField
keyNavigationBacktab: editCommentField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Extra Arguments")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editExtraFlagsField
width: parent.width
placeholderText: "--flag --option=value"
keyNavigationTab: editNameField
keyNavigationBacktab: editEnvVarsField
}
}
}
}
Row {
id: buttonsRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
id: resetButton
width: 90
height: 40
radius: Theme.cornerRadius
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
visible: SessionData.getAppOverride(root.editAppId) !== null
StyledText {
text: I18n.tr("Reset")
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: resetButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.resetAppOverride()
}
}
Rectangle {
id: cancelButton
width: 90
height: 40
radius: Theme.cornerRadius
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
StyledText {
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.closeRequested()
}
}
Rectangle {
id: saveButton
width: 90
height: 40
radius: Theme.cornerRadius
color: saveButtonArea.containsMouse ? Theme.withAlpha(Theme.primary, 0.9) : Theme.primary
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: saveButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.saveAppOverride()
}
}
}
}
}
@@ -13,7 +13,7 @@ Rectangle {
property string cachedMimeType: ""
property var _requestedEntryId: null
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
readonly property bool canLoadImage: typeof entry?.id === "number" && !!entry?.isImage && String(entry?.mimeType ?? "").startsWith("image/")
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
radius: Math.max(6, Theme.cornerRadius - 2)
@@ -41,13 +41,18 @@ Rectangle {
}
function reloadPreview() {
cachedImageData = "";
cachedMimeType = "";
if (!canLoadImage || !entry?.id) {
if (!canLoadImage || typeof entry?.id !== "number") {
_requestedEntryId = null;
cachedImageData = "";
cachedMimeType = "";
return;
}
// Entry objects are rebuilt per search; same id means same content
if (entry.id === _requestedEntryId)
return;
cachedImageData = "";
cachedMimeType = "";
const entryId = entry.id;
_requestedEntryId = entryId;
DMSService.sendRequest("clipboard.getEntry", {
@@ -55,17 +60,22 @@ Rectangle {
}, function (response) {
if (_requestedEntryId !== entryId)
return;
if (response.error)
if (response.error) {
_requestedEntryId = null;
return;
}
if (!response.result) {
_requestedEntryId = null;
ClipboardService.refresh();
return;
}
const result = response.result;
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
const data = (result.data ?? "").toString();
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
if (data.length === 0 || !resolvedSourceUrl(data, mimeType)) {
_requestedEntryId = null;
return;
}
cachedMimeType = mimeType;
cachedImageData = data;
});
@@ -78,6 +88,8 @@ Rectangle {
asynchronous: true
cache: false
smooth: true
sourceSize.width: 128
sourceSize.height: 128
fillMode: Image.PreserveAspectCrop
visible: status === Image.Ready
}
+114 -58
View File
@@ -50,15 +50,15 @@ Item {
}
onActiveChanged: {
if (!active) {
SessionData.addLauncherHistory(searchQuery);
ClipboardService.invalidateLauncherSearchCache();
if (active)
return;
sections = [];
flatModel = [];
selectedItem = null;
_clearModeCache();
ClipboardService.invalidateLauncherSearchCache();
}
SessionData.addLauncherHistory(searchQuery);
sections = [];
flatModel = [];
selectedItem = null;
_clearModeCache();
}
onSearchModeChanged: {
@@ -110,7 +110,7 @@ Item {
if (query !== effectiveQuery)
return;
searchDebounce.restart();
root.requestSearch();
}
}
@@ -276,9 +276,23 @@ Item {
property string appCategory: ""
property var appCategories: []
function builtInSectionViewPref(sectionId) {
switch (sectionId) {
case "clipboard":
return getPluginViewPref("dms_clipboard_search");
case "settings":
return getPluginViewPref("dms_settings_search");
default:
return null;
}
}
function getSectionViewMode(sectionId) {
if (sectionId === "browse_plugins")
return "list";
var builtInPref = builtInSectionViewPref(sectionId);
if (builtInPref?.enforced)
return builtInPref.mode;
if (pluginViewPreferences[sectionId]?.enforced)
return pluginViewPreferences[sectionId].mode;
if (sectionViewModes[sectionId])
@@ -302,6 +316,8 @@ Item {
function setSectionViewMode(sectionId, mode) {
if (sectionId === "browse_plugins")
return;
if (builtInSectionViewPref(sectionId)?.enforced)
return;
if (pluginViewPreferences[sectionId]?.enforced)
return;
sectionViewModes = Object.assign({}, sectionViewModes, {
@@ -325,6 +341,8 @@ Item {
function canChangeSectionViewMode(sectionId) {
if (sectionId === "browse_plugins")
return false;
if (builtInSectionViewPref(sectionId)?.enforced)
return false;
return !pluginViewPreferences[sectionId]?.enforced;
}
@@ -380,10 +398,29 @@ Item {
property bool _pluginPhaseForceFirst: false
property var _phase1Items: []
property bool _searchPending: false
// Leading-edge debounce: search immediately when idle, coalesce bursts
function requestSearch() {
if (searchDebounce.running) {
_searchPending = true;
searchDebounce.restart();
return;
}
_searchPending = false;
performSearch();
searchDebounce.restart();
}
Timer {
id: searchDebounce
interval: 60
onTriggered: root.performSearch()
onTriggered: {
if (!root._searchPending)
return;
root._searchPending = false;
root.performSearch();
}
}
Timer {
@@ -409,7 +446,7 @@ Item {
_phase1Items = [];
pluginPhaseTimer.stop();
searchQuery = query;
searchDebounce.restart();
requestSearch();
if (searchMode !== "plugins" && query.startsWith("/")) {
var prefix = Utils.parseFileSearchPrefix(query);
@@ -694,6 +731,7 @@ Item {
if (triggerMatch.isBuiltIn) {
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
for (var j = 0; j < builtInItems.length; j++) {
builtInItems[j]._preScored = 1000 - j;
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
}
}
@@ -892,23 +930,10 @@ Item {
searchCompleted();
return;
} else if (!searchQuery) {
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
for (var i = 0; i < emptyTriggerOrdered.length; i++) {
var plugin = emptyTriggerOrdered[i];
if (plugin.isBuiltIn) {
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
for (var j = 0; j < blItems.length; j++)
allItems.push(transformBuiltInSearchItem(blItems[j], plugin.id));
} else {
var pItems = getPluginItems(plugin.id, searchQuery);
for (var j = 0; j < pItems.length; j++)
allItems.push(pItems[j]);
}
}
var browseItems = getPluginBrowseItems();
for (var i = 0; i < browseItems.length; i++)
allItems.push(browseItems[i]);
_pluginPhasePending = true;
_phase1Items = allItems.slice();
_pluginPhaseForceFirst = shouldResetSelection;
pluginPhaseTimer.restart();
}
}
@@ -939,11 +964,6 @@ Item {
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
AppSearchService.setCachedDefaultSections(sections, flatModel);
_saveDiskCache(sections);
}
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
@@ -953,7 +973,9 @@ Item {
function _performPluginPhase() {
_pluginPhasePending = false;
if (!searchQuery || searchQuery.length < 2 || searchMode !== "all")
if (searchMode !== "all")
return;
if (searchQuery && searchQuery.length < 2)
return;
var currentVersion = _searchVersion;
@@ -961,27 +983,49 @@ Item {
var allItems = _phase1Items;
_phase1Items = [];
var allPluginsOrdered = getAllVisiblePluginsOrdered();
var maxPerPlugin = 10;
for (var i = 0; i < allPluginsOrdered.length; i++) {
if (currentVersion !== _searchVersion)
return;
var plugin = allPluginsOrdered[i];
if (plugin.isBuiltIn && (plugin.id === "dms_settings_search" || plugin.id === "dms_clipboard_search"))
continue;
if (plugin.isBuiltIn) {
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
var blLimit = Math.min(blItems.length, maxPerPlugin);
for (var j = 0; j < blLimit; j++) {
var item = transformBuiltInSearchItem(blItems[j], plugin.id);
item._preScored = 900 - j;
allItems.push(item);
if (!searchQuery) {
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
for (var i = 0; i < emptyTriggerOrdered.length; i++) {
if (currentVersion !== _searchVersion)
return;
var plugin = emptyTriggerOrdered[i];
if (plugin.isBuiltIn) {
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
for (var j = 0; j < blItems.length; j++)
allItems.push(transformBuiltInSearchItem(blItems[j], plugin.id));
} else {
var pItems = getPluginItems(plugin.id, searchQuery);
for (var j = 0; j < pItems.length; j++)
allItems.push(pItems[j]);
}
} else {
var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
for (var j = 0; j < pItems.length; j++) {
pItems[j]._preScored = 900 - j;
allItems.push(pItems[j]);
}
var browseItems = getPluginBrowseItems();
for (var i = 0; i < browseItems.length; i++)
allItems.push(browseItems[i]);
} else {
var allPluginsOrdered = getAllVisiblePluginsOrdered();
var maxPerPlugin = 10;
for (var i = 0; i < allPluginsOrdered.length; i++) {
if (currentVersion !== _searchVersion)
return;
var plugin = allPluginsOrdered[i];
if (plugin.isBuiltIn && (plugin.id === "dms_settings_search" || plugin.id === "dms_clipboard_search"))
continue;
if (plugin.isBuiltIn) {
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
var blLimit = Math.min(blItems.length, maxPerPlugin);
for (var j = 0; j < blLimit; j++) {
var item = transformBuiltInSearchItem(blItems[j], plugin.id);
item._preScored = 900 - j;
allItems.push(item);
}
} else {
var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
for (var j = 0; j < pItems.length; j++) {
pItems[j]._preScored = 900 - j;
allItems.push(pItems[j]);
}
}
}
}
@@ -991,7 +1035,8 @@ Item {
var dynamicDefs = buildDynamicSectionDefs(allItems);
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50);
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, searchQuery ? 50 : 500);
if (currentVersion !== _searchVersion)
return;
@@ -1005,6 +1050,12 @@ Item {
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
if (!AppSearchService.isCacheValid() && !searchQuery && !pluginFilter) {
AppSearchService.setCachedDefaultSections(sections, flatModel);
_saveDiskCache(sections);
}
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -1198,8 +1249,12 @@ Item {
}
function transformBuiltInSearchItem(item, pluginId) {
if (pluginId === "dms_clipboard_search" || item.type === "clipboard")
return transformClipboardEntry(item.data || item);
if (pluginId === "dms_clipboard_search" || item.type === "clipboard") {
var transformed = transformClipboardEntry(item.data || item);
if (item._preScored !== undefined)
transformed._preScored = item._preScored;
return transformed;
}
return transformBuiltInLauncherItem(item, pluginId);
}
@@ -1871,7 +1926,8 @@ Item {
}
function executeSelected() {
if (searchDebounce.running) {
if (_searchPending) {
_searchPending = false;
searchDebounce.stop();
performSearch();
}
@@ -120,6 +120,18 @@ Item {
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
// Blur region isn't auto-committed on geometry changes; kick twice to catch resize settling.
function _kickBlurCommit() {
launcherBlur.kick();
Qt.callLater(launcherBlur.kick);
}
onAlignedXChanged: _kickBlurCommit()
onAlignedYChanged: _kickBlurCommit()
onAlignedWidthChanged: _kickBlurCommit()
on_ContentImplicitHChanged: _kickBlurCommit()
onContentVisibleChanged: _kickBlurCommit()
signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) {
@@ -328,6 +340,7 @@ Item {
exclusionMode: ExclusionMode.Ignore
WindowBlur {
id: launcherBlur
targetWindow: launcherWindow
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
blurX: modalContainer.x
@@ -337,6 +350,9 @@ Item {
blurRadius: root.cornerRadius
}
onWidthChanged: root._kickBlurCommit()
onHeightChanged: root._kickBlurCommit()
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
@@ -421,6 +437,9 @@ Item {
opacity: contentVisible ? 1 : 0
onOpacityChanged: root._kickBlurCommit()
onSlideOffsetChanged: root._kickBlurCommit()
Behavior on opacity {
NumberAnimation {
duration: contentVisible ? root._openDuration : root._closeDuration
@@ -82,6 +82,12 @@ Item {
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real windowHeight: alignedHeight + contentY + shadowPad
onAlignedXChanged: _kickBlurCommit()
onAlignedYChanged: _kickBlurCommit()
onAlignedWidthChanged: _kickBlurCommit()
onAlignedHeightChanged: _kickBlurCommit()
onContentVisibleChanged: _kickBlurCommit()
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
@@ -113,6 +119,11 @@ Item {
signal dialogClosed
function _kickBlurCommit() {
if (typeof launcherWindow.update === "function")
launcherWindow.update();
}
function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || "";
_pendingMode = mode || "";
@@ -366,11 +377,13 @@ Item {
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.publishedScale)
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
readonly property real visibleScale: s * op
// Blur tracks the surface's scaled rect
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
blurWidth: contentVisible ? modalContainer.width * s : 0
blurHeight: contentVisible ? modalContainer.height * s : 0
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5
blurWidth: contentVisible ? modalContainer.width * visibleScale : 0
blurHeight: contentVisible ? modalContainer.height * visibleScale : 0
blurRadius: root.cornerRadius
}
@@ -460,6 +473,8 @@ Item {
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
onOpacityChanged: root._kickBlurCommit()
onPublishedScaleChanged: root._kickBlurCommit()
Behavior on opacity {
NumberAnimation {
@@ -54,14 +54,7 @@ FocusScope {
return;
editingApp = app;
editAppId = app.id || app.execString || app.exec || "";
var existing = SessionData.getAppOverride(editAppId);
editNameField.text = existing?.name || "";
editIconField.text = existing?.icon || "";
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus());
}
function closeEditMode() {
@@ -71,27 +64,6 @@ FocusScope {
Qt.callLater(() => searchField.forceActiveFocus());
}
function saveAppOverride() {
var override = {};
if (editNameField.text.trim())
override.name = editNameField.text.trim();
if (editIconField.text.trim())
override.icon = editIconField.text.trim();
if (editCommentField.text.trim())
override.comment = editCommentField.text.trim();
if (editEnvVarsField.text.trim())
override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim();
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
function resetAppOverride() {
SessionData.clearAppOverride(editAppId);
closeEditMode();
}
function showContextMenu(item, x, y, fromKeyboard) {
if (!item)
return;
@@ -817,291 +789,21 @@ FocusScope {
}
}
FocusScope {
id: editView
Loader {
id: editLoader
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: editMode
focus: editMode
active: root.editMode
visible: active
focus: root.editMode
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
closeEditMode();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
sourceComponent: AppEditView {
focus: true
editingApp: root.editingApp
editAppId: root.editAppId
onCloseRequested: root.closeEditMode()
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: backButtonArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceHover, 0)
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: 20
color: Theme.surfaceText
}
MouseArea {
id: backButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: closeEditMode()
}
}
Image {
width: 40
height: 40
source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
sourceSize.width: 40
sourceSize.height: 40
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
StyledText {
text: I18n.tr("Edit App")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: editingApp?.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outlineMedium
}
Flickable {
width: parent.width
height: parent.height - y - buttonsRow.height - Theme.spacingM
contentHeight: editFieldsColumn.height
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: editFieldsColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Name")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editNameField
width: parent.width
placeholderText: editingApp?.name || ""
keyNavigationTab: editIconField
keyNavigationBacktab: editExtraFlagsField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editIconField
width: parent.width
placeholderText: editingApp?.icon || ""
keyNavigationTab: editCommentField
keyNavigationBacktab: editNameField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Description")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editCommentField
width: parent.width
placeholderText: editingApp?.comment || ""
keyNavigationTab: editEnvVarsField
keyNavigationBacktab: editIconField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Environment Variables")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "KEY=value KEY2=value2"
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankTextField {
id: editEnvVarsField
width: parent.width
placeholderText: "VAR=value"
keyNavigationTab: editExtraFlagsField
keyNavigationBacktab: editCommentField
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Extra Arguments")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editExtraFlagsField
width: parent.width
placeholderText: "--flag --option=value"
keyNavigationTab: editNameField
keyNavigationBacktab: editEnvVarsField
}
}
}
}
Row {
id: buttonsRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
id: resetButton
width: 90
height: 40
radius: Theme.cornerRadius
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
visible: SessionData.getAppOverride(editAppId) !== null
StyledText {
text: I18n.tr("Reset")
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: resetButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: resetAppOverride()
}
}
Rectangle {
id: cancelButton
width: 90
height: 40
radius: Theme.cornerRadius
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
StyledText {
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: closeEditMode()
}
}
Rectangle {
id: saveButton
width: 90
height: 40
radius: Theme.cornerRadius
color: saveButtonArea.containsMouse ? Theme.withAlpha(Theme.primary, 0.9) : Theme.primary
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: saveButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: saveAppOverride()
}
}
}
}
onLoaded: item.loadOverride()
}
}
@@ -33,7 +33,7 @@ Rectangle {
return item.icon || "";
}
}
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
width: parent?.width ?? 200
height: 52
@@ -241,107 +241,124 @@ Item {
required property var modelData
required property int index
readonly property string rowType: modelData?.type ?? ""
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
Loader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
active: delegateRoot.rowType === "header"
visible: active
sourceComponent: SectionHeader {
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
transientSurfaceTracker: root.transientSurfaceTracker
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
transientSurfaceTracker: root.transientSurfaceTracker
}
ResultItem {
Loader {
anchors.fill: parent
anchors.topMargin: 2
anchors.bottomMargin: 2
visible: delegateRoot.modelData?.type === "list_item"
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
active: delegateRoot.rowType === "list_item"
visible: active
sourceComponent: ResultItem {
item: delegateRoot.modelData?.item ?? null
isSelected: (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && delegateRoot.modelData?.item) {
root.controller.executeItem(delegateRoot.modelData.item);
onClicked: {
if (root.controller && delegateRoot.modelData?.item) {
root.controller.executeItem(delegateRoot.modelData.item);
}
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
}
}
}
Row {
id: gridRowContent
Loader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "grid_row"
active: delegateRoot.rowType === "grid_row"
visible: active
sourceComponent: Row {
Repeater {
model: delegateRoot.modelData?.items ?? []
Repeater {
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
Item {
id: gridCellDelegate
required property var modelData
required property int index
Item {
id: gridCellDelegate
required property var modelData
required property int index
readonly property bool isTile: delegateRoot.modelData?.viewMode === "tile"
readonly property real cellWidth: isTile ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
width: cellWidth
height: delegateRoot.height
width: cellWidth
height: delegateRoot.height
Loader {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
sourceComponent: gridCellDelegate.isTile ? tileCellComponent : gridCellComponent
GridItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "grid"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
Component {
id: gridCellComponent
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
GridItem {
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
Component {
id: tileCellComponent
TileItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
TileItem {
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
}
@@ -48,9 +48,8 @@ Rectangle {
return "file://" + raw;
return raw;
}
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0
readonly property string typeLabel: {
if (!item)
@@ -284,16 +283,10 @@ Rectangle {
anchors.fill: parent
source: root.previewSource
asynchronous: true
sourceSize.width: 128
sourceSize.height: 128
fillMode: Image.PreserveAspectCrop
visible: !root.hasClipboardPreview && !root.previewAnimated
}
AnimatedImage {
anchors.fill: parent
source: root.previewSource
fillMode: Image.PreserveAspectCrop
playing: visible
visible: !root.hasClipboardPreview && root.previewAnimated
visible: !root.hasClipboardPreview
}
ClipboardLauncherPreview {
@@ -165,6 +165,9 @@ Rectangle {
anchors.margins: root.hasScreencopy ? 4 : 0
fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || ""
asynchronous: true
sourceSize.width: 80
sourceSize.height: 80
mipmap: true
}
}
@@ -1,5 +1,5 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Widgets
@@ -98,25 +98,25 @@ StyledRect {
}
property string _videoThumb: ""
property bool _thumbGenAttempted: false
// Probe the thumbnail optimistically; Image.Error triggers generation
onVideoThumbnailPathChanged: {
_videoThumb = "";
if (!videoThumbnailPath)
_thumbGenAttempted = false;
_videoThumb = videoThumbnailPath;
}
function generateVideoThumbnail() {
if (_thumbGenAttempted)
return;
_thumbGenAttempted = true;
_videoThumb = "";
const thumbPath = videoThumbnailPath;
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
const size = _thumbnailPx;
const fp = delegateRoot.filePath;
Paths.mkdir(thumbDir);
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
if (exitCode === 0) {
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
} else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function (output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
});
}
});
}
@@ -160,75 +160,52 @@ StyledRect {
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
anchors.horizontalCenter: parent.horizontalCenter
Image {
id: gridPreviewImage
ClippingRectangle {
anchors.fill: parent
anchors.margins: 2
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
property int weExtIndex: 0
property string imagePath: {
if (weMode && delegateRoot.fileIsDir)
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
if (_videoThumb)
return _videoThumb;
return "";
}
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
onStatusChanged: {
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++;
} else {
imagePath = "";
}
}
}
fillMode: Image.PreserveAspectCrop
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
asynchronous: true
visible: false
}
radius: Theme.cornerRadius
color: "transparent"
CachingImage {
anchors.fill: parent
anchors.margins: 2
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
maxCacheSize: 256
visible: !delegateRoot.fileIsDir && isImage
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridImageMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: gridPreviewImage
maskEnabled: true
maskSource: gridImageMask
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: gridImageMask
anchors.fill: parent
anchors.margins: 2
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
Image {
id: gridPreviewImage
anchors.fill: parent
radius: Theme.cornerRadius
color: "black"
antialiasing: true
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
property int weExtIndex: 0
property string imagePath: {
if (weMode && delegateRoot.fileIsDir)
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
if (_videoThumb)
return _videoThumb;
return "";
}
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
onStatusChanged: {
if (status !== Image.Error)
return;
if (weMode && delegateRoot.fileIsDir) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++;
} else {
imagePath = "";
}
return;
}
if (_videoThumb)
generateVideoThumbnail();
}
fillMode: Image.PreserveAspectCrop
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
asynchronous: true
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
}
CachingImage {
anchors.fill: parent
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
maxCacheSize: 256
animate: false
visible: !delegateRoot.fileIsDir && isImage
}
}
@@ -1,5 +1,5 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Widgets
@@ -95,23 +95,25 @@ StyledRect {
}
property string _videoThumb: ""
property bool _thumbGenAttempted: false
// Probe the thumbnail optimistically; Image.Error triggers generation
onVideoThumbnailPathChanged: {
_videoThumb = "";
if (!videoThumbnailPath)
_thumbGenAttempted = false;
_videoThumb = videoThumbnailPath;
}
function generateVideoThumbnail() {
if (_thumbGenAttempted)
return;
_thumbGenAttempted = true;
_videoThumb = "";
const thumbPath = videoThumbnailPath;
const fp = listDelegateRoot.filePath;
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
if (exitCode === 0) {
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, thumbPath], function (output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
} else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function (output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
});
}
});
}
@@ -165,54 +167,33 @@ StyledRect {
height: 28
anchors.verticalCenter: parent.verticalCenter
Image {
id: listPreviewImage
ClippingRectangle {
anchors.fill: parent
property string imagePath: _videoThumb
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
fillMode: Image.PreserveAspectCrop
sourceSize.width: 32
sourceSize.height: 32
asynchronous: true
visible: false
}
radius: Theme.cornerRadius
color: "transparent"
CachingImage {
anchors.fill: parent
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
maxCacheSize: 256
visible: !listDelegateRoot.fileIsDir && isImage
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listImageMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
MultiEffect {
anchors.fill: parent
source: listPreviewImage
maskEnabled: true
maskSource: listImageMask
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: listImageMask
anchors.fill: parent
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
Image {
id: listPreviewImage
anchors.fill: parent
radius: Theme.cornerRadius
color: "black"
antialiasing: true
property string imagePath: _videoThumb
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
fillMode: Image.PreserveAspectCrop
sourceSize.width: 32
sourceSize.height: 32
asynchronous: true
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
onStatusChanged: {
if (status === Image.Error && _videoThumb)
generateVideoThumbnail();
}
}
CachingImage {
anchors.fill: parent
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
maxCacheSize: 256
animate: false
visible: !listDelegateRoot.fileIsDir && isImage
}
}
+5 -31
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -591,24 +591,11 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: gridProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -729,24 +716,11 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: listProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -108,16 +108,48 @@ Variants {
function invalidate() {
_settleFrames = 3;
backingWindow?.update();
if (!_wedgeBounced)
wedgeWatchdog.restart();
}
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
// Same wedge recovery as WallpaperBackground
property bool _wedgeBounced: false
Timer {
id: wedgeWatchdog
interval: 3000
repeat: false
onTriggered: {
if (!root.backingWindow || !blurWallpaperWindow.visible || IdleService.isShellLocked)
return;
log.warn("no frame swapped on", modelData.name, "since last invalidate, re-attaching surface");
root._wedgeBounced = true;
surfaceReattach.restart();
}
}
Timer {
id: surfaceReattach
interval: 0
repeat: false
onTriggered: {
blurWallpaperWindow.visible = false;
Qt.callLater(() => {
blurWallpaperWindow.visible = true;
});
}
}
Connections {
target: root.backingWindow
function onFrameSwapped() {
if (root._settleFrames > 0)
root._settleFrames--;
root._wedgeBounced = false;
wedgeWatchdog.stop();
}
function onVisibleChanged() {
root.invalidate();
@@ -142,10 +174,29 @@ Variants {
function onWallpaperFillModeChanged() {
root.invalidate();
}
function onWallpaperBackgroundColorModeChanged() {
function onEffectiveWallpaperBackgroundColorChanged() {
root.invalidate();
}
function onWallpaperBackgroundCustomColorChanged() {
}
Connections {
target: SessionData
function onMonitorWallpaperFillModesChanged() {
root.invalidate();
}
function onPerMonitorWallpaperChanged() {
root.invalidate();
}
}
// Theme changes repaint DankBackdrop but nothing else wakes the render loop
Connections {
target: Theme
enabled: root.isColorSource || currentWallpaper.status === Image.Error
function onPrimaryChanged() {
root.invalidate();
}
function onBackgroundChanged() {
root.invalidate();
}
}
@@ -170,6 +221,7 @@ Variants {
}
onSourceChanged: {
invalidate();
if (!source || source.startsWith("#")) {
setWallpaperImmediate("");
return;
@@ -190,6 +242,7 @@ Variants {
}
function setWallpaperImmediate(newSource) {
transitionDelayTimer.stop();
transitionAnimation.stop();
root.transitionProgress = 0.0;
root.effectActive = false;
@@ -7,15 +7,24 @@ Item {
id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing
readonly property bool live: visible && isPlaying
width: 20
height: Theme.iconSize
Loader {
active: isPlaying
readonly property real maxBarHeight: Theme.iconSize - 2
readonly property real minBarHeight: 3
onLiveChanged: {
if (!live) {
bars.bandsA = Qt.vector4d(0, 0, 0, 0);
bars.bandsB = Qt.vector2d(0, 0);
}
}
Loader {
active: root.live
sourceComponent: Component {
Ref {
service: CavaService
@@ -23,15 +32,8 @@ Item {
}
}
readonly property real maxBarHeight: Theme.iconSize - 2
readonly property real minBarHeight: 3
readonly property real heightRange: maxBarHeight - minBarHeight
property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight]
Timer {
id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying
running: !CavaService.cavaAvailable && root.live
interval: 500
repeat: true
onTriggered: {
@@ -41,54 +43,32 @@ Item {
Connections {
target: CavaService
enabled: root.live
function onValuesChanged() {
if (!root.isPlaying) {
root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
const v = CavaService.values;
if (v.length < 6)
return;
}
const newHeights = [];
for (let i = 0; i < 6; i++) {
if (CavaService.values.length <= i) {
newHeights.push(root.minBarHeight);
continue;
}
const rawLevel = CavaService.values[i];
if (rawLevel <= 0) {
newHeights.push(root.minBarHeight);
} else if (rawLevel >= 100) {
newHeights.push(root.maxBarHeight);
} else {
newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange);
}
}
root.barHeights = newHeights;
const n = i => {
const x = v[i];
return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01);
};
bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3));
bars.bandsB = Qt.vector2d(n(4), n(5));
}
}
Row {
anchors.centerIn: parent
spacing: 1.5
ShaderEffect {
id: bars
anchors.fill: parent
Repeater {
model: 6
property real widthPx: width
property real heightPx: height
property real minH: root.minBarHeight
property real maxH: root.maxBarHeight
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
property vector2d bandsB: Qt.vector2d(0, 0)
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
Rectangle {
width: 2
height: root.barHeights[index]
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
enabled: root.isPlaying && !CavaService.cavaAvailable
NumberAnimation {
duration: 100
easing.type: Easing.Linear
}
}
}
}
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb")
}
}
@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
@@ -234,16 +233,19 @@ BasePill {
color: Theme.widgetTextColor
}
RowLayout {
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.isVerticalOrientation
readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
IconImage {
id: horizontalAppIcon
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
Layout.preferredHeight: Layout.preferredWidth
width: contentRow.iconSize
height: contentRow.iconSize
anchors.verticalCenter: parent.verticalCenter
visible: root.showIcon && activeWindow && status === Image.Ready
source: {
if (!activeWindow || !activeWindow.appId)
@@ -264,8 +266,10 @@ BasePill {
}
DankIcon {
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
size: Layout.preferredWidth
id: horizontalSteamIcon
width: contentRow.iconSize
size: contentRow.iconSize
anchors.verticalCenter: parent.verticalCenter
name: "sports_esports"
color: Theme.widgetTextColor
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
@@ -280,9 +284,11 @@ BasePill {
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
elide: Text.ElideRight
maximumLineCount: 1
Layout.maximumWidth: compactMode ? 80 : 180
width: Math.min(implicitWidth, compactMode ? 80 : 180)
visible: text.length > 0
}
@@ -291,6 +297,7 @@ BasePill {
text: compactMode ? "" : "•"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text
}
@@ -318,9 +325,24 @@ BasePill {
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
elide: Text.ElideRight
maximumLineCount: 1
Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
width: {
const sp = contentRow.spacing;
let used = 0;
if (horizontalAppIcon.visible)
used += horizontalAppIcon.width + sp;
else if (horizontalSteamIcon.visible)
used += horizontalSteamIcon.width + sp;
if (appText.visible)
used += appText.width + sp;
if (appSeparator.visible)
used += appSeparator.width + sp;
const budget = root.maxWidth - root.horizontalPadding * 2 - used;
return Math.min(implicitWidth, Math.max(0, budget));
}
visible: text.length > 0
}
}
@@ -182,7 +182,7 @@ Item {
function mapWorkspace(ws) {
return {
"num": ws.number,
"name": ws.name,
"name": stripSwayWorkspaceNumber(ws.number, ws.name),
"focused": ws.focused,
"active": ws.active,
"urgent": ws.urgent,
@@ -202,6 +202,18 @@ Item {
];
}
// sway/scroll fold `<num>:<name>` into the name field (num 1 name "1:test"); drop the redundant prefix so the index option controls it
function stripSwayWorkspaceNumber(num, name) {
if (num === undefined || num === -1)
return name;
if (typeof name !== "string")
return name;
const prefix = num + ":";
if (!name.startsWith(prefix))
return name;
return name.slice(prefix.length);
}
// Numbered workspaces first in ascending order; purely-named workspaces (sway reports num -1) after, by name
function swayWorkspaceOrder(a, b) {
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
@@ -240,8 +240,10 @@ DankPopout {
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
mainContainer.forceActiveFocus();
if (!root.shouldBeVisible)
return;
mainContainer.forceActiveFocus();
tabBar.snapIndicator();
}
}
@@ -411,6 +413,7 @@ DankPopout {
anchors.fill: parent
active: root.currentTabId === "media"
visible: active
asynchronous: true
sourceComponent: Component {
MediaPlayerTab {
targetScreen: root.screen
@@ -462,7 +465,7 @@ DankPopout {
DankSpinner {
anchors.centerIn: parent
size: 40
visible: wallpaperLoader.active && wallpaperLoader.status === Loader.Loading
visible: (wallpaperLoader.active && wallpaperLoader.status === Loader.Loading) || (mediaLoader.active && mediaLoader.status === Loader.Loading) || (weatherLoader.active && weatherLoader.status === Loader.Loading)
}
Loader {
@@ -470,6 +473,7 @@ DankPopout {
anchors.fill: parent
active: root.currentTabId === "weather"
visible: active
asynchronous: true
sourceComponent: Component {
WeatherTab {}
}
@@ -44,6 +44,8 @@ Item {
property int __panelHoverCount: 0
readonly property bool overlayBlurActive: dropdownBlur.active
onDropdownTypeChanged: {
if (dropdownType === 0) {
__panelHoverCount = 0;
@@ -75,8 +77,10 @@ Item {
}
WindowBlur {
id: dropdownBlur
targetWindow: root.targetWindow
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
blurEnabled: active && Theme.connectedSurfaceBlurEnabled
readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1
blurX: root.__activePanel ? root.__activePanel.x + root.__activePanel.width * (1 - s) * 0.5 : 0
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
+113 -54
View File
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Services.Mpris
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -24,6 +25,11 @@ Item {
property string section: ""
property int barPosition: SettingsData.Position.Top
readonly property color accent: MediaAccentService.accent
readonly property color onAccent: MediaAccentService.onAccent
readonly property color accentHover: MediaAccentService.accentHover
readonly property color accentPressed: MediaAccentService.accentPressed
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
@@ -70,7 +76,7 @@ Item {
property bool _switchHold: false
Timer {
id: _switchHoldTimer
interval: 650
interval: 1500
repeat: false
onTriggered: _switchHold = false
}
@@ -78,7 +84,8 @@ Item {
onActivePlayerChanged: {
if (!activePlayer) {
isSwitching = false;
_switchHold = false;
_switchHold = true;
_switchHoldTimer.restart();
return;
}
isSwitching = true;
@@ -117,14 +124,10 @@ Item {
Connections {
target: MprisController
function onAvailablePlayersChanged() {
const count = (MprisController.availablePlayers?.length || 0);
if (count === 0) {
if ((MprisController.availablePlayers?.length || 0) === 0)
isSwitching = false;
_switchHold = false;
} else {
_switchHold = true;
_switchHoldTimer.restart();
}
_switchHold = true;
_switchHoldTimer.restart();
}
}
@@ -290,34 +293,95 @@ Item {
Item {
id: bgContainer
anchors.fill: parent
visible: TrackArtService.resolvedArtUrl !== ""
Image {
id: bgImage
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
source: TrackArtService.resolvedArtUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
onStatusChanged: {
if (status === Image.Ready)
maybeFinishSwitch();
// Fall back to the live mpris url so the background is never blank.
readonly property string curArt: {
const resolved = TrackArtService.resolvedArtUrl;
if (resolved !== "")
return resolved;
const p = root.activePlayer;
if (!p)
return "";
if (p.trackArtUrl)
return p.trackArtUrl;
const m = p.metadata;
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
}
// Two layers crossfade: new art loads into the hidden one and fades in once decoded.
property bool _showA: true
visible: layerA.ready || layerB.ready
onCurArtChanged: syncArt()
Component.onCompleted: syncArt()
function syncArt() {
if (curArt === "")
return;
const front = _showA ? layerA : layerB;
const back = _showA ? layerB : layerA;
if (front.art == curArt)
return;
if (back.art == curArt) {
// Already decoded in the hidden layer; flip once ready (else promote() does).
if (back.ready)
_showA = !_showA;
return;
}
back.art = curArt;
}
Item {
id: blurredBg
// Flip to the hidden layer only when it holds the current art, ignoring stale
// Ready re-emits (e.g. popout re-expose) that would otherwise ping-pong _showA.
function promote(layer) {
const back = _showA ? layerB : layerA;
if (layer !== back)
return;
if (layer.art != curArt)
return;
_showA = (layer === layerA);
root.maybeFinishSwitch();
}
component BgBlurLayer: ClippingRectangle {
id: layer
property alias art: layerImg.source
readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != ""
property bool front: false
signal loaded
anchors.fill: parent
visible: false
radius: Theme.cornerRadius
color: "transparent"
antialiasing: true
opacity: front ? 0.7 : 0
Behavior on opacity {
NumberAnimation {
duration: 350
easing.type: Easing.InOutQuad
}
}
Image {
id: layerImg
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
onStatusChanged: {
if (status === Image.Ready && source != "")
layer.loaded();
}
}
MultiEffect {
anchors.centerIn: parent
width: bgImage.width
height: bgImage.height
source: bgImage
width: layerImg.width
height: layerImg.height
source: layerImg
blurEnabled: true
blurMax: 64
blur: 0.8
@@ -326,22 +390,16 @@ Item {
}
}
Rectangle {
id: bgMask
anchors.fill: parent
radius: Theme.cornerRadius
visible: false
layer.enabled: true
BgBlurLayer {
id: layerA
front: bgContainer._showA
onLoaded: bgContainer.promote(layerA)
}
MultiEffect {
anchors.fill: parent
source: blurredBg
maskEnabled: true
maskSource: bgMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
opacity: 0.7
BgBlurLayer {
id: layerB
front: !bgContainer._showA
onLoaded: bgContainer.promote(layerB)
}
Rectangle {
@@ -442,7 +500,8 @@ Item {
elide: Text.ElideRight
wrapMode: Text.WordWrap
maximumLineCount: 1
visible: text.length > 0
// Reserve the line so late album metadata doesn't shift the seekbar.
height: Math.max(implicitHeight, Theme.fontSizeSmall * 1.4)
}
}
@@ -531,13 +590,13 @@ Item {
height: 40
radius: 20
anchors.centerIn: parent
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
DankIcon {
anchors.centerIn: parent
name: "shuffle"
size: 20
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
}
MouseArea {
@@ -593,13 +652,13 @@ Item {
height: 50
radius: 25
anchors.centerIn: parent
color: Theme.primary
color: root.accent
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28
color: Theme.background
color: root.onAccent
weight: 500
}
@@ -663,7 +722,7 @@ Item {
height: 40
radius: 20
anchors.centerIn: parent
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
DankIcon {
anchors.centerIn: parent
@@ -680,7 +739,7 @@ Item {
}
}
size: 20
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
}
MouseArea {
@@ -719,7 +778,7 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185
color: playerSelectorArea.containsMouse || playersExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
color: playerSelectorArea.containsMouse || playersExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
border.color: Theme.outlineStrong
border.width: 1
z: 100
@@ -786,7 +845,7 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
border.width: 1
z: 101
@@ -798,7 +857,7 @@ Item {
anchors.centerIn: parent
name: getVolumeIcon()
size: 18
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
color: volumeAvailable && currentVolume > 0 ? root.accent : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
}
MouseArea {
@@ -849,7 +908,7 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240
color: audioDevicesArea.containsMouse || devicesExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
color: audioDevicesArea.containsMouse || devicesExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
border.color: Theme.outlineStrong
border.width: 1
z: 100
@@ -154,13 +154,13 @@ Card {
width: 32
height: 32
radius: 16
color: Theme.primary
color: MediaAccentService.accent
DankIcon {
anchors.centerIn: parent
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 16
color: Theme.background
color: MediaAccentService.onAccent
}
MouseArea {
+17 -6
View File
@@ -1,6 +1,7 @@
import QtQuick
import qs.Common
import qs.Modules.DankDash.Overview
import qs.Widgets
Item {
id: root
@@ -17,7 +18,7 @@ Item {
signal navFocusRequested
function handleKeyEvent(event) {
return calendarCard.handleKeyEvent(event);
return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false;
}
Item {
@@ -57,16 +58,26 @@ Item {
height: 220
}
// Calendar - bottom middle (wider and taller)
CalendarOverviewCard {
id: calendarCard
// Calendar - bottom middle; deferred so the grid stays off the emerge frame.
Loader {
id: calendarLoader
x: parent.width * 0.2 - Theme.spacingM
y: 100 + Theme.spacingM
width: parent.width * 0.6
height: 300
asynchronous: true
sourceComponent: Component {
CalendarOverviewCard {
onCloseDash: root.closeDash()
onNavFocusRequested: root.navFocusRequested()
}
}
onCloseDash: root.closeDash()
onNavFocusRequested: root.navFocusRequested()
DankSpinner {
anchors.centerIn: parent
size: 32
visible: calendarLoader.status === Loader.Loading
}
}
// Media - bottom right (narrow and taller)
+266 -284
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import qs.Common
import qs.Services
@@ -245,17 +244,6 @@ Item {
size: Theme.iconSize * 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
layer.enabled: Theme.elevationEnabled
layer.effect: MultiEffect {
shadowEnabled: Theme.elevationEnabled
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
}
}
Column {
@@ -562,295 +550,287 @@ Item {
color: "transparent"
Rectangle {
anchors.fill: parent
opacity: skyBox.backgroundOpacity
gradient: Gradient {
GradientStop {
position: 0.0
color: Theme.withAlpha(skyBox.blackColor, 0.0)
}
GradientStop {
position: 0.05
color: skyBox.topColor
}
GradientStop {
position: 0.3
color: skyBox.topColor
}
GradientStop {
position: 0.5
color: skyBox.topColor
}
GradientStop {
position: 0.501
color: skyBox.blackColor
}
GradientStop {
position: 0.9
color: skyBox.blackColor
}
GradientStop {
position: 1.0
color: Theme.withAlpha(skyBox.blackColor, 0.0)
}
}
}
property var currentDate: dateStepper.currentDate
property var hMargin: 0
property var vMargin: Theme.spacingM
property var effectiveHeight: skyBox.height - 2 * vMargin
property var effectiveWidth: skyBox.width - 2 * hMargin
StyledText {
text: parent.sunTime?.period ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceText, 0.7)
x: 0
y: 0
}
Shape {
id: skyShape
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
height: parent.height / 2
opacity: skyBox.backgroundOpacity
ShapePath {
strokeColor: "transparent"
fillGradient: RadialGradient {
centerX: skyBox.hMargin + sun.x + sun.width / 2
centerY: skyBox.vMargin + sun.y + 30
centerRadius: {
const a = Math.abs((skyBox.sunTime?.dayPercent ?? 0) - 0.5);
const out = 200 * (0.5 - a * a);
return out;
}
focalX: skyBox.hMargin + sun.x + sun.width / 2
focalY: skyBox.vMargin + sun.y
GradientStop {
position: 0
color: skyBox.sunColor
}
GradientStop {
position: 0.3
color: Theme.blendAlpha(skyBox.sunColor, 0.5)
}
GradientStop {
position: 1
color: "transparent"
}
}
PathLine {
x: 0
y: 0
}
PathLine {
x: skyShape.width
y: 0
}
PathLine {
x: skyShape.width
y: skyShape.height
}
PathLine {
x: 0
y: skyShape.height
}
}
ShapePath {
strokeColor: "transparent"
fillGradient: RadialGradient {
centerX: sun.x
centerY: sun.y
centerRadius: 500
focalX: centerX
focalY: centerY + 0.99 * (centerRadius - focalRadius)
focalRadius: 10
GradientStop {
position: 0
color: skyBox.sunColor
}
GradientStop {
position: 0.45
color: skyBox.sunColor
}
GradientStop {
position: 0.55
color: "transparent"
}
GradientStop {
position: 1
color: "transparent"
}
}
PathLine {
x: 0
y: 0
}
PathLine {
x: skyShape.width
y: 0
}
PathLine {
x: skyShape.width
y: skyShape.height
}
PathLine {
x: 0
y: skyShape.height
}
}
}
Canvas {
id: ecliptic
anchors.fill: parent
property var points: WeatherService.getEcliptic(dateStepper.currentDate)
function getX(index) {
return points[index].h * skyBox.effectiveWidth + skyBox.hMargin;
}
function getY(index) {
return points[index].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin;
}
onPointsChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
if (!points || points.length === 0)
return;
ctx.beginPath();
ctx.moveTo(getX(0), getY(0));
for (var i = 1; i < points.length; i++) {
ctx.lineTo(getX(i), getY(i));
}
ctx.strokeStyle = Theme.withAlpha(Theme.outline, 0.2);
ctx.stroke();
}
}
property real latitude: WeatherService.getLocation()?.latitude ?? 0
property real sunDeclination: WeatherService.getSunDeclination(dateStepper.currentDate)
readonly property bool solarNoonIsSouth: latitude > sunDeclination
StyledText {
id: middle
text: skyBox.solarNoonIsSouth ? "S" : "N"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: skyBox.width / 2 - middle.width / 2
y: skyBox.height / 2 - middle.height / 2
}
Loader {
anchors.fill: parent
asynchronous: true
sourceComponent: skyContentComponent
opacity: status === Loader.Ready ? 1 : 0
StyledText {
id: left
text: skyBox.solarNoonIsSouth ? "E" : "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: skyBox.width / 4 - left.width / 2
y: skyBox.height / 2 - left.height / 2
}
StyledText {
id: right
text: skyBox.solarNoonIsSouth ? "W" : "E"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: 3 * skyBox.width / 4 - right.width / 2
y: skyBox.height / 2 - right.height / 2
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: right.right
anchors.right: skyBox.right
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: middle.right
anchors.right: right.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: left.right
anchors.right: middle.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: skyBox.left
anchors.right: left.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
DankNFIcon {
id: moonPhase
name: WeatherService.getMoonPhase(skyBox.currentDate) || ""
size: Theme.fontSizeXLarge
color: Theme.withAlpha(Theme.surfaceText, 0.7)
rotation: (WeatherService.getMoonAngle(skyBox.currentDate) || 0) / Math.PI * 180
visible: !!pos
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
layer.enabled: Theme.elevationEnabled
layer.effect: MultiEffect {
shadowEnabled: Theme.elevationEnabled
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
DankIcon {
id: sun
name: "light_mode"
size: Theme.fontSizeXLarge
color: Theme.primary
visible: !!pos
Component {
id: skyContentComponent
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
Item {
Rectangle {
anchors.fill: parent
opacity: skyBox.backgroundOpacity
layer.enabled: Theme.elevationEnabled
layer.effect: MultiEffect {
shadowEnabled: Theme.elevationEnabled
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
gradient: Gradient {
GradientStop {
position: 0.0
color: Theme.withAlpha(skyBox.blackColor, 0.0)
}
GradientStop {
position: 0.05
color: skyBox.topColor
}
GradientStop {
position: 0.3
color: skyBox.topColor
}
GradientStop {
position: 0.5
color: skyBox.topColor
}
GradientStop {
position: 0.501
color: skyBox.blackColor
}
GradientStop {
position: 0.9
color: skyBox.blackColor
}
GradientStop {
position: 1.0
color: Theme.withAlpha(skyBox.blackColor, 0.0)
}
}
}
StyledText {
text: skyBox.sunTime?.period ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceText, 0.7)
x: 0
y: 0
}
Shape {
id: skyShape
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
height: parent.height / 2
opacity: skyBox.backgroundOpacity
ShapePath {
strokeColor: "transparent"
fillGradient: RadialGradient {
centerX: skyBox.hMargin + sun.x + sun.width / 2
centerY: skyBox.vMargin + sun.y + 30
centerRadius: {
const a = Math.abs((skyBox.sunTime?.dayPercent ?? 0) - 0.5);
const out = 200 * (0.5 - a * a);
return out;
}
focalX: skyBox.hMargin + sun.x + sun.width / 2
focalY: skyBox.vMargin + sun.y
GradientStop {
position: 0
color: skyBox.sunColor
}
GradientStop {
position: 0.3
color: Theme.blendAlpha(skyBox.sunColor, 0.5)
}
GradientStop {
position: 1
color: "transparent"
}
}
PathLine {
x: 0
y: 0
}
PathLine {
x: skyShape.width
y: 0
}
PathLine {
x: skyShape.width
y: skyShape.height
}
PathLine {
x: 0
y: skyShape.height
}
}
ShapePath {
strokeColor: "transparent"
fillGradient: RadialGradient {
centerX: sun.x
centerY: sun.y
centerRadius: 500
focalX: centerX
focalY: centerY + 0.99 * (centerRadius - focalRadius)
focalRadius: 10
GradientStop {
position: 0
color: skyBox.sunColor
}
GradientStop {
position: 0.45
color: skyBox.sunColor
}
GradientStop {
position: 0.55
color: "transparent"
}
GradientStop {
position: 1
color: "transparent"
}
}
PathLine {
x: 0
y: 0
}
PathLine {
x: skyShape.width
y: 0
}
PathLine {
x: skyShape.width
y: skyShape.height
}
PathLine {
x: 0
y: skyShape.height
}
}
}
Shape {
anchors.fill: parent
ShapePath {
strokeColor: Theme.withAlpha(Theme.outline, 0.2)
strokeWidth: 1
fillColor: "transparent"
PathPolyline {
path: {
const points = WeatherService.getEcliptic(dateStepper.currentDate) ?? [];
const out = [];
for (let i = 0; i < points.length; i++) {
out.push(Qt.point(points[i].h * skyBox.effectiveWidth + skyBox.hMargin, points[i].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin));
}
return out;
}
}
}
}
StyledText {
id: middle
text: skyBox.solarNoonIsSouth ? "S" : "N"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: skyBox.width / 2 - middle.width / 2
y: skyBox.height / 2 - middle.height / 2
}
StyledText {
id: left
text: skyBox.solarNoonIsSouth ? "E" : "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: skyBox.width / 4 - left.width / 2
y: skyBox.height / 2 - left.height / 2
}
StyledText {
id: right
text: skyBox.solarNoonIsSouth ? "W" : "E"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: 3 * skyBox.width / 4 - right.width / 2
y: skyBox.height / 2 - right.height / 2
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: right.right
anchors.right: parent.right
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: middle.right
anchors.right: right.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: left.right
anchors.right: middle.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: parent.left
anchors.right: left.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
DankNFIcon {
id: moonPhase
name: WeatherService.getMoonPhase(skyBox.currentDate) || ""
size: Theme.fontSizeXLarge
color: Theme.withAlpha(Theme.surfaceText, 0.7)
rotation: (WeatherService.getMoonAngle(skyBox.currentDate) || 0) / Math.PI * 180
visible: !!pos
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
}
DankIcon {
id: sun
name: "light_mode"
size: Theme.fontSizeXLarge
color: Theme.primary
visible: !!pos
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
}
}
}
@@ -993,6 +973,7 @@ Item {
ListView {
id: hourlyList
anchors.fill: parent
reuseItems: true
orientation: ListView.Horizontal
spacing: Theme.spacingS
clip: true
@@ -1079,6 +1060,7 @@ Item {
ListView {
id: dailyList
anchors.fill: parent
reuseItems: true
orientation: ListView.Horizontal
spacing: Theme.spacingS
clip: true
+12 -10
View File
@@ -807,22 +807,23 @@ PanelWindow {
function _notifBodyScene() {
const isHoriz = SurfaceGeometry.isHorizontal(win._notifDescriptor.barSide);
const body = win._notifBodyGeometry;
const start = win._notifStartUnderlapValue;
const end = win._notifEndUnderlapValue;
const side = win._notifSideUnderlapValue;
if (isHoriz) {
return {
"x": _notifBodyBlurAnchor.x - start,
"y": _notifBodyBlurAnchor.y,
"width": _notifBodyBlurAnchor.width + start + end,
"height": _notifBodyBlurAnchor.height
"x": body.x - start,
"y": body.y,
"width": body.width + start + end,
"height": body.height
};
}
return {
"x": _notifBodyBlurAnchor.x - (win._notifDescriptor.barSide === "left" ? side : 0),
"y": _notifBodyBlurAnchor.y - start,
"width": _notifBodyBlurAnchor.width + side,
"height": _notifBodyBlurAnchor.height + start + end
"x": body.x - (win._notifDescriptor.barSide === "left" ? side : 0),
"y": body.y - start,
"width": body.width + side,
"height": body.height + start + end
};
}
@@ -866,8 +867,9 @@ PanelWindow {
"surfaceRadius": win._surfaceRadius
}
});
const n = win._notifBodyGeometry;
if (win._frameActive && win._notifDescriptor.visible && n.width > 0 && n.height > 0)
const n = win._notifBodyScene();
const nb = win._notifBodyGeometry;
if (win._frameActive && win._notifDescriptor.visible && nb.width > 0 && nb.height > 0)
arr.push({
"side": win._notifDescriptor.barSide,
"body": {"x": n.x, "y": n.y, "width": n.width, "height": n.height},
+5 -31
View File
@@ -1,7 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -567,24 +567,11 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: gridProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -700,24 +687,11 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: listProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -847,6 +847,34 @@ Item {
cursorPosition = pos;
}
function deleteToLineStart() {
clampCursorPosition();
if (cursorPosition === 0)
return;
root.passwordEdited(text.slice(cursorPosition));
cursorPosition = 0;
}
function deleteToLineEnd() {
clampCursorPosition();
if (cursorPosition === text.length)
return;
root.passwordEdited(text.slice(0, cursorPosition));
}
function deleteWordBackward() {
clampCursorPosition();
if (cursorPosition === 0)
return;
let pos = cursorPosition;
while (pos > 0 && text.charAt(pos - 1) === " ")
pos--;
while (pos > 0 && text.charAt(pos - 1) !== " ")
pos--;
root.passwordEdited(text.slice(0, pos) + text.slice(cursorPosition));
cursorPosition = pos;
}
function isPrintableText(value) {
if (value.length === 0)
return false;
@@ -912,6 +940,49 @@ Item {
return;
}
if ((event.modifiers & Qt.ControlModifier) && !(event.modifiers & (Qt.AltModifier | Qt.MetaModifier))) {
switch (event.key) {
case Qt.Key_A:
cursorPosition = 0;
event.accepted = true;
return;
case Qt.Key_E:
cursorPosition = text.length;
event.accepted = true;
return;
case Qt.Key_B:
clampCursorPosition();
cursorPosition = Math.max(0, cursorPosition - 1);
event.accepted = true;
return;
case Qt.Key_F:
clampCursorPosition();
cursorPosition = Math.min(text.length, cursorPosition + 1);
event.accepted = true;
return;
case Qt.Key_U:
deleteToLineStart();
event.accepted = true;
return;
case Qt.Key_K:
deleteToLineEnd();
event.accepted = true;
return;
case Qt.Key_W:
deleteWordBackward();
event.accepted = true;
return;
case Qt.Key_H:
backspace();
event.accepted = true;
return;
case Qt.Key_D:
deleteForward();
event.accepted = true;
return;
}
}
switch (event.key) {
case Qt.Key_Return:
case Qt.Key_Enter:
+78 -3
View File
@@ -13,12 +13,19 @@ Column {
property int draggedIndex: -1
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
readonly property real tabItemSize: 128 + Theme.spacingXS
property int editingIndex: -1
readonly property real tabItemSize: tabRow.dynamicTabWidth + Theme.spacingXS
signal tabSwitched(int tabIndex)
signal tabClosed(int tabIndex)
signal newTabRequested
function commitRename(index, newTitle) {
if (index >= 0)
NotepadStorageService.renameTab(index, newTitle);
editingIndex = -1;
}
function hasUnsavedChangesForTab(tab) {
if (!tab)
return false;
@@ -32,11 +39,19 @@ Column {
spacing: Theme.spacingXS
Row {
id: tabRow
width: parent.width
height: 36
spacing: Theme.spacingXS
readonly property real dynamicTabWidth: {
var count = Math.max(1, NotepadStorageService.tabs.length);
var raw = (tabScroll.width - (count - 1) * Theme.spacingXS) / count;
return Math.max(128, Math.min(300, raw));
}
ScrollView {
id: tabScroll
width: parent.width - newTabButton.width - Theme.spacingXS
height: parent.height
clip: true
@@ -57,7 +72,8 @@ Column {
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
readonly property real tabWidth: 128
readonly property bool editing: root.editingIndex === index
readonly property real tabWidth: tabRow.dynamicTabWidth
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
@@ -138,6 +154,7 @@ Column {
StyledText {
id: tabText
visible: !delegateItem.editing
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
text: {
var prefix = "";
@@ -155,13 +172,54 @@ Column {
anchors.verticalCenter: parent.verticalCenter
}
TextInput {
id: renameField
visible: delegateItem.editing
enabled: delegateItem.editing
width: parent.width
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
selectionColor: Theme.primary
selectedTextColor: Theme.background
selectByMouse: true
clip: true
onEditingFinished: root.commitRename(index, text)
Keys.onEscapePressed: event => {
text = modelData.title || "Untitled";
root.editingIndex = -1;
event.accepted = true;
}
// A tab switch re-focuses the editor via Qt.callLater; the
// timer fires afterwards so the field keeps focus + selection.
Timer {
id: renameFocusTimer
interval: 20
repeat: false
onTriggered: {
renameField.forceActiveFocus();
renameField.selectAll();
}
}
onVisibleChanged: {
if (!visible)
return;
text = modelData.title || "Untitled";
renameFocusTimer.restart();
}
}
Rectangle {
id: tabCloseButton
width: 20
height: 20
radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
visible: NotepadStorageService.tabs.length > 1
visible: NotepadStorageService.tabs.length > 1 && !delegateItem.editing
anchors.verticalCenter: parent.verticalCenter
DankIcon {
@@ -195,11 +253,24 @@ Column {
MouseArea {
id: tabMouseArea
anchors.fill: parent
enabled: !delegateItem.editing
hoverEnabled: true
preventStealing: dragging || longPressing
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onDoubleClicked: {
root.tabSwitched(index);
root.editingIndex = index;
}
onExited: tabTooltip.hide()
onContainsMouseChanged: {
if (containsMouse && tabText.truncated)
tabTooltip.show(modelData.title || "Untitled", delegateItem, 0, 0, "bottom");
}
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
@@ -279,4 +350,8 @@ Column {
onClicked: root.newTabRequested()
}
}
DankTooltipV2 {
id: tabTooltip
}
}
+5 -21
View File
@@ -4,6 +4,7 @@ import qs.Common
import qs.Services
import qs.Widgets
import Quickshell.Services.Mpris
import Quickshell.Widgets
DankOSD {
id: root
@@ -185,10 +186,11 @@ DankOSD {
visible: false
}
Item {
id: blurredBg
ClippingRectangle {
anchors.fill: parent
visible: false
radius: Theme.cornerRadius
color: "transparent"
opacity: 0.7
MultiEffect {
anchors.centerIn: parent
@@ -203,24 +205,6 @@ DankOSD {
}
}
Rectangle {
id: bgMask
anchors.fill: parent
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
MultiEffect {
anchors.fill: parent
source: blurredBg
maskEnabled: true
maskSource: bgMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
opacity: 0.7
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
+20 -31
View File
@@ -1,7 +1,7 @@
import QtCore
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
@@ -520,28 +520,27 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = Theme.wallpaperPath;
if (!wp || wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
sourceSize.width: 120
sourceSize.height: 120
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: autoWallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = Theme.wallpaperPath;
if (!wp || wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
sourceSize.width: 120
sourceSize.height: 120
asynchronous: true
}
}
@@ -553,16 +552,6 @@ Item {
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
}
Rectangle {
id: autoWallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
+64 -97
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
@@ -75,28 +75,27 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = root.currentWallpaper;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: wallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = root.currentWallpaper;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
}
}
@@ -108,16 +107,6 @@ Item {
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
}
Rectangle {
id: wallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: "image"
@@ -421,31 +410,30 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = SessionData.wallpaperPathLight;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var lightWallpaper = SessionData.wallpaperPathLight;
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: lightMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = SessionData.wallpaperPathLight;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var lightWallpaper = SessionData.wallpaperPathLight;
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
}
}
@@ -463,16 +451,6 @@ Item {
}
}
Rectangle {
id: lightMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: "light_mode"
@@ -611,31 +589,30 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = SessionData.wallpaperPathDark;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var darkWallpaper = SessionData.wallpaperPathDark;
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: darkMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = SessionData.wallpaperPathDark;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var darkWallpaper = SessionData.wallpaperPathDark;
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
}
}
@@ -653,16 +630,6 @@ Item {
}
}
Rectangle {
id: darkMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: "dark_mode"
+53 -5
View File
@@ -141,16 +141,37 @@ Variants {
function invalidate() {
_settleFrames = 3;
backingWindow?.update();
if (!_wedgeBounced)
wedgeWatchdog.restart();
}
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
// No swap after a requested frame: dropped frame callback left the window
// unexposed (qtwayland mFrameCallbackTimedOut); only surface re-attach recovers.
property bool _wedgeBounced: false
Timer {
id: wedgeWatchdog
interval: 3000
repeat: false
onTriggered: {
if (!root.backingWindow || !wallpaperWindow.visible || IdleService.isShellLocked)
return;
log.warn("no frame swapped on", modelData.name, "since last invalidate, re-attaching surface");
root._wedgeBounced = true;
surfaceReattach.restart();
}
}
Connections {
target: root.backingWindow
function onFrameSwapped() {
if (root._settleFrames > 0)
root._settleFrames--;
root._wedgeBounced = false;
wedgeWatchdog.stop();
}
function onVisibleChanged() {
root.invalidate();
@@ -176,10 +197,29 @@ Variants {
function onWallpaperFillModeChanged() {
root.invalidate();
}
function onWallpaperBackgroundColorModeChanged() {
function onEffectiveWallpaperBackgroundColorChanged() {
root.invalidate();
}
function onWallpaperBackgroundCustomColorChanged() {
}
Connections {
target: SessionData
function onMonitorWallpaperFillModesChanged() {
root.invalidate();
}
function onPerMonitorWallpaperChanged() {
root.invalidate();
}
}
// Theme changes repaint DankBackdrop but nothing else wakes the render loop
Connections {
target: Theme
enabled: root.isColorSource || currentWallpaper.status === Image.Error
function onPrimaryChanged() {
root.invalidate();
}
function onBackgroundChanged() {
root.invalidate();
}
}
@@ -502,13 +542,13 @@ Variants {
}
onSourceChanged: {
invalidate();
if (!source || source.startsWith("#")) {
setWallpaperImmediate("");
return;
}
root.changePending = true;
invalidate();
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
@@ -528,10 +568,14 @@ Variants {
}
function setWallpaperImmediate(newSource) {
transitionDelayTimer.stop();
transitionAnimation.stop();
root.transitionProgress = 0.0;
root.effectActive = false;
root.screenScale = CompositorService.getScreenScale(modelData);
// No status change coming to clear the flag
if (!newSource || currentWallpaper.source.toString() === newSource)
root.changePending = false;
currentWallpaper.source = newSource;
nextWallpaper.source = "";
@@ -567,10 +611,14 @@ Variants {
}
function changeWallpaper(newPath, force) {
if (!force && newPath === currentWallpaper.source)
if (!force && newPath === currentWallpaper.source.toString()) {
root.changePending = false;
return;
if (!newPath || newPath.startsWith("#"))
}
if (!newPath || newPath.startsWith("#")) {
root.changePending = false;
return;
}
root.screenScale = CompositorService.getScreenScale(modelData);
if (root.transitioning || root.effectActive) {
root.pendingWallpaper = newPath;
@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
Item {
@@ -58,23 +58,6 @@ Item {
visible: intersectsViewport
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
Rectangle {
id: maskRect
width: root.width
height: root.height
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: maskRect
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
@@ -104,51 +87,55 @@ Item {
}
}
ScreencopyView {
id: windowPreview
ClippingRectangle {
anchors.fill: parent
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
live: true
radius: Theme.cornerRadius
color: "transparent"
Rectangle {
ScreencopyView {
id: windowPreview
anchors.fill: parent
radius: Theme.cornerRadius
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
live: true
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.fontSizeSmall * 0.5
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
Image {
id: windowIcon
property var iconSize: {
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
}
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.fontSizeSmall * 0.5
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
Image {
id: windowIcon
property var iconSize: {
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
}
}
+5 -1
View File
@@ -298,7 +298,11 @@ Singleton {
function getBuiltInLauncherItems(pluginId, query) {
if (pluginId === "dms_clipboard_search") {
const trimmed = (query || "").toString().trim();
const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
const entries = ClipboardService.getCachedLauncherSearchEntries(trimmed, 20).slice().sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return (b.id || 0) - (a.id || 0);
});
return entries.map(entry => ({
type: "clipboard",
data: entry
+22 -19
View File
@@ -841,13 +841,32 @@ EOFCONFIG
function setVolume(percentage) {
if (!root.sink?.audio)
return "No audio sink available";
if (isNaN(percentage))
return "Invalid percentage";
const maxVol = root.sinkMaxVolume;
const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
root.sink.audio.volume = clampedVolume / 100;
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", String(clampedVolume / 100)]);
return `Volume set to ${clampedVolume}%`;
}
function outputVolumeStep(step) {
const parsed = parseInt(step || "5");
return isNaN(parsed) ? 5 : Math.max(0, parsed);
}
function adjustDefaultSinkVolume(step, direction) {
const maxVol = root.sinkMaxVolume;
const stepValue = outputVolumeStep(step);
const currentVolume = Math.round(root.sink.audio.volume * 100);
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + direction * stepValue));
const suffix = direction > 0 ? "+" : "-";
Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "0"]);
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", `${stepValue}%${suffix}`]);
return newVolume;
}
function toggleMute() {
if (!root.sink?.audio) {
return "No audio sink available";
@@ -939,15 +958,7 @@ EOFCONFIG
if (!root.sink?.audio)
return "No audio sink available";
if (root.sink.audio.muted)
root.sink.audio.muted = false;
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + stepValue));
root.sink.audio.volume = newVolume / 100;
const newVolume = root.adjustDefaultSinkVolume(step, 1);
return `Volume increased to ${newVolume}%`;
}
@@ -955,15 +966,7 @@ EOFCONFIG
if (!root.sink?.audio)
return "No audio sink available";
if (root.sink.audio.muted)
root.sink.audio.muted = false;
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(maxVol, currentVolume - stepValue));
root.sink.audio.volume = newVolume / 100;
const newVolume = root.adjustDefaultSinkVolume(step, -1);
return `Volume decreased to ${newVolume}%`;
}
+2 -2
View File
@@ -12,8 +12,8 @@ Singleton {
id: root
readonly property var log: Log.scoped("ChangelogService")
readonly property string currentVersion: "1.4"
readonly property bool changelogEnabled: false
readonly property string currentVersion: "1.5"
readonly property bool changelogEnabled: true
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
+19 -84
View File
@@ -30,7 +30,6 @@ Singleton {
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property int refCount: 0
property real _launcherLastRefresh: 0
property bool _launcherCacheValid: false
property string _launcherCachedQuery: ""
property var _launcherCachedEntries: []
@@ -85,31 +84,30 @@ Singleton {
}
function updateFilteredModel() {
let filtered = internalEntries;
const query = searchText.trim().toLowerCase();
const filterAll = activeFilter === "all";
const unpinned = [];
const pinned = [];
if (activeFilter !== "all") {
filtered = filtered.filter(entry => getEntryType(entry) === activeFilter);
for (let i = 0; i < internalEntries.length; i++) {
const entry = internalEntries[i];
if (!filterAll && getEntryType(entry) !== activeFilter)
continue;
if (query.length > 0 && !entry.preview.toLowerCase().includes(query))
continue;
(entry.pinned ? pinned : unpinned).push(entry);
}
const query = searchText.trim();
const byIdDesc = (a, b) => b.id - a.id;
pinned.sort(byIdDesc);
unpinned.sort(byIdDesc);
if (query.length > 0) {
const lowerQuery = query.toLowerCase();
filtered = filtered.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
}
filtered.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return b.id - a.id;
});
clipboardEntries = filtered;
unpinnedEntries = filtered.filter(e => !e.pinned);
pinnedEntries = filtered.filter(e => e.pinned);
pinnedEntries = pinned;
unpinnedEntries = unpinned;
clipboardEntries = pinned.concat(unpinned);
totalCount = clipboardEntries.length;
const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length);
const activeCount = Math.max(unpinned.length, pinned.length);
if (activeCount === 0) {
keyboardNavigationActive = false;
@@ -117,9 +115,8 @@ Singleton {
return;
}
if (selectedIndex >= activeCount) {
if (selectedIndex >= activeCount)
selectedIndex = activeCount - 1;
}
}
function refresh() {
@@ -138,18 +135,6 @@ Singleton {
});
}
function ensureLauncherHistory() {
if (!clipboardAvailable) {
return;
}
const now = Date.now();
if (internalEntries.length === 0 || now - _launcherLastRefresh > 5000) {
_launcherLastRefresh = now;
refresh();
}
}
function requestLauncherSearch(query, limit) {
if (!clipboardAvailable) {
return;
@@ -207,56 +192,6 @@ Singleton {
_launcherSearchSeq++;
}
function getLauncherEntries(query, limit, minLength) {
if (!clipboardAvailable) {
return [];
}
const trimmed = (query || "").toString().trim();
const requiredLength = minLength !== undefined ? minLength : 2;
if (trimmed.length < requiredLength) {
return [];
}
const lowerQuery = trimmed.toLowerCase();
const maxItems = limit > 0 ? limit : 8;
const matches = [];
for (var i = 0; i < internalEntries.length; i++) {
const entry = internalEntries[i];
const preview = getEntryPreview(entry).toString();
const typeText = entry.isImage ? "image picture screenshot clipboard" : "text clipboard";
const haystack = (preview + " " + typeText).toLowerCase();
if (haystack.indexOf(lowerQuery) === -1) {
continue;
}
matches.push(entry);
}
matches.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return (b.id || 0) - (a.id || 0);
});
return matches.slice(0, maxItems);
}
function getRecentLauncherEntries(limit) {
if (!clipboardAvailable) {
return [];
}
const maxItems = limit > 0 ? limit : 20;
const entries = internalEntries.slice();
entries.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return (b.id || 0) - (a.id || 0);
});
return entries.slice(0, maxItems);
}
function reset() {
searchText = "";
selectedIndex = 0;
@@ -0,0 +1,76 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Common
import qs.Services
// Accent color extracted from the current track's album art via ColorQuantizer,
// falling back to Theme.primary when no usable accent is available.
Singleton {
id: root
readonly property bool hasAccent: _accent !== null
readonly property color accent: _accent !== null ? _accent : Theme.primary
readonly property color onAccent: {
const c = accent;
const lum = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
return lum > 0.6 ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1);
}
readonly property color accentHover: Theme.withAlpha(accent, 0.12)
readonly property color accentPressed: Theme.withAlpha(accent, Theme.transparentBlurLayers ? 0.24 : 0.16)
readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
readonly property string artUrl: TrackArtService.resolvedArtUrl
// Hold the last accent across the brief artUrl blank between tracks; never reset to primary.
property var _accent: null
ColorQuantizer {
id: quantizer
source: root.artUrl
depth: 4
rescaleSize: 64
onColorsChanged: {
const a = root._pickAccent(colors);
if (a !== null)
root._accent = a;
}
}
function _pickAccent(colors) {
if (!colors || colors.length === 0)
return null;
let best = null;
let bestScore = -1;
for (let i = 0; i < colors.length; i++) {
const c = colors[i];
const s = c.hsvSaturation;
const v = c.hsvValue;
if (v < 0.22 || v > 0.96 || s < 0.22)
continue;
const score = s * (1 - Math.abs(v - 0.68));
if (score > bestScore) {
bestScore = score;
best = c;
}
}
if (!best)
return null;
return _normalize(best);
}
function _normalize(c) {
const hue = c.hsvHue < 0 ? 0 : c.hsvHue;
const s = Math.min(1, c.hsvSaturation * 1.05);
const v = Math.max(c.hsvValue, 0.62);
return Qt.hsva(hue, s, v, 1);
}
}
+227 -276
View File
@@ -29,45 +29,48 @@ Singleton {
function setSessionBuffer(tabId, content, baseline) {
if (tabId === undefined || tabId === null || tabId < 0)
return
var next = Object.assign({}, sessionBuffers)
return;
var next = Object.assign({}, sessionBuffers);
if (content !== baseline) {
next[tabId] = { content: content, baseline: baseline }
next[tabId] = {
content: content,
baseline: baseline
};
} else {
delete next[tabId]
delete next[tabId];
}
sessionBuffers = next
sessionBufferRevision++
sessionBuffers = next;
sessionBufferRevision++;
}
function getSessionBuffer(tabId) {
return sessionBuffers[tabId]
return sessionBuffers[tabId];
}
function clearSessionBuffer(tabId) {
if (sessionBuffers[tabId] === undefined)
return
var next = Object.assign({}, sessionBuffers)
delete next[tabId]
sessionBuffers = next
sessionBufferRevision++
return;
var next = Object.assign({}, sessionBuffers);
delete next[tabId];
sessionBuffers = next;
sessionBufferRevision++;
}
property var conflictTabId: -1
property string conflictDiskContent: ""
function flagConflict(tabId, diskContent) {
conflictDiskContent = diskContent
conflictTabId = tabId
conflictDiskContent = diskContent;
conflictTabId = tabId;
}
function clearConflict() {
conflictTabId = -1
conflictDiskContent = ""
conflictTabId = -1;
conflictDiskContent = "";
}
Component.onCompleted: {
ensureDirectories()
ensureDirectories();
}
FileView {
@@ -78,64 +81,66 @@ Singleton {
onLoaded: {
try {
var data = JSON.parse(text())
root.tabs = data.tabs || []
root.currentTabIndex = data.currentTabIndex || 0
root.metadataLoaded = true
root.validateTabs()
} catch(e) {
log.warn("Failed to parse notepad metadata:", e)
root.createDefaultTab()
var data = JSON.parse(text());
root.tabs = data.tabs || [];
root.currentTabIndex = data.currentTabIndex || 0;
root.metadataLoaded = true;
root.validateTabs();
} catch (e) {
log.warn("Failed to parse notepad metadata:", e);
root.createDefaultTab();
}
}
onLoadFailed: {
root.createDefaultTab()
root.createDefaultTab();
}
}
onRefCountChanged: {
if (refCount === 1 && !metadataLoaded) {
metadataFile.path = ""
metadataFile.path = root.metadataPath
metadataFile.path = "";
metadataFile.path = root.metadataPath;
}
}
function ensureDirectories() {
mkdirProcess.running = true
Proc.runCommand("", ["mkdir", "-p", root.baseDir, root.filesDir], null);
}
function loadMetadata() {
metadataFile.path = ""
metadataFile.path = root.metadataPath
metadataFile.path = "";
metadataFile.path = root.metadataPath;
}
function createDefaultTab() {
var id = Date.now()
var filePath = "notepad-files/untitled-" + id + ".txt"
var fullPath = baseDir + "/" + filePath
var id = Date.now();
var filePath = "notepad-files/untitled-" + id + ".txt";
var fullPath = baseDir + "/" + filePath;
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
newTabsBeingCreated[id] = true
tabsBeingCreated = newTabsBeingCreated
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
newTabsBeingCreated[id] = true;
tabsBeingCreated = newTabsBeingCreated;
root.createEmptyFile(fullPath, function() {
root.tabs = [{
id: id,
title: I18n.tr("Untitled"),
filePath: filePath,
isTemporary: true,
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
}]
root.currentTabIndex = 0
root.createEmptyFile(fullPath, function () {
root.tabs = [
{
id: id,
title: I18n.tr("Untitled"),
filePath: filePath,
isTemporary: true,
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
}
];
root.currentTabIndex = 0;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
delete updatedTabsBeingCreated[id]
tabsBeingCreated = updatedTabsBeingCreated
root.saveMetadata()
})
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
delete updatedTabsBeingCreated[id];
tabsBeingCreated = updatedTabsBeingCreated;
root.saveMetadata();
});
}
function saveMetadata() {
@@ -143,82 +148,73 @@ Singleton {
version: 1,
currentTabIndex: currentTabIndex,
tabs: tabs
}
metadataFile.setText(JSON.stringify(metadata, null, 2))
};
metadataFile.setText(JSON.stringify(metadata, null, 2));
}
function getTabById(tabId) {
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].id === tabId)
return tabs[i]
return tabs[i];
}
return null
return null;
}
function loadTabContent(tabIndex, callback) {
if (tabIndex < 0 || tabIndex >= tabs.length) {
callback("")
return
callback("");
return;
}
var tab = tabs[tabIndex]
var requestTabId = tab.id
var fullPath = tab.isTemporary
? baseDir + "/" + tab.filePath
: tab.filePath
var tab = tabs[tabIndex];
var requestTabId = tab.id;
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
if (tabsBeingCreated[tab.id]) {
Qt.callLater(() => {
loadTabContent(tabIndex, callback)
})
return
loadTabContent(tabIndex, callback);
});
return;
}
var fileChecker = fileExistsComponent.createObject(root, {
path: fullPath,
callback: (exists) => {
var currentTab = root.getTabById(requestTabId)
var currentPath = currentTab
? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath)
: ""
Proc.runCommand("", ["test", "-f", fullPath], (output, exitCode) => {
var currentTab = root.getTabById(requestTabId);
var currentPath = currentTab ? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath) : "";
if (!currentTab || currentPath !== fullPath) {
callback("")
return
}
if (exists) {
var loader = tabFileLoaderComponent.createObject(root, {
path: fullPath,
callback: callback
})
} else {
log.warn("Tab file does not exist:", fullPath)
callback("")
}
if (!currentTab || currentPath !== fullPath) {
callback("");
return;
}
})
if (exitCode === 0) {
tabFileLoaderComponent.createObject(root, {
path: fullPath,
callback: callback
});
} else {
log.warn("Tab file does not exist:", fullPath);
callback("");
}
});
}
function saveTabContent(tabIndex, content) {
if (tabIndex < 0 || tabIndex >= tabs.length) return
var tab = tabs[tabIndex]
var fullPath = tab.isTemporary
? baseDir + "/" + tab.filePath
: tab.filePath
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var tab = tabs[tabIndex];
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
var saver = tabFileSaverComponent.createObject(root, {
path: fullPath,
content: content,
tabIndex: tabIndex
})
});
}
function createNewTab() {
var id = Date.now()
var filePath = "notepad-files/untitled-" + id + ".txt"
var fullPath = baseDir + "/" + filePath
var id = Date.now();
var filePath = "notepad-files/untitled-" + id + ".txt";
var fullPath = baseDir + "/" + filePath;
var newTab = {
id: id,
@@ -228,29 +224,29 @@ Singleton {
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
}
};
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
newTabsBeingCreated[id] = true
tabsBeingCreated = newTabsBeingCreated
createEmptyFile(fullPath, function() {
var newTabs = tabs.slice()
newTabs.push(newTab)
tabs = newTabs
currentTabIndex = tabs.length - 1
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
newTabsBeingCreated[id] = true;
tabsBeingCreated = newTabsBeingCreated;
createEmptyFile(fullPath, function () {
var newTabs = tabs.slice();
newTabs.push(newTab);
tabs = newTabs;
currentTabIndex = tabs.length - 1;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
delete updatedTabsBeingCreated[id]
tabsBeingCreated = updatedTabsBeingCreated
saveMetadata()
})
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
delete updatedTabsBeingCreated[id];
tabsBeingCreated = updatedTabsBeingCreated;
saveMetadata();
});
return newTab
return newTab;
}
function createTabForFile(path) {
var id = Date.now()
var fileName = path.split('/').pop()
var id = Date.now();
var fileName = path.split('/').pop();
var newTab = {
id: id,
@@ -260,34 +256,34 @@ Singleton {
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
}
};
var newTabs = tabs.slice()
newTabs.push(newTab)
tabs = newTabs
currentTabIndex = tabs.length - 1
saveMetadata()
var newTabs = tabs.slice();
newTabs.push(newTab);
tabs = newTabs;
currentTabIndex = tabs.length - 1;
saveMetadata();
return newTab
return newTab;
}
function closeTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length) return
var newTabs = tabs.slice()
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1
clearSessionBuffer(closedTabId)
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var newTabs = tabs.slice();
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1;
clearSessionBuffer(closedTabId);
if (conflictTabId === closedTabId)
clearConflict()
clearConflict();
if (newTabs.length <= 1) {
var id = Date.now()
var filePath = "notepad-files/untitled-" + id + ".txt"
var id = Date.now();
var filePath = "notepad-files/untitled-" + id + ".txt";
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
newTabsBeingCreated[id] = true
tabsBeingCreated = newTabsBeingCreated
createEmptyFile(baseDir + "/" + filePath, function() {
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
newTabsBeingCreated[id] = true;
tabsBeingCreated = newTabsBeingCreated;
createEmptyFile(baseDir + "/" + filePath, function () {
newTabs[0] = {
id: id,
title: I18n.tr("Untitled"),
@@ -296,110 +292,129 @@ Singleton {
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
}
currentTabIndex = 0
tabs = newTabs
};
currentTabIndex = 0;
tabs = newTabs;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
delete updatedTabsBeingCreated[id]
tabsBeingCreated = updatedTabsBeingCreated
saveMetadata()
})
return
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
delete updatedTabsBeingCreated[id];
tabsBeingCreated = updatedTabsBeingCreated;
saveMetadata();
});
return;
} else {
var tabToDelete = newTabs[tabIndex]
var tabToDelete = newTabs[tabIndex];
if (tabToDelete && tabToDelete.isTemporary) {
deleteFile(baseDir + "/" + tabToDelete.filePath)
deleteFile(baseDir + "/" + tabToDelete.filePath);
}
newTabs.splice(tabIndex, 1)
newTabs.splice(tabIndex, 1);
if (currentTabIndex >= newTabs.length) {
currentTabIndex = newTabs.length - 1
currentTabIndex = newTabs.length - 1;
} else if (currentTabIndex > tabIndex) {
currentTabIndex -= 1
currentTabIndex -= 1;
}
}
tabs = newTabs
saveMetadata()
tabs = newTabs;
saveMetadata();
}
function switchToTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length) return
currentTabIndex = tabIndex
saveMetadata()
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
currentTabIndex = tabIndex;
saveMetadata();
}
function reorderTab(fromIndex, toIndex) {
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
return
return;
if (fromIndex === toIndex)
return
var newTabs = tabs.slice()
var moved = newTabs.splice(fromIndex, 1)[0]
newTabs.splice(toIndex, 0, moved)
tabs = newTabs
return;
var newTabs = tabs.slice();
var moved = newTabs.splice(fromIndex, 1)[0];
newTabs.splice(toIndex, 0, moved);
tabs = newTabs;
if (currentTabIndex === fromIndex) {
currentTabIndex = toIndex
currentTabIndex = toIndex;
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
currentTabIndex--
currentTabIndex--;
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
currentTabIndex++
currentTabIndex++;
}
saveMetadata()
saveMetadata();
}
function saveTabAs(tabIndex, userPath) {
if (tabIndex < 0 || tabIndex >= tabs.length) return
var tab = tabs[tabIndex]
var fileName = userPath.split('/').pop()
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var tab = tabs[tabIndex];
var fileName = userPath.split('/').pop();
if (tab.isTemporary) {
var tempPath = baseDir + "/" + tab.filePath
copyFile(tempPath, userPath)
deleteFile(tempPath)
var tempPath = baseDir + "/" + tab.filePath;
copyFile(tempPath, userPath);
deleteFile(tempPath);
}
var newTabs = tabs.slice()
var newTabs = tabs.slice();
newTabs[tabIndex] = Object.assign({}, tab, {
title: fileName,
filePath: userPath,
isTemporary: false,
lastModified: new Date().toISOString()
})
tabs = newTabs
saveMetadata()
});
tabs = newTabs;
saveMetadata();
}
function renameTab(tabIndex, newTitle) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var trimmed = (newTitle || "").trim();
var tab = tabs[tabIndex];
if (trimmed.length === 0 || trimmed === tab.title)
return;
if (tab.isTemporary) {
updateTabMetadata(tabIndex, {
title: trimmed
});
return;
}
var dir = tab.filePath.substring(0, tab.filePath.lastIndexOf('/') + 1);
var newPath = dir + trimmed;
moveFile(tab.filePath, newPath);
updateTabMetadata(tabIndex, {
title: trimmed,
filePath: newPath
});
}
function updateTabMetadata(tabIndex, properties) {
if (tabIndex < 0 || tabIndex >= tabs.length) return
var newTabs = tabs.slice()
var updatedTab = Object.assign({}, newTabs[tabIndex], properties)
updatedTab.lastModified = new Date().toISOString()
newTabs[tabIndex] = updatedTab
tabs = newTabs
saveMetadata()
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var newTabs = tabs.slice();
var updatedTab = Object.assign({}, newTabs[tabIndex], properties);
updatedTab.lastModified = new Date().toISOString();
newTabs[tabIndex] = updatedTab;
tabs = newTabs;
saveMetadata();
}
function validateTabs() {
var validTabs = []
var validTabs = [];
for (var i = 0; i < tabs.length; i++) {
var tab = tabs[i]
validTabs.push(tab)
var tab = tabs[i];
validTabs.push(tab);
}
tabs = validTabs
tabs = validTabs;
if (tabs.length === 0) {
root.createDefaultTab()
root.createDefaultTab();
}
}
@@ -411,29 +426,13 @@ Singleton {
preload: true
onLoaded: {
callback(text())
destroy()
callback(text());
destroy();
}
onLoadFailed: {
callback("")
destroy()
}
}
}
Component {
id: fileExistsComponent
Process {
property string path
property var callback
command: ["test", "-f", path]
Component.onCompleted: running = true
onExited: (exitCode) => {
callback(exitCode === 0)
destroy()
callback("");
destroy();
}
}
}
@@ -452,94 +451,46 @@ Singleton {
onSaved: {
if (tabIndex >= 0) {
root.updateTabMetadata(tabIndex, {})
root.updateTabMetadata(tabIndex, {});
}
if (creationCallback) {
creationCallback()
creationCallback();
}
destroy()
destroy();
}
onSaveFailed: {
log.error("Failed to save tab content")
log.error("Failed to save tab content");
if (creationCallback) {
creationCallback()
creationCallback();
}
destroy()
destroy();
}
}
}
function createEmptyFile(path, callback) {
var cleanPath = decodeURI(path.toString())
var cleanPath = decodeURI(path.toString());
if (!cleanPath.startsWith("/")) {
cleanPath = baseDir + "/" + cleanPath
cleanPath = baseDir + "/" + cleanPath;
}
var creator = fileCreatorComponent.createObject(root, {
filePath: cleanPath,
creationCallback: callback
})
Proc.runCommand("", ["touch", cleanPath], (output, exitCode) => {
if (callback)
callback();
});
}
function copyFile(source, destination) {
copyProcess.source = source
copyProcess.destination = destination
copyProcess.running = true
Proc.runCommand("", ["cp", source, destination], null);
}
function deleteFile(path) {
deleteProcess.filePath = path
deleteProcess.running = true
Proc.runCommand("", ["rm", "-f", path], null);
}
Component {
id: fileCreatorComponent
QtObject {
property string filePath
property var creationCallback
Component.onCompleted: {
var touchProcess = touchProcessComponent.createObject(this, {
filePath: filePath,
callback: creationCallback
})
}
}
}
Component {
id: touchProcessComponent
Process {
property string filePath
property var callback
command: ["touch", filePath]
Component.onCompleted: running = true
onExited: (exitCode) => {
if (callback) callback()
destroy()
}
}
}
Process {
id: copyProcess
property string source
property string destination
command: ["cp", source, destination]
}
Process {
id: deleteProcess
property string filePath
command: ["rm", "-f", filePath]
}
Process {
id: mkdirProcess
command: ["mkdir", "-p", root.baseDir, root.filesDir]
function moveFile(source, destination) {
Proc.runCommand("", ["mv", source, destination], null);
}
}
+27 -6
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Singleton {
id: root
@@ -850,10 +851,28 @@ Singleton {
}
}
function notepadSlideoutForFocusedScreen() {
if (!notepadSlideouts || notepadSlideouts.length === 0)
return null;
const focused = BarWidgetService.getFocusedScreenName();
if (focused) {
for (var i = 0; i < notepadSlideouts.length; i++) {
if (notepadSlideouts[i]?.modelData?.name === focused)
return notepadSlideouts[i];
}
}
return notepadSlideouts[0];
}
// Remembered presentation wins over the configured default until the user
// changes the default in settings (handled below).
readonly property string notepadResolvedMode: SessionData.notepadLastMode || SettingsData.notepadDefaultMode
function openNotepadSlideout() {
SessionData.setNotepadLastMode("slideout");
notepadPopout?.hide();
if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.show();
notepadSlideoutForFocusedScreen()?.show();
}
}
@@ -861,6 +880,7 @@ Singleton {
Connections {
target: SettingsData
function onNotepadDefaultModeChanged() {
SessionData.setNotepadLastMode(SettingsData.notepadDefaultMode);
if (SettingsData.notepadDefaultMode === "popout") {
var hadSlideout = false;
for (var i = 0; i < root.notepadSlideouts.length; i++) {
@@ -879,7 +899,7 @@ Singleton {
}
function openNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
if (notepadResolvedMode === "popout") {
openNotepadPopout();
return;
}
@@ -887,22 +907,22 @@ Singleton {
}
function closeNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
if (notepadResolvedMode === "popout") {
notepadPopout?.hide();
return;
}
if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.hide();
notepadSlideoutForFocusedScreen()?.hide();
}
}
function toggleNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
if (notepadResolvedMode === "popout") {
toggleNotepadPopout();
return;
}
if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.toggle();
notepadSlideoutForFocusedScreen()?.toggle();
}
}
@@ -912,6 +932,7 @@ Singleton {
property string _notepadPendingOpenFilePath: ""
function openNotepadPopout() {
SessionData.setNotepadLastMode("popout");
closeNotepadSlideouts();
if (notepadPopout) {
notepadPopout.show();
+34 -5
View File
@@ -14,6 +14,8 @@ Singleton {
property bool hasUwsm: false
property bool isElogind: false
property bool loginctlCommandAvailable: false
property bool systemctlCommandAvailable: false
property bool hibernateSupported: false
property bool inhibitorAvailable: true
property bool idleInhibited: false
@@ -52,6 +54,8 @@ Singleton {
repeat: false
onTriggered: {
detectElogindProcess.running = true;
detectLoginctlProcess.running = true;
detectSystemctlProcess.running = true;
detectHibernateProcess.running = true;
detectPrimeRunProcess.running = true;
if (!SettingsData.loginctlLockIntegration) {
@@ -87,6 +91,26 @@ Singleton {
}
}
Process {
id: detectLoginctlProcess
running: false
command: ["sh", "-c", "command -v loginctl"]
onExited: function (exitCode) {
loginctlCommandAvailable = (exitCode === 0);
}
}
Process {
id: detectSystemctlProcess
running: false
command: ["sh", "-c", "command -v systemctl"]
onExited: function (exitCode) {
systemctlCommandAvailable = (exitCode === 0);
}
}
Process {
id: detectHibernateProcess
running: false
@@ -325,9 +349,14 @@ Singleton {
}
}
function powerManagerCommand(action) {
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
return [useLoginctl ? "loginctl" : "systemctl", action];
}
function suspend() {
if (SettingsData.customPowerActionSuspend.length === 0) {
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]);
Quickshell.execDetached(powerManagerCommand("suspend"));
} else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
}
@@ -338,13 +367,13 @@ Singleton {
if (SettingsData.customPowerActionHibernate.length > 0) {
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
} else {
hibernateProcess.command = [isElogind ? "loginctl" : "systemctl", "hibernate"];
hibernateProcess.command = powerManagerCommand("hibernate");
}
hibernateProcess.running = true;
}
function suspendThenHibernate() {
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend-then-hibernate"]);
Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate"));
}
function suspendWithBehavior(behavior) {
@@ -359,7 +388,7 @@ Singleton {
function reboot() {
if (SettingsData.customPowerActionReboot.length === 0) {
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]);
Quickshell.execDetached(powerManagerCommand("reboot"));
} else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]);
}
@@ -367,7 +396,7 @@ Singleton {
function poweroff() {
if (SettingsData.customPowerActionPowerOff.length === 0) {
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"]);
Quickshell.execDetached(powerManagerCommand("poweroff"));
} else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]);
}

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