mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-03 12:08:31 -04:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdfd565b72 | |||
| f3b698590c | |||
| 7826d42ce4 | |||
| 6cc30c289b | |||
| 43cc8e69d1 | |||
| a62ae336e0 | |||
| e94af2a7aa | |||
| bb9e69e0c1 | |||
| 1cc9218ff6 | |||
| 56712f46fa | |||
| 93168ee073 | |||
| 1724aedd49 | |||
| d799175c07 | |||
| ee6f7b4798 | |||
| 0511cd19df | |||
| 71b1901ab0 | |||
| 32e2d96e55 | |||
| 647766b9fa | |||
| 0cd8974110 | |||
| 0c4c5fc146 | |||
| b447e16374 | |||
| 0bb8353a33 | |||
| 19a7dcf17d | |||
| 8a1acb63c9 | |||
| 6f298d3f52 | |||
| 3e481a566b |
@@ -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
@@ -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.
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
**dankinstall**
|
**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
|
## System Integration
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
|
|||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
|
||||||
|
|
||||||
**Arch Linux**
|
**Arch Linux**
|
||||||
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
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**
|
**Gentoo**
|
||||||
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
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.
|
See installer output for distribution-specific details during installation.
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ var (
|
|||||||
replaceConfigs []string
|
replaceConfigs []string
|
||||||
replaceConfigsAll bool
|
replaceConfigsAll bool
|
||||||
yes bool
|
yes bool
|
||||||
|
danksearch bool
|
||||||
|
dankcalendar bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
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().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().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
||||||
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
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() {
|
func main() {
|
||||||
@@ -74,6 +78,8 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
|||||||
"replace-configs",
|
"replace-configs",
|
||||||
"replace-configs-all",
|
"replace-configs-all",
|
||||||
"yes",
|
"yes",
|
||||||
|
"danksearch",
|
||||||
|
"dankcalendar",
|
||||||
}
|
}
|
||||||
var set []string
|
var set []string
|
||||||
for _, name := range headlessOnly {
|
for _, name := range headlessOnly {
|
||||||
@@ -109,6 +115,8 @@ func runHeadless() error {
|
|||||||
ReplaceConfigs: replaceConfigs,
|
ReplaceConfigs: replaceConfigs,
|
||||||
ReplaceConfigsAll: replaceConfigsAll,
|
ReplaceConfigsAll: replaceConfigsAll,
|
||||||
Yes: yes,
|
Yes: yes,
|
||||||
|
DankSearch: danksearch,
|
||||||
|
DankCalendar: dankcalendar,
|
||||||
}
|
}
|
||||||
|
|
||||||
runner := headless.NewRunner(cfg)
|
runner := headless.NewRunner(cfg)
|
||||||
|
|||||||
@@ -774,6 +774,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
trashCmd,
|
trashCmd,
|
||||||
systemCmd,
|
systemCmd,
|
||||||
switchUserCmd,
|
switchUserCmd,
|
||||||
trayWatcherCmd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -886,6 +886,7 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
{"cava", "cava", "Audio visualizer", true},
|
{"cava", "cava", "Audio visualizer", true},
|
||||||
{"khal", "khal", "Calendar events", false},
|
{"khal", "khal", "Calendar events", false},
|
||||||
{"danksearch", "dsearch", "File search", false},
|
{"danksearch", "dsearch", "File search", false},
|
||||||
|
{"dankcalendar", "dcal", "Calendar app", false},
|
||||||
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1534,6 +1534,8 @@ func packageInstallHint() string {
|
|||||||
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
|
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:
|
default:
|
||||||
return "Run 'dms greeter install' to install greeter"
|
return "Run 'dms greeter install' to install greeter"
|
||||||
}
|
}
|
||||||
@@ -1572,7 +1574,8 @@ func isPackageOnlyGreeterDistro() bool {
|
|||||||
config.Family == distros.FamilySUSE ||
|
config.Family == distros.FamilySUSE ||
|
||||||
config.Family == distros.FamilyUbuntu ||
|
config.Family == distros.FamilyUbuntu ||
|
||||||
config.Family == distros.FamilyFedora ||
|
config.Family == distros.FamilyFedora ||
|
||||||
config.Family == distros.FamilyArch
|
config.Family == distros.FamilyArch ||
|
||||||
|
config.Family == distros.FamilyVoid
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptCompositorChoice(compositors []string) (string, error) {
|
func promptCompositorChoice(compositors []string) (string, error) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"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/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
@@ -298,6 +299,9 @@ func runSetup() error {
|
|||||||
if wmSelected {
|
if wmSelected {
|
||||||
if wm == deps.WindowManagerMango {
|
if wm == deps.WindowManagerMango {
|
||||||
useSystemd = false
|
useSystemd = false
|
||||||
|
} else if isVoidSetup() {
|
||||||
|
useSystemd = false
|
||||||
|
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
|
||||||
} else {
|
} else {
|
||||||
useSystemd = promptSystemd()
|
useSystemd = promptSystemd()
|
||||||
}
|
}
|
||||||
@@ -372,6 +376,15 @@ func runSetup() error {
|
|||||||
return nil
|
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.
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
func ensureInputGroup() {
|
func ensureInputGroup() {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -61,6 +61,10 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
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) {
|
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
@@ -880,9 +884,8 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
|
|||||||
|
|
||||||
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
|
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" "run"`
|
||||||
spawnDms := "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\""
|
if !strings.Contains(config, spawnDms) {
|
||||||
if !strings.Contains(config, `spawn-at-startup "dms" "run"`) {
|
|
||||||
// Insert spawn-at-startup for dms after the environment block
|
// Insert spawn-at-startup for dms after the environment block
|
||||||
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
||||||
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
||||||
|
|||||||
@@ -520,21 +520,9 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandLuaConfig, "input =")
|
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) {
|
func TestMangoConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, MangoConfig, "exec-once=dms run")
|
assert.Contains(t, MangoConfig, "exec-once=dms run")
|
||||||
assert.NotContains(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, MangoConfig, "source=./dms/binds.conf")
|
||||||
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
|
||||||
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
|
|||||||
copy-on-select = false
|
copy-on-select = false
|
||||||
confirm-close-surface = false
|
confirm-close-surface = false
|
||||||
|
|
||||||
# Disable annoying copied to clipboard
|
# Disable in-app Ghostty toast notifications
|
||||||
app-notifications = no-clipboard-copy,no-config-reload
|
app-notifications = false
|
||||||
|
|
||||||
# Key bindings for common actions
|
# Key bindings for common actions
|
||||||
#keybind = ctrl+c=copy_to_clipboard
|
#keybind = ctrl+c=copy_to_clipboard
|
||||||
|
|||||||
@@ -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 + 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 + 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 + 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 + 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 + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
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
|
# Notepad
|
||||||
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||||
# Browse Wallpapers
|
# Browse Wallpapers
|
||||||
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
|
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
|
||||||
# Power Menu
|
# Power Menu
|
||||||
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||||
# Cycle Display Profile
|
# Cycle Display Profile
|
||||||
|
|||||||
@@ -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
|
# 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
|
# 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
|
# spawn a new shell on every reload.
|
||||||
# the SNI name before dms and the tray apps come up.
|
|
||||||
exec-once=dms tray-watcher
|
|
||||||
exec-once=dms run
|
exec-once=dms run
|
||||||
|
|
||||||
source=./dms/colors.conf
|
source=./dms/colors.conf
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ binds {
|
|||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
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+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"; }
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
|
|||||||
@@ -20,3 +20,10 @@ window-rule {
|
|||||||
tiled-state true
|
tiled-state true
|
||||||
draw-border-with-background false
|
draw-border-with-background false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layer-rule {
|
||||||
|
exclude namespace="^dms:bar$"
|
||||||
|
background-effect {
|
||||||
|
xray false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -265,9 +265,9 @@ recent-windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Include dms files
|
// Include dms files
|
||||||
include "dms/colors.kdl"
|
include optional=true "dms/colors.kdl"
|
||||||
include "dms/layout.kdl"
|
include optional=true "dms/layout.kdl"
|
||||||
include "dms/alttab.kdl"
|
include optional=true "dms/alttab.kdl"
|
||||||
include "dms/binds.kdl"
|
include optional=true "dms/binds.kdl"
|
||||||
include "dms/outputs.kdl"
|
include optional=true "dms/outputs.kdl"
|
||||||
include "dms/cursor.kdl"
|
include optional=true "dms/cursor.kdl"
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string {
|
|||||||
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
|
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
|
||||||
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
|
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
|
||||||
`hl.on("hyprland.start", function()` + "\n" +
|
`hl.on("hyprland.start", function()` + "\n" +
|
||||||
` hl.exec_cmd("dms tray-watcher")` + "\n" +
|
|
||||||
` hl.exec_cmd("dms run")` + "\n" +
|
` hl.exec_cmd("dms run")` + "\n" +
|
||||||
`end)` + "\n" +
|
`end)` + "\n" +
|
||||||
hyprlandStartupEnd
|
hyprlandStartupEnd
|
||||||
|
|||||||
@@ -119,10 +119,30 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
|
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
|
dependencies = append(dependencies, a.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, a.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
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 {
|
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||||
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
|
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 {
|
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 {
|
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)"]),
|
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
"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"]),
|
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
@@ -199,6 +225,8 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"danksearch": a.getDanksearchMapping(variants["danksearch"]),
|
||||||
|
"dankcalendar": a.getDankCalendarMapping(variants["dankcalendar"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
@@ -253,6 +281,27 @@ func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) Packag
|
|||||||
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
|
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 {
|
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if forceDMSGit || variant == deps.VariantGit {
|
if forceDMSGit || variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ func (b *BaseDistribution) detectDgop() deps.Dependency {
|
|||||||
return b.detectCommand("dgop", "Desktop portal management tool")
|
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 {
|
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
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 {
|
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 {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
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")
|
b.log("Warning: failed to add dms as a want for niri.service")
|
||||||
}
|
}
|
||||||
case deps.WindowManagerHyprland:
|
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")
|
b.log("Warning: failed to add dms as a want for hyprland-session.target")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, d.detectMatugen())
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
dependencies = append(dependencies, d.detectDgop())
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
|
dependencies = append(dependencies, d.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, d.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,8 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"ghostty": {Name: "ghostty", 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 {
|
if wm == deps.WindowManagerNiri {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -104,6 +104,8 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
|
dependencies = append(dependencies, f.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, f.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -138,6 +140,8 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"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 {
|
switch wm {
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
|
dependencies = append(dependencies, g.detectDanksearch())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -171,6 +172,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
"dms (DankMaterialShell)": g.getDmsMapping(),
|
"dms (DankMaterialShell)": g.getDmsMapping(),
|
||||||
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
|
"danksearch": {Name: "gui-apps/danksearch", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
FamilyDebian DistroFamily = "debian"
|
FamilyDebian DistroFamily = "debian"
|
||||||
FamilyNix DistroFamily = "nix"
|
FamilyNix DistroFamily = "nix"
|
||||||
FamilyGentoo DistroFamily = "gentoo"
|
FamilyGentoo DistroFamily = "gentoo"
|
||||||
|
FamilyVoid DistroFamily = "void"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PackageManagerType defines the package manager a distro uses
|
// PackageManagerType defines the package manager a distro uses
|
||||||
@@ -29,6 +30,7 @@ const (
|
|||||||
PackageManagerZypper PackageManagerType = "zypper"
|
PackageManagerZypper PackageManagerType = "zypper"
|
||||||
PackageManagerNix PackageManagerType = "nix"
|
PackageManagerNix PackageManagerType = "nix"
|
||||||
PackageManagerPortage PackageManagerType = "portage"
|
PackageManagerPortage PackageManagerType = "portage"
|
||||||
|
PackageManagerXBPS PackageManagerType = "xbps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepositoryType defines the type of repository for a package
|
// RepositoryType defines the type of repository for a package
|
||||||
@@ -42,6 +44,7 @@ const (
|
|||||||
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
||||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||||
|
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
|
||||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
|
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
|
dependencies = append(dependencies, o.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, o.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -129,6 +131,8 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", 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 {
|
switch wm {
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
|
dependencies = append(dependencies, u.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, u.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -124,6 +126,8 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", 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 {
|
switch wm {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
|||||||
|
|
||||||
const GreeterCacheDir = "/var/cache/dms-greeter"
|
const GreeterCacheDir = "/var/cache/dms-greeter"
|
||||||
|
|
||||||
|
const (
|
||||||
|
runitSvDir = "/etc/sv"
|
||||||
|
runitServiceDir = "/var/service"
|
||||||
|
)
|
||||||
|
|
||||||
func DetectDMSPath() (string, error) {
|
func DetectDMSPath() (string, error) {
|
||||||
return config.LocateDMSConfig()
|
return config.LocateDMSConfig()
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,96 @@ func IsNixOS() bool {
|
|||||||
return err == nil
|
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 {
|
func DetectGreeterGroup() string {
|
||||||
data, err := os.ReadFile("/etc/group")
|
data, err := os.ReadFile("/etc/group")
|
||||||
if err != nil {
|
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")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
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:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||||
default:
|
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)
|
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")
|
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:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -909,6 +1014,20 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
|||||||
return true
|
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
|
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
if IsGreeterPackaged() {
|
if IsGreeterPackaged() {
|
||||||
@@ -2275,6 +2394,19 @@ func checkSystemdEnabled(service string) (string, error) {
|
|||||||
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
|
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
|
||||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
for _, dm := range conflictingDMs {
|
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)
|
state, err := checkSystemdEnabled(dm)
|
||||||
if err != nil || state == "" || state == "not-found" {
|
if err != nil || state == "" || state == "not-found" {
|
||||||
continue
|
continue
|
||||||
@@ -2294,6 +2426,19 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
|||||||
|
|
||||||
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
|
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
|
||||||
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
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")
|
state, err := checkSystemdEnabled("greetd")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check greetd state: %w", err)
|
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 {
|
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")
|
cmd := exec.Command("systemctl", "get-default")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ type Config struct {
|
|||||||
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
|
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
|
||||||
ReplaceConfigsAll bool // deploy/replace all configurations
|
ReplaceConfigsAll bool // deploy/replace all configurations
|
||||||
Yes bool
|
Yes bool
|
||||||
|
DankSearch bool // install danksearch and enable its user service
|
||||||
|
DankCalendar bool // install dankcalendar
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runner orchestrates unattended (headless) installation.
|
// Runner orchestrates unattended (headless) installation.
|
||||||
@@ -214,6 +216,11 @@ func (r *Runner) Run() error {
|
|||||||
return fmt.Errorf("package installation failed: %w", err)
|
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)
|
// 9. Greeter setup (if dms-greeter was included)
|
||||||
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
|
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
|
||||||
compositorName := "niri"
|
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
|
// 10. Deploy configurations
|
||||||
fmt.Fprintln(os.Stdout, "Deploying configurations...")
|
fmt.Fprintln(os.Stdout, "Deploying configurations...")
|
||||||
r.log("Starting configuration deployment")
|
r.log("Starting configuration deployment")
|
||||||
|
|
||||||
deployer := config.NewConfigDeployer(r.logChan)
|
deployer := config.NewConfigDeployer(r.logChan)
|
||||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
|
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
wm,
|
wm,
|
||||||
terminal,
|
terminal,
|
||||||
dependencies,
|
dependencies,
|
||||||
replaceConfigs,
|
replaceConfigs,
|
||||||
reinstallItems,
|
reinstallItems,
|
||||||
|
useSystemd,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration deployment failed: %w", err)
|
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
|
// buildDisabledItems computes the set of dependencies that should be skipped
|
||||||
// during installation, applying the --include-deps and --exclude-deps filters.
|
// during installation. Optional components are opt-in (disabled by default),
|
||||||
// dms-greeter is disabled by default (opt-in), matching TUI behavior.
|
// then re-enabled by the dedicated flags and --include-deps.
|
||||||
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
|
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
|
||||||
disabledItems := make(map[string]bool)
|
disabledItems := make(map[string]bool)
|
||||||
|
|
||||||
// dms-greeter is opt-in (disabled by default), matching TUI behavior
|
|
||||||
for i := range dependencies {
|
for i := range dependencies {
|
||||||
if dependencies[i].Name == "dms-greeter" {
|
if !dependencies[i].Required {
|
||||||
disabledItems["dms-greeter"] = true
|
disabledItems[dependencies[i].Name] = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
// Process --include-deps (enable items that are disabled by default)
|
||||||
for _, name := range r.cfg.IncludeDeps {
|
for _, name := range r.cfg.IncludeDeps {
|
||||||
name = strings.TrimSpace(name)
|
name = strings.TrimSpace(name)
|
||||||
|
|||||||
@@ -342,17 +342,21 @@ func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
|
|||||||
|
|
||||||
func TestBuildDisabledItems(t *testing.T) {
|
func TestBuildDisabledItems(t *testing.T) {
|
||||||
dependencies := []deps.Dependency{
|
dependencies := []deps.Dependency{
|
||||||
{Name: "niri", Status: deps.StatusInstalled},
|
{Name: "niri", Status: deps.StatusInstalled, Required: true},
|
||||||
{Name: "ghostty", Status: deps.StatusMissing},
|
{Name: "ghostty", Status: deps.StatusMissing, Required: true},
|
||||||
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
|
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled, Required: true},
|
||||||
{Name: "dms-greeter", Status: deps.StatusMissing},
|
{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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
includeDeps []string
|
includeDeps []string
|
||||||
excludeDeps []string
|
excludeDeps []string
|
||||||
|
dankSearch bool
|
||||||
|
dankCalendar bool
|
||||||
deps []deps.Dependency // nil means use the shared fixture
|
deps []deps.Dependency // nil means use the shared fixture
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errContains string // substring expected in error message
|
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)
|
wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no flags set, dms-greeter disabled by default",
|
name: "no flags set, optional deps disabled by default",
|
||||||
wantDisabled: []string{"dms-greeter"},
|
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar"},
|
||||||
wantEnabled: []string{"niri", "ghostty", "waybar"},
|
wantEnabled: []string{"niri", "ghostty", "waybar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "include dms-greeter enables it",
|
name: "include dms-greeter enables it",
|
||||||
includeDeps: []string{"dms-greeter"},
|
includeDeps: []string{"dms-greeter"},
|
||||||
wantEnabled: []string{"dms-greeter"},
|
wantEnabled: []string{"dms-greeter"},
|
||||||
|
wantDisabled: []string{"danksearch", "dankcalendar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "exclude a regular dep",
|
name: "exclude a regular dep",
|
||||||
excludeDeps: []string{"waybar"},
|
excludeDeps: []string{"waybar"},
|
||||||
wantDisabled: []string{"dms-greeter", "waybar"},
|
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar", "waybar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "include unknown dep returns error",
|
name: "include unknown dep returns error",
|
||||||
@@ -402,14 +407,41 @@ func TestBuildDisabledItems(t *testing.T) {
|
|||||||
name: "whitespace entries are skipped",
|
name: "whitespace entries are skipped",
|
||||||
includeDeps: []string{" ", "dms-greeter"},
|
includeDeps: []string{" ", "dms-greeter"},
|
||||||
wantEnabled: []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{
|
deps: []deps.Dependency{
|
||||||
{Name: "niri", Status: deps.StatusInstalled},
|
{Name: "niri", Status: deps.StatusInstalled, Required: true},
|
||||||
},
|
},
|
||||||
wantEnabled: []string{"niri"},
|
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 {
|
for _, tt := range tests {
|
||||||
@@ -417,6 +449,8 @@ func TestBuildDisabledItems(t *testing.T) {
|
|||||||
r := NewRunner(Config{
|
r := NewRunner(Config{
|
||||||
IncludeDeps: tt.includeDeps,
|
IncludeDeps: tt.includeDeps,
|
||||||
ExcludeDeps: tt.excludeDeps,
|
ExcludeDeps: tt.excludeDeps,
|
||||||
|
DankSearch: tt.dankSearch,
|
||||||
|
DankCalendar: tt.dankCalendar,
|
||||||
})
|
})
|
||||||
d := tt.deps
|
d := tt.deps
|
||||||
if d == nil {
|
if d == nil {
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ func GetQtLoggingRules() string {
|
|||||||
level = "info"
|
level = "info"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
|
||||||
|
// probes); suppressed except at debug level
|
||||||
var rules []string
|
var rules []string
|
||||||
switch strings.ToLower(level) {
|
switch strings.ToLower(level) {
|
||||||
case "fatal":
|
case "fatal":
|
||||||
@@ -75,13 +77,13 @@ func GetQtLoggingRules() string {
|
|||||||
case "error":
|
case "error":
|
||||||
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
|
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
|
||||||
case "warn", "warning":
|
case "warn", "warning":
|
||||||
rules = []string{"*.debug=false", "*.info=false"}
|
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
|
||||||
case "info":
|
case "info":
|
||||||
rules = []string{"*.debug=false"}
|
rules = []string{"*.debug=false", "scene.warning=false"}
|
||||||
case "debug":
|
case "debug":
|
||||||
return ""
|
return ""
|
||||||
default:
|
default:
|
||||||
rules = []string{"*.debug=false"}
|
rules = []string{"*.debug=false", "scene.warning=false"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(rules, ";")
|
return strings.Join(rules, ";")
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ func (m Model) deployConfigurations() tea.Cmd {
|
|||||||
|
|
||||||
deployer := config.NewConfigDeployer(m.logChan)
|
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{
|
return configDeploymentResult{
|
||||||
results: results,
|
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 {
|
func (m Model) viewConfigConfirmation() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
@@ -195,12 +219,50 @@ func (m Model) viewConfigConfirmation() string {
|
|||||||
b.WriteString(backup)
|
b.WriteString(backup)
|
||||||
b.WriteString("\n\n")
|
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")
|
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
|
||||||
b.WriteString(help)
|
b.WriteString(help)
|
||||||
|
|
||||||
return b.String()
|
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) {
|
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if result, ok := msg.(configCheckResult); ok {
|
if result, ok := msg.(configCheckResult); ok {
|
||||||
if result.error != nil {
|
if result.error != nil {
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ func (m Model) viewDetectingDeps() string {
|
|||||||
return b.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 {
|
func (m Model) viewDependencyReview() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
@@ -39,7 +54,15 @@ func (m Model) viewDependencyReview() string {
|
|||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
if len(m.dependencies) > 0 {
|
if len(m.dependencies) > 0 {
|
||||||
|
optionalHeaderShown := false
|
||||||
for i, dep := range m.dependencies {
|
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 status string
|
||||||
var reinstallMarker string
|
var reinstallMarker string
|
||||||
var variantMarker string
|
var variantMarker string
|
||||||
@@ -82,8 +105,13 @@ func (m Model) viewDependencyReview() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
note := ""
|
note := ""
|
||||||
if dep.Name == "dms-greeter" {
|
switch dep.Name {
|
||||||
|
case "dms-greeter":
|
||||||
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
|
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
|
var line string
|
||||||
@@ -120,13 +148,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.err = depsMsg.err
|
m.err = depsMsg.err
|
||||||
m.state = StateError
|
m.state = StateError
|
||||||
} else {
|
} else {
|
||||||
m.dependencies = depsMsg.deps
|
m.dependencies = partitionOptionalLast(depsMsg.deps)
|
||||||
// dms-greeter is opt-in skipped by default
|
// Optional components are opt-in, skipped by default
|
||||||
for _, dep := range depsMsg.deps {
|
for _, dep := range m.dependencies {
|
||||||
if dep.Name == "dms-greeter" {
|
if dep.Required {
|
||||||
m.disabledItems["dms-greeter"] = true
|
continue
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
m.disabledItems[dep.Name] = true
|
||||||
}
|
}
|
||||||
m.state = StateDependencyReview
|
m.state = StateDependencyReview
|
||||||
}
|
}
|
||||||
@@ -231,14 +259,7 @@ func (m Model) installPackages() tea.Cmd {
|
|||||||
for msg := range installerProgressChan {
|
for msg := range installerProgressChan {
|
||||||
// Run optional greeter setup
|
// Run optional greeter setup
|
||||||
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
||||||
greeterSelected := false
|
if m.optionalDepSelected("dms-greeter") {
|
||||||
for _, dep := range m.dependencies {
|
|
||||||
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
|
|
||||||
greeterSelected = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if greeterSelected {
|
|
||||||
compositorName := "niri"
|
compositorName := "niri"
|
||||||
switch m.selectedWindowManager() {
|
switch m.selectedWindowManager() {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
@@ -265,6 +286,28 @@ func (m Model) installPackages() tea.Cmd {
|
|||||||
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
|
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{
|
tuiMsg := packageInstallProgressMsg{
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
|||||||
return "dms-shell-git"
|
return "dms-shell-git"
|
||||||
}
|
}
|
||||||
return "dms-shell"
|
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 {
|
if isGit {
|
||||||
return "dms-git"
|
return "dms-git"
|
||||||
}
|
}
|
||||||
@@ -168,6 +168,8 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
|
|||||||
return "sudo apt remove " + pkg
|
return "sudo apt remove " + pkg
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
return "sudo zypper remove " + pkg
|
return "sudo zypper remove " + pkg
|
||||||
|
case distros.FamilyVoid:
|
||||||
|
return "sudo xbps-remove -R " + pkg
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -201,8 +203,18 @@ func (m Model) viewInstallComplete() string {
|
|||||||
|
|
||||||
wm := m.selectedWindowManager()
|
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\""
|
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
||||||
|
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 {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
||||||
@@ -211,6 +223,7 @@ func (m Model) viewInstallComplete() string {
|
|||||||
case deps.WindowManagerMango:
|
case deps.WindowManagerMango:
|
||||||
loginHint = "If you do not have a greeter, login with \"mango\""
|
loginHint = "If you do not have a greeter, login with \"mango\""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + loginHint)
|
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + loginHint)
|
||||||
@@ -222,7 +235,17 @@ func (m Model) viewInstallComplete() string {
|
|||||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
||||||
|
|
||||||
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
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(" 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")
|
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ override_dh_auto_install:
|
|||||||
if [ -d quickshell ]; then \
|
if [ -d quickshell ]; then \
|
||||||
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
|
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.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/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.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; \
|
install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ override_dh_auto_install:
|
|||||||
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
||||||
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
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.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/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.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; \
|
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ core/bin/${DMS_BINARY} completion fish > %{buildroot}%{_datadir}/fish/vendor_com
|
|||||||
|
|
||||||
# Install systemd user service
|
# Install systemd user service
|
||||||
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
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/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.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
|
%doc quickshell/README.md
|
||||||
%{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_userunitdir}/dms-tray-watcher.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
|
|||||||
@@ -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 || :
|
%{_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.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/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
|
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
|
%doc README.md CONTRIBUTING.md
|
||||||
%{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_userunitdir}/dms-tray-watcher.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
|
|||||||
@@ -101,24 +101,6 @@ in
|
|||||||
Install.WantedBy = [ cfg.systemd.target ];
|
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 != { }) {
|
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
|
||||||
source = jsonFormat.generate "session.json" cfg.session;
|
source = jsonFormat.generate "session.json" cfg.session;
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ in {
|
|||||||
text = lib.pipe cfg'.filesToInclude [
|
text = lib.pipe cfg'.filesToInclude [
|
||||||
(map (filename: "dms/${filename}"))
|
(map (filename: "dms/${filename}"))
|
||||||
withOriginalConfig
|
withOriginalConfig
|
||||||
(map (filename: "include \"${filename}.kdl\""))
|
(map (filename: "include optional=true \"${filename}.kdl\""))
|
||||||
(files: files ++ fixes)
|
(files: files ++ fixes)
|
||||||
(builtins.concatStringsSep "\n")
|
(builtins.concatStringsSep "\n")
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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.systemPackages = [ cfg.quickshell.package ] ++ common.packages;
|
||||||
|
|
||||||
environment.etc = lib.mapAttrs' (name: value: {
|
environment.etc = lib.mapAttrs' (name: value: {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
let
|
let
|
||||||
homeManagerNixosModule =
|
homeManagerNixosModule =
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
|
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
|
||||||
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
|
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
|
||||||
})
|
})
|
||||||
+ "/nixos";
|
+ "/nixos";
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
let
|
let
|
||||||
homeManagerNixosModule =
|
homeManagerNixosModule =
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
|
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
|
||||||
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
|
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
|
||||||
})
|
})
|
||||||
+ "/nixos";
|
+ "/nixos";
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ pkgs.testers.runNixOSTest {
|
|||||||
machine.wait_for_unit("multi-user.target")
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
|
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 optional=true \\\"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 \\\"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 \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
|
||||||
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
|
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|||||||
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
./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.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/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.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
|
%dir %{_datadir}/quickshell
|
||||||
%{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_userunitdir}/dms-tray-watcher.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|||||||
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
./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.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/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.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
|
%dir %{_datadir}/quickshell
|
||||||
%{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_userunitdir}/dms-tray-watcher.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ override_dh_auto_install:
|
|||||||
# Install systemd user service
|
# Install systemd user service
|
||||||
install -Dm644 dms-git-repo/assets/systemd/dms.service \
|
install -Dm644 dms-git-repo/assets/systemd/dms.service \
|
||||||
debian/dms-git/usr/lib/systemd/user/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 desktop file and icon
|
||||||
install -Dm644 dms-git-repo/assets/dms-open.desktop \
|
install -Dm644 dms-git-repo/assets/dms-open.desktop \
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ override_dh_auto_install:
|
|||||||
# Install systemd user service (before copying everything else)
|
# Install systemd user service (before copying everything else)
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms.service \
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms.service \
|
||||||
debian/dms/usr/lib/systemd/user/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 desktop file and icon
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/dms-open.desktop \
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/dms-open.desktop \
|
||||||
|
|||||||
+23
-2
@@ -52,8 +52,8 @@ checkout at `srcpkgs/<pkg>/template` to build or submit it.
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
|
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
|
||||||
and `matugen` (which drives the Material You theming). The rest are optional —
|
`matugen` (which drives the Material You theming), `dbus`, and `elogind`.
|
||||||
install whichever features you want:
|
The rest are optional, install whichever features you want:
|
||||||
|
|
||||||
| Package | Enables |
|
| Package | Enables |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -98,6 +98,27 @@ spawn-at-startup "dms" "run"
|
|||||||
|
|
||||||
or Hyprland: `exec-once = 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)
|
## Greeter (optional)
|
||||||
|
|
||||||
Install `dms-greeter`, then let the CLI do the setup:
|
Install `dms-greeter`, then let the CLI do the setup:
|
||||||
|
|||||||
@@ -32,13 +32,12 @@ conflicts="dms"
|
|||||||
provides="dms-${version}_${revision}"
|
provides="dms-${version}_${revision}"
|
||||||
|
|
||||||
# Optional feature deps are listed in distro/void/README.md.
|
# 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() {
|
post_install() {
|
||||||
# QML shell tree (build_style=go already installed the dms binary)
|
# QML shell tree (build_style=go already installed the dms binary)
|
||||||
vmkdir usr/share/quickshell/dms
|
vmkdir usr/share/quickshell/dms
|
||||||
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||||
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
|
||||||
|
|
||||||
# Desktop entry + icon
|
# Desktop entry + icon
|
||||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||||
|
|||||||
@@ -22,13 +22,12 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$
|
|||||||
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
||||||
|
|
||||||
# Optional feature deps are listed in distro/void/README.md.
|
# 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() {
|
post_install() {
|
||||||
# QML shell tree (build_style=go already installed the dms binary)
|
# QML shell tree (build_style=go already installed the dms binary)
|
||||||
vmkdir usr/share/quickshell/dms
|
vmkdir usr/share/quickshell/dms
|
||||||
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||||
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
|
||||||
|
|
||||||
# Desktop entry + icon
|
# Desktop entry + icon
|
||||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||||
|
|||||||
Generated
+3
-3
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778443072,
|
"lastModified": 1783224372,
|
||||||
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -120,12 +120,18 @@ Singleton {
|
|||||||
|
|
||||||
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
|
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
|
||||||
var newPositions = Object.assign({}, monitorScrollPositions);
|
var newPositions = Object.assign({}, monitorScrollPositions);
|
||||||
newPositions[screenName] = { scrollX: scrollX, scrollY: scrollY };
|
newPositions[screenName] = {
|
||||||
|
scrollX: scrollX,
|
||||||
|
scrollY: scrollY
|
||||||
|
};
|
||||||
monitorScrollPositions = newPositions;
|
monitorScrollPositions = newPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMonitorScrollPosition(screenName) {
|
function getMonitorScrollPosition(screenName) {
|
||||||
return monitorScrollPositions[screenName] || { scrollX: 50, scrollY: 50 };
|
return monitorScrollPositions[screenName] || {
|
||||||
|
scrollX: 50,
|
||||||
|
scrollY: 50
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMonitorScrollPosition(screenName) {
|
function clearMonitorScrollPosition(screenName) {
|
||||||
@@ -209,6 +215,8 @@ Singleton {
|
|||||||
property string locale: ""
|
property string locale: ""
|
||||||
property string timeLocale: ""
|
property string timeLocale: ""
|
||||||
|
|
||||||
|
property string notepadLastMode: ""
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
property string launcherLastFileSearchType: "all"
|
property string launcherLastFileSearchType: "all"
|
||||||
property string launcherLastQuery: ""
|
property string launcherLastQuery: ""
|
||||||
@@ -1216,6 +1224,13 @@ Singleton {
|
|||||||
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setNotepadLastMode(mode) {
|
||||||
|
if (notepadLastMode === mode)
|
||||||
|
return;
|
||||||
|
notepadLastMode = mode;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function setLauncherLastMode(mode) {
|
function setLauncherLastMode(mode) {
|
||||||
launcherLastMode = mode;
|
launcherLastMode = mode;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ var SPEC = {
|
|||||||
locale: { def: "", onChange: "updateLocale" },
|
locale: { def: "", onChange: "updateLocale" },
|
||||||
timeLocale: { def: "" },
|
timeLocale: { def: "" },
|
||||||
|
|
||||||
|
notepadLastMode: { def: "" },
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
launcherLastFileSearchType: { def: "all" },
|
launcherLastFileSearchType: { def: "all" },
|
||||||
launcherLastQuery: { def: "" },
|
launcherLastQuery: { def: "" },
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open(): string {
|
function open(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.openNotepadPopout();
|
PopoutService.openNotepadPopout();
|
||||||
return "NOTEPAD_OPEN_SUCCESS";
|
return "NOTEPAD_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -388,7 +388,7 @@ Item {
|
|||||||
function openFile(path: string): string {
|
function openFile(path: string): string {
|
||||||
if (!path)
|
if (!path)
|
||||||
return open();
|
return open();
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.openNotepadPopoutWithFile(path);
|
PopoutService.openNotepadPopoutWithFile(path);
|
||||||
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.notepadPopout?.hide();
|
PopoutService.notepadPopout?.hide();
|
||||||
return "NOTEPAD_CLOSE_SUCCESS";
|
return "NOTEPAD_CLOSE_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -415,7 +415,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (PopoutService.notepadResolvedMode === "popout") {
|
||||||
PopoutService.toggleNotepadPopout();
|
PopoutService.toggleNotepadPopout();
|
||||||
return "NOTEPAD_TOGGLE_SUCCESS";
|
return "NOTEPAD_TOGGLE_SUCCESS";
|
||||||
}
|
}
|
||||||
@@ -712,12 +712,13 @@ Item {
|
|||||||
target: "hypr"
|
target: "hypr"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! TODO - remove for v1.6
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function wallpaper(): string {
|
function wallpaper(): string {
|
||||||
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerWallpaperBrowser();
|
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";
|
return "ERROR: Failed to toggle wallpaper browser";
|
||||||
}
|
}
|
||||||
@@ -1792,8 +1793,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open(): string {
|
function open(): string {
|
||||||
if (!CompositorService.isNiri)
|
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||||
return "WINDOW_RULES_NIRI_ONLY";
|
return "WINDOW_RULES_UNSUPPORTED_COMPOSITOR";
|
||||||
root.windowRuleModalLoader.active = true;
|
root.windowRuleModalLoader.active = true;
|
||||||
if (root.windowRuleModalLoader.item) {
|
if (root.windowRuleModalLoader.item) {
|
||||||
root.windowRuleModalLoader.item.show(getFocusedWindow());
|
root.windowRuleModalLoader.item.show(getFocusedWindow());
|
||||||
@@ -1811,8 +1812,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
if (!CompositorService.isNiri)
|
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||||
return "WINDOW_RULES_NIRI_ONLY";
|
return "WINDOW_RULES_UNSUPPORTED_COMPOSITOR";
|
||||||
root.windowRuleModalLoader.active = true;
|
root.windowRuleModalLoader.active = true;
|
||||||
if (root.windowRuleModalLoader.item) {
|
if (root.windowRuleModalLoader.item) {
|
||||||
if (root.windowRuleModalLoader.item.visible) {
|
if (root.windowRuleModalLoader.item.visible) {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Column {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: codenameText
|
id: codenameText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "Saffron Bloom"
|
text: "The Wolverine"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
@@ -74,7 +74,7 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "New launcher, enhanced plugin system, KDE Connect, & more"
|
text: "Frame Mode, DankCalendar, Spotlight, & more"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
@@ -108,77 +108,99 @@ Column {
|
|||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "space_dashboard"
|
iconName: "border_outer"
|
||||||
title: "Dank Launcher V2"
|
title: "Frame Mode"
|
||||||
description: "New capabilities & plugins"
|
description: "Connected shell surfaces"
|
||||||
onClicked: PopoutService.openDankLauncherV2()
|
onClicked: PopoutService.openSettingsWithTab("frame")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "smartphone"
|
iconName: "calendar_month"
|
||||||
title: "Phone Connect"
|
title: "DankCalendar"
|
||||||
description: "KDE Connect & Valent"
|
description: "Native calendar & events"
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
|
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dankcalendar")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "monitor_heart"
|
iconName: "search"
|
||||||
title: "System Monitor"
|
title: "Spotlight"
|
||||||
description: "Redesigned process list"
|
description: "Lightweight launcher"
|
||||||
onClicked: PopoutService.showProcessListModal()
|
onClicked: PopoutService.openSpotlightBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "window"
|
iconName: "window"
|
||||||
title: "Window Rules"
|
title: "Window Rules"
|
||||||
description: "niri window rule manager"
|
description: "Rules for many compositors"
|
||||||
visible: CompositorService.isNiri
|
|
||||||
onClicked: PopoutService.openSettingsWithTab("window_rules")
|
onClicked: PopoutService.openSettingsWithTab("window_rules")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "notifications_active"
|
iconName: "display_settings"
|
||||||
title: "Enhanced Notifications"
|
title: "Display Profiles"
|
||||||
description: "Configurable rules & styling"
|
description: "Auto-switch monitor layouts"
|
||||||
visible: !CompositorService.isNiri
|
onClicked: PopoutService.openSettingsWithTab("display_config")
|
||||||
onClicked: PopoutService.openSettingsWithTab("notifications")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "dock_to_bottom"
|
iconName: "dvr"
|
||||||
title: "Dock Enhancements"
|
title: "Multiplexer Launcher"
|
||||||
description: "Bar dock widget & more"
|
description: "Attach to tmux sessions"
|
||||||
onClicked: PopoutService.openSettingsWithTab("dock")
|
onClicked: PopoutService.openSettingsWithTab("multiplexers")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "volume_up"
|
iconName: "edit_note"
|
||||||
title: "Audio Aliases"
|
title: "Notepad Rewrite"
|
||||||
description: "Custom device names"
|
description: "Popout & tiling support"
|
||||||
onClicked: PopoutService.openSettingsWithTab("audio")
|
onClicked: PopoutService.openNotepad()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "extension"
|
iconName: "gradient"
|
||||||
title: "Enhanced Plugin System"
|
title: "M3 Shadows"
|
||||||
description: "Enables new types of plugins"
|
description: "Reworked elevation system"
|
||||||
onClicked: PopoutService.openSettingsWithTab("plugins")
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
|
||||||
iconName: "light_mode"
|
|
||||||
title: "Auto Light/Dark"
|
|
||||||
description: "Automatic mode switching"
|
|
||||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
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 {
|
ChangelogUpgradeNote {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
|
text: "App ID changed to com.danklinux.dms — update any compositor window rules targeting the old ID"
|
||||||
}
|
|
||||||
|
|
||||||
ChangelogUpgradeNote {
|
|
||||||
width: parent.width
|
|
||||||
text: "Plugin API updated — third-party plugins may need updates"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ FloatingWindow {
|
|||||||
iconName: "open_in_new"
|
iconName: "open_in_new"
|
||||||
backgroundColor: Theme.surfaceContainerHighest
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
|
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-5-release")
|
||||||
}
|
}
|
||||||
|
|
||||||
DankButton {
|
DankButton {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
ClippingRectangle {
|
||||||
id: thumbnail
|
id: thumbnail
|
||||||
readonly property var log: Log.scoped("ClipboardThumbnail")
|
readonly property var log: Log.scoped("ClipboardThumbnail")
|
||||||
|
|
||||||
@@ -15,6 +15,10 @@ Item {
|
|||||||
required property int itemIndex
|
required property int itemIndex
|
||||||
property bool disposed: false
|
property bool disposed: false
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
color: "transparent"
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: thumbnailImage
|
id: thumbnailImage
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ Item {
|
|||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: false
|
cache: false
|
||||||
visible: false
|
visible: entryType === "image" && status === Image.Ready && source != ""
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
sourceSize.width: 128
|
sourceSize.width: 128
|
||||||
sourceSize.height: 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 {
|
DankIcon {
|
||||||
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ Item {
|
|||||||
|
|
||||||
property bool animationsEnabled: true
|
property bool animationsEnabled: true
|
||||||
|
|
||||||
|
function _kickBlurCommit() {
|
||||||
|
if (typeof contentWindow.update === "function")
|
||||||
|
contentWindow.update();
|
||||||
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
@@ -201,6 +206,12 @@ Item {
|
|||||||
}
|
}
|
||||||
})(), dpr)
|
})(), dpr)
|
||||||
|
|
||||||
|
onAlignedXChanged: _kickBlurCommit()
|
||||||
|
onAlignedYChanged: _kickBlurCommit()
|
||||||
|
onAlignedWidthChanged: _kickBlurCommit()
|
||||||
|
onAlignedHeightChanged: _kickBlurCommit()
|
||||||
|
onShouldBeVisibleChanged: _kickBlurCommit()
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: clickCatcher
|
id: clickCatcher
|
||||||
visible: false
|
visible: false
|
||||||
@@ -244,11 +255,13 @@ Item {
|
|||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: contentWindow
|
targetWindow: contentWindow
|
||||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
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.
|
// 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)
|
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||||
blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0
|
blurWidth: root.shouldBeVisible ? modalContainer.width * visibleScale : 0
|
||||||
blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0
|
blurHeight: root.shouldBeVisible ? modalContainer.height * visibleScale : 0
|
||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +351,7 @@ Item {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: morph
|
id: morph
|
||||||
property real openProgress: root.shouldBeVisible ? 1 : 0
|
property real openProgress: root.shouldBeVisible ? 1 : 0
|
||||||
|
onOpenProgressChanged: root._kickBlurCommit()
|
||||||
Behavior on openProgress {
|
Behavior on openProgress {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
DankAnim {
|
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 string cachedMimeType: ""
|
||||||
property var _requestedEntryId: null
|
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 ?? ""))
|
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
||||||
|
|
||||||
radius: Math.max(6, Theme.cornerRadius - 2)
|
radius: Math.max(6, Theme.cornerRadius - 2)
|
||||||
@@ -41,13 +41,18 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reloadPreview() {
|
function reloadPreview() {
|
||||||
|
if (!canLoadImage || typeof entry?.id !== "number") {
|
||||||
|
_requestedEntryId = null;
|
||||||
cachedImageData = "";
|
cachedImageData = "";
|
||||||
cachedMimeType = "";
|
cachedMimeType = "";
|
||||||
if (!canLoadImage || !entry?.id) {
|
|
||||||
_requestedEntryId = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Entry objects are rebuilt per search; same id means same content
|
||||||
|
if (entry.id === _requestedEntryId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cachedImageData = "";
|
||||||
|
cachedMimeType = "";
|
||||||
const entryId = entry.id;
|
const entryId = entry.id;
|
||||||
_requestedEntryId = entryId;
|
_requestedEntryId = entryId;
|
||||||
DMSService.sendRequest("clipboard.getEntry", {
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
@@ -55,17 +60,22 @@ Rectangle {
|
|||||||
}, function (response) {
|
}, function (response) {
|
||||||
if (_requestedEntryId !== entryId)
|
if (_requestedEntryId !== entryId)
|
||||||
return;
|
return;
|
||||||
if (response.error)
|
if (response.error) {
|
||||||
|
_requestedEntryId = null;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
if (!response.result) {
|
if (!response.result) {
|
||||||
|
_requestedEntryId = null;
|
||||||
ClipboardService.refresh();
|
ClipboardService.refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = response.result;
|
const result = response.result;
|
||||||
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
|
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
|
||||||
const data = (result.data ?? "").toString();
|
const data = (result.data ?? "").toString();
|
||||||
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
|
if (data.length === 0 || !resolvedSourceUrl(data, mimeType)) {
|
||||||
|
_requestedEntryId = null;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
cachedMimeType = mimeType;
|
cachedMimeType = mimeType;
|
||||||
cachedImageData = data;
|
cachedImageData = data;
|
||||||
});
|
});
|
||||||
@@ -78,6 +88,8 @@ Rectangle {
|
|||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: false
|
cache: false
|
||||||
smooth: true
|
smooth: true
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,15 +50,15 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!active) {
|
ClipboardService.invalidateLauncherSearchCache();
|
||||||
SessionData.addLauncherHistory(searchQuery);
|
if (active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SessionData.addLauncherHistory(searchQuery);
|
||||||
sections = [];
|
sections = [];
|
||||||
flatModel = [];
|
flatModel = [];
|
||||||
selectedItem = null;
|
selectedItem = null;
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
ClipboardService.invalidateLauncherSearchCache();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchModeChanged: {
|
onSearchModeChanged: {
|
||||||
@@ -110,7 +110,7 @@ Item {
|
|||||||
if (query !== effectiveQuery)
|
if (query !== effectiveQuery)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
searchDebounce.restart();
|
root.requestSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +276,23 @@ Item {
|
|||||||
property string appCategory: ""
|
property string appCategory: ""
|
||||||
property var appCategories: []
|
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) {
|
function getSectionViewMode(sectionId) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
return "list";
|
return "list";
|
||||||
|
var builtInPref = builtInSectionViewPref(sectionId);
|
||||||
|
if (builtInPref?.enforced)
|
||||||
|
return builtInPref.mode;
|
||||||
if (pluginViewPreferences[sectionId]?.enforced)
|
if (pluginViewPreferences[sectionId]?.enforced)
|
||||||
return pluginViewPreferences[sectionId].mode;
|
return pluginViewPreferences[sectionId].mode;
|
||||||
if (sectionViewModes[sectionId])
|
if (sectionViewModes[sectionId])
|
||||||
@@ -302,6 +316,8 @@ Item {
|
|||||||
function setSectionViewMode(sectionId, mode) {
|
function setSectionViewMode(sectionId, mode) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
return;
|
return;
|
||||||
|
if (builtInSectionViewPref(sectionId)?.enforced)
|
||||||
|
return;
|
||||||
if (pluginViewPreferences[sectionId]?.enforced)
|
if (pluginViewPreferences[sectionId]?.enforced)
|
||||||
return;
|
return;
|
||||||
sectionViewModes = Object.assign({}, sectionViewModes, {
|
sectionViewModes = Object.assign({}, sectionViewModes, {
|
||||||
@@ -325,6 +341,8 @@ Item {
|
|||||||
function canChangeSectionViewMode(sectionId) {
|
function canChangeSectionViewMode(sectionId) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
return false;
|
return false;
|
||||||
|
if (builtInSectionViewPref(sectionId)?.enforced)
|
||||||
|
return false;
|
||||||
return !pluginViewPreferences[sectionId]?.enforced;
|
return !pluginViewPreferences[sectionId]?.enforced;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,10 +398,29 @@ Item {
|
|||||||
property bool _pluginPhaseForceFirst: false
|
property bool _pluginPhaseForceFirst: false
|
||||||
property var _phase1Items: []
|
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 {
|
Timer {
|
||||||
id: searchDebounce
|
id: searchDebounce
|
||||||
interval: 60
|
interval: 60
|
||||||
onTriggered: root.performSearch()
|
onTriggered: {
|
||||||
|
if (!root._searchPending)
|
||||||
|
return;
|
||||||
|
root._searchPending = false;
|
||||||
|
root.performSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -409,7 +446,7 @@ Item {
|
|||||||
_phase1Items = [];
|
_phase1Items = [];
|
||||||
pluginPhaseTimer.stop();
|
pluginPhaseTimer.stop();
|
||||||
searchQuery = query;
|
searchQuery = query;
|
||||||
searchDebounce.restart();
|
requestSearch();
|
||||||
|
|
||||||
if (searchMode !== "plugins" && query.startsWith("/")) {
|
if (searchMode !== "plugins" && query.startsWith("/")) {
|
||||||
var prefix = Utils.parseFileSearchPrefix(query);
|
var prefix = Utils.parseFileSearchPrefix(query);
|
||||||
@@ -694,6 +731,7 @@ Item {
|
|||||||
if (triggerMatch.isBuiltIn) {
|
if (triggerMatch.isBuiltIn) {
|
||||||
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
||||||
for (var j = 0; j < builtInItems.length; j++) {
|
for (var j = 0; j < builtInItems.length; j++) {
|
||||||
|
builtInItems[j]._preScored = 1000 - j;
|
||||||
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
|
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -892,23 +930,10 @@ Item {
|
|||||||
searchCompleted();
|
searchCompleted();
|
||||||
return;
|
return;
|
||||||
} else if (!searchQuery) {
|
} else if (!searchQuery) {
|
||||||
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
|
_pluginPhasePending = true;
|
||||||
for (var i = 0; i < emptyTriggerOrdered.length; i++) {
|
_phase1Items = allItems.slice();
|
||||||
var plugin = emptyTriggerOrdered[i];
|
_pluginPhaseForceFirst = shouldResetSelection;
|
||||||
if (plugin.isBuiltIn) {
|
pluginPhaseTimer.restart();
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,11 +964,6 @@ Item {
|
|||||||
flatModel = Scorer.flattenSections(newSections);
|
flatModel = Scorer.flattenSections(newSections);
|
||||||
sections = newSections;
|
sections = newSections;
|
||||||
|
|
||||||
if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
|
|
||||||
AppSearchService.setCachedDefaultSections(sections, flatModel);
|
|
||||||
_saveDiskCache(sections);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedFlatIndex = restoreSelection(flatModel);
|
selectedFlatIndex = restoreSelection(flatModel);
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
|
|
||||||
@@ -953,7 +973,9 @@ Item {
|
|||||||
|
|
||||||
function _performPluginPhase() {
|
function _performPluginPhase() {
|
||||||
_pluginPhasePending = false;
|
_pluginPhasePending = false;
|
||||||
if (!searchQuery || searchQuery.length < 2 || searchMode !== "all")
|
if (searchMode !== "all")
|
||||||
|
return;
|
||||||
|
if (searchQuery && searchQuery.length < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var currentVersion = _searchVersion;
|
var currentVersion = _searchVersion;
|
||||||
@@ -961,6 +983,27 @@ Item {
|
|||||||
var allItems = _phase1Items;
|
var allItems = _phase1Items;
|
||||||
_phase1Items = [];
|
_phase1Items = [];
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var browseItems = getPluginBrowseItems();
|
||||||
|
for (var i = 0; i < browseItems.length; i++)
|
||||||
|
allItems.push(browseItems[i]);
|
||||||
|
} else {
|
||||||
var allPluginsOrdered = getAllVisiblePluginsOrdered();
|
var allPluginsOrdered = getAllVisiblePluginsOrdered();
|
||||||
var maxPerPlugin = 10;
|
var maxPerPlugin = 10;
|
||||||
for (var i = 0; i < allPluginsOrdered.length; i++) {
|
for (var i = 0; i < allPluginsOrdered.length; i++) {
|
||||||
@@ -985,13 +1028,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentVersion !== _searchVersion)
|
if (currentVersion !== _searchVersion)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var dynamicDefs = buildDynamicSectionDefs(allItems);
|
var dynamicDefs = buildDynamicSectionDefs(allItems);
|
||||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
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)
|
if (currentVersion !== _searchVersion)
|
||||||
return;
|
return;
|
||||||
@@ -1005,6 +1050,12 @@ Item {
|
|||||||
_applyHighlights(newSections, searchQuery);
|
_applyHighlights(newSections, searchQuery);
|
||||||
flatModel = Scorer.flattenSections(newSections);
|
flatModel = Scorer.flattenSections(newSections);
|
||||||
sections = newSections;
|
sections = newSections;
|
||||||
|
|
||||||
|
if (!AppSearchService.isCacheValid() && !searchQuery && !pluginFilter) {
|
||||||
|
AppSearchService.setCachedDefaultSections(sections, flatModel);
|
||||||
|
_saveDiskCache(sections);
|
||||||
|
}
|
||||||
|
|
||||||
selectedFlatIndex = restoreSelection(flatModel);
|
selectedFlatIndex = restoreSelection(flatModel);
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
isSearching = false;
|
isSearching = false;
|
||||||
@@ -1198,8 +1249,12 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformBuiltInSearchItem(item, pluginId) {
|
function transformBuiltInSearchItem(item, pluginId) {
|
||||||
if (pluginId === "dms_clipboard_search" || item.type === "clipboard")
|
if (pluginId === "dms_clipboard_search" || item.type === "clipboard") {
|
||||||
return transformClipboardEntry(item.data || item);
|
var transformed = transformClipboardEntry(item.data || item);
|
||||||
|
if (item._preScored !== undefined)
|
||||||
|
transformed._preScored = item._preScored;
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
return transformBuiltInLauncherItem(item, pluginId);
|
return transformBuiltInLauncherItem(item, pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1871,7 +1926,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function executeSelected() {
|
function executeSelected() {
|
||||||
if (searchDebounce.running) {
|
if (_searchPending) {
|
||||||
|
_searchPending = false;
|
||||||
searchDebounce.stop();
|
searchDebounce.stop();
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,18 @@ Item {
|
|||||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
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
|
signal dialogClosed
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
@@ -328,6 +340,7 @@ Item {
|
|||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
|
id: launcherBlur
|
||||||
targetWindow: launcherWindow
|
targetWindow: launcherWindow
|
||||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
||||||
blurX: modalContainer.x
|
blurX: modalContainer.x
|
||||||
@@ -337,6 +350,9 @@ Item {
|
|||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onWidthChanged: root._kickBlurCommit()
|
||||||
|
onHeightChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
@@ -421,6 +437,9 @@ Item {
|
|||||||
|
|
||||||
opacity: contentVisible ? 1 : 0
|
opacity: contentVisible ? 1 : 0
|
||||||
|
|
||||||
|
onOpacityChanged: root._kickBlurCommit()
|
||||||
|
onSlideOffsetChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: contentVisible ? root._openDuration : root._closeDuration
|
duration: contentVisible ? root._openDuration : root._closeDuration
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ Item {
|
|||||||
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
||||||
readonly property real windowHeight: alignedHeight + contentY + 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 color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
|
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
||||||
@@ -113,6 +119,11 @@ Item {
|
|||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
|
function _kickBlurCommit() {
|
||||||
|
if (typeof launcherWindow.update === "function")
|
||||||
|
launcherWindow.update();
|
||||||
|
}
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
_pendingQuery = query || "";
|
_pendingQuery = query || "";
|
||||||
_pendingMode = mode || "";
|
_pendingMode = mode || "";
|
||||||
@@ -366,11 +377,13 @@ Item {
|
|||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: launcherWindow
|
targetWindow: launcherWindow
|
||||||
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
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
|
// Blur tracks the surface's scaled rect
|
||||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
|
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5
|
||||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
|
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5
|
||||||
blurWidth: contentVisible ? modalContainer.width * s : 0
|
blurWidth: contentVisible ? modalContainer.width * visibleScale : 0
|
||||||
blurHeight: contentVisible ? modalContainer.height * s : 0
|
blurHeight: contentVisible ? modalContainer.height * visibleScale : 0
|
||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +473,8 @@ Item {
|
|||||||
opacity: contentVisible ? 1 : 0
|
opacity: contentVisible ? 1 : 0
|
||||||
scale: contentVisible ? 1 : 0.96
|
scale: contentVisible ? 1 : 0.96
|
||||||
transformOrigin: Item.Center
|
transformOrigin: Item.Center
|
||||||
|
onOpacityChanged: root._kickBlurCommit()
|
||||||
|
onPublishedScaleChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|||||||
@@ -54,14 +54,7 @@ FocusScope {
|
|||||||
return;
|
return;
|
||||||
editingApp = app;
|
editingApp = app;
|
||||||
editAppId = app.id || app.execString || app.exec || "";
|
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;
|
editMode = true;
|
||||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeEditMode() {
|
function closeEditMode() {
|
||||||
@@ -71,27 +64,6 @@ FocusScope {
|
|||||||
Qt.callLater(() => searchField.forceActiveFocus());
|
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) {
|
function showContextMenu(item, x, y, fromKeyboard) {
|
||||||
if (!item)
|
if (!item)
|
||||||
return;
|
return;
|
||||||
@@ -817,291 +789,21 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusScope {
|
Loader {
|
||||||
id: editView
|
id: editLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
visible: editMode
|
active: root.editMode
|
||||||
focus: editMode
|
visible: active
|
||||||
|
focus: root.editMode
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
sourceComponent: AppEditView {
|
||||||
if (event.key === Qt.Key_Escape) {
|
focus: true
|
||||||
closeEditMode();
|
editingApp: root.editingApp
|
||||||
event.accepted = true;
|
editAppId: root.editAppId
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
onCloseRequested: root.closeEditMode()
|
||||||
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 {
|
onLoaded: item.loadOverride()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Rectangle {
|
|||||||
return item.icon || "";
|
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
|
width: parent?.width ?? 200
|
||||||
height: 52
|
height: 52
|
||||||
|
|||||||
@@ -241,12 +241,16 @@ Item {
|
|||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
|
readonly property string rowType: modelData?.type ?? ""
|
||||||
|
|
||||||
width: mainListView.width
|
width: mainListView.width
|
||||||
height: modelData?.height ?? 52
|
height: modelData?.height ?? 52
|
||||||
|
|
||||||
SectionHeader {
|
Loader {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: delegateRoot.modelData?.type === "header"
|
active: delegateRoot.rowType === "header"
|
||||||
|
visible: active
|
||||||
|
sourceComponent: SectionHeader {
|
||||||
section: delegateRoot.modelData?.section ?? null
|
section: delegateRoot.modelData?.section ?? null
|
||||||
controller: root.controller
|
controller: root.controller
|
||||||
viewMode: {
|
viewMode: {
|
||||||
@@ -262,16 +266,19 @@ Item {
|
|||||||
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
||||||
transientSurfaceTracker: root.transientSurfaceTracker
|
transientSurfaceTracker: root.transientSurfaceTracker
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ResultItem {
|
Loader {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: 2
|
anchors.topMargin: 2
|
||||||
anchors.bottomMargin: 2
|
anchors.bottomMargin: 2
|
||||||
visible: delegateRoot.modelData?.type === "list_item"
|
active: delegateRoot.rowType === "list_item"
|
||||||
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
|
visible: active
|
||||||
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
sourceComponent: ResultItem {
|
||||||
|
item: delegateRoot.modelData?.item ?? null
|
||||||
|
isSelected: (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
controller: root.controller
|
controller: root.controller
|
||||||
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
|
flatIndex: delegateRoot.modelData?.flatIndex ?? -1
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.controller && delegateRoot.modelData?.item) {
|
if (root.controller && delegateRoot.modelData?.item) {
|
||||||
@@ -283,30 +290,37 @@ Item {
|
|||||||
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Loader {
|
||||||
id: gridRowContent
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: delegateRoot.modelData?.type === "grid_row"
|
active: delegateRoot.rowType === "grid_row"
|
||||||
|
visible: active
|
||||||
|
sourceComponent: Row {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
|
model: delegateRoot.modelData?.items ?? []
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: gridCellDelegate
|
id: gridCellDelegate
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
|
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))
|
||||||
|
|
||||||
width: cellWidth
|
width: cellWidth
|
||||||
height: delegateRoot.height
|
height: delegateRoot.height
|
||||||
|
|
||||||
GridItem {
|
Loader {
|
||||||
width: parent.width - 4
|
width: parent.width - 4
|
||||||
height: parent.height - 4
|
height: parent.height - 4
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: delegateRoot.modelData?.viewMode === "grid"
|
sourceComponent: gridCellDelegate.isTile ? tileCellComponent : gridCellComponent
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: gridCellComponent
|
||||||
|
|
||||||
|
GridItem {
|
||||||
item: gridCellDelegate.modelData?.item ?? null
|
item: gridCellDelegate.modelData?.item ?? null
|
||||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
controller: root.controller
|
controller: root.controller
|
||||||
@@ -322,12 +336,12 @@ Item {
|
|||||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: tileCellComponent
|
||||||
|
|
||||||
TileItem {
|
TileItem {
|
||||||
width: parent.width - 4
|
|
||||||
height: parent.height - 4
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: delegateRoot.modelData?.viewMode === "tile"
|
|
||||||
item: gridCellDelegate.modelData?.item ?? null
|
item: gridCellDelegate.modelData?.item ?? null
|
||||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
controller: root.controller
|
controller: root.controller
|
||||||
@@ -349,6 +363,9 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bottomShadow
|
id: bottomShadow
|
||||||
|
|||||||
@@ -48,9 +48,8 @@ Rectangle {
|
|||||||
return "file://" + raw;
|
return "file://" + raw;
|
||||||
return 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 hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
||||||
readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0
|
|
||||||
|
|
||||||
readonly property string typeLabel: {
|
readonly property string typeLabel: {
|
||||||
if (!item)
|
if (!item)
|
||||||
@@ -284,16 +283,10 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: root.previewSource
|
source: root.previewSource
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
visible: !root.hasClipboardPreview && !root.previewAnimated
|
visible: !root.hasClipboardPreview
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: root.previewSource
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
playing: visible
|
|
||||||
visible: !root.hasClipboardPreview && root.previewAnimated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipboardLauncherPreview {
|
ClipboardLauncherPreview {
|
||||||
|
|||||||
@@ -165,6 +165,9 @@ Rectangle {
|
|||||||
anchors.margins: root.hasScreencopy ? 4 : 0
|
anchors.margins: root.hasScreencopy ? 4 : 0
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
source: root.item?.data?.attribution || ""
|
source: root.item?.data?.attribution || ""
|
||||||
|
asynchronous: true
|
||||||
|
sourceSize.width: 80
|
||||||
|
sourceSize.height: 80
|
||||||
mipmap: true
|
mipmap: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -98,27 +98,27 @@ StyledRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property string _videoThumb: ""
|
property string _videoThumb: ""
|
||||||
|
property bool _thumbGenAttempted: false
|
||||||
|
|
||||||
|
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||||
onVideoThumbnailPathChanged: {
|
onVideoThumbnailPathChanged: {
|
||||||
_videoThumb = "";
|
_thumbGenAttempted = false;
|
||||||
if (!videoThumbnailPath)
|
_videoThumb = videoThumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVideoThumbnail() {
|
||||||
|
if (_thumbGenAttempted)
|
||||||
return;
|
return;
|
||||||
|
_thumbGenAttempted = true;
|
||||||
|
_videoThumb = "";
|
||||||
const thumbPath = videoThumbnailPath;
|
const thumbPath = videoThumbnailPath;
|
||||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||||
const size = _thumbnailPx;
|
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
|
||||||
const fp = delegateRoot.filePath;
|
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||||
Paths.mkdir(thumbDir);
|
|
||||||
Proc.runCommand(null, ["test", "-f", 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)
|
if (exitCode === 0)
|
||||||
_videoThumb = thumbPath;
|
_videoThumb = thumbPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconForFile(fileName) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
@@ -160,10 +160,15 @@ StyledRect {
|
|||||||
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: gridPreviewImage
|
id: gridPreviewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
|
||||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||||
property int weExtIndex: 0
|
property int weExtIndex: 0
|
||||||
property string imagePath: {
|
property string imagePath: {
|
||||||
@@ -175,60 +180,32 @@ StyledRect {
|
|||||||
}
|
}
|
||||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
|
if (status !== Image.Error)
|
||||||
|
return;
|
||||||
|
if (weMode && delegateRoot.fileIsDir) {
|
||||||
if (weExtIndex < weExtensions.length - 1) {
|
if (weExtIndex < weExtensions.length - 1) {
|
||||||
weExtIndex++;
|
weExtIndex++;
|
||||||
} else {
|
} else {
|
||||||
imagePath = "";
|
imagePath = "";
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (_videoThumb)
|
||||||
|
generateVideoThumbnail();
|
||||||
}
|
}
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: false
|
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
CachingImage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
|
||||||
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
||||||
maxCacheSize: 256
|
maxCacheSize: 256
|
||||||
|
animate: false
|
||||||
visible: !delegateRoot.fileIsDir && isImage
|
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 {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -95,25 +95,27 @@ StyledRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property string _videoThumb: ""
|
property string _videoThumb: ""
|
||||||
|
property bool _thumbGenAttempted: false
|
||||||
|
|
||||||
|
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||||
onVideoThumbnailPathChanged: {
|
onVideoThumbnailPathChanged: {
|
||||||
_videoThumb = "";
|
_thumbGenAttempted = false;
|
||||||
if (!videoThumbnailPath)
|
_videoThumb = videoThumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVideoThumbnail() {
|
||||||
|
if (_thumbGenAttempted)
|
||||||
return;
|
return;
|
||||||
|
_thumbGenAttempted = true;
|
||||||
|
_videoThumb = "";
|
||||||
const thumbPath = videoThumbnailPath;
|
const thumbPath = videoThumbnailPath;
|
||||||
const fp = listDelegateRoot.filePath;
|
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
|
||||||
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
|
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
|
||||||
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
|
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)
|
if (exitCode === 0)
|
||||||
_videoThumb = thumbPath;
|
_videoThumb = thumbPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconForFile(fileName) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
@@ -165,6 +167,11 @@ StyledRect {
|
|||||||
height: 28
|
height: 28
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: listPreviewImage
|
id: listPreviewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -174,45 +181,19 @@ StyledRect {
|
|||||||
sourceSize.width: 32
|
sourceSize.width: 32
|
||||||
sourceSize.height: 32
|
sourceSize.height: 32
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: false
|
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error && _videoThumb)
|
||||||
|
generateVideoThumbnail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
CachingImage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
||||||
maxCacheSize: 256
|
maxCacheSize: 256
|
||||||
|
animate: false
|
||||||
visible: !listDelegateRoot.fileIsDir && isImage
|
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 {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -591,24 +591,11 @@ DankModal {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: gridProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: gridButtonRect.isHolding
|
visible: gridButtonRect.isHolding
|
||||||
layer.enabled: gridButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: gridProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -729,24 +716,11 @@ DankModal {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: listProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: listButtonRect.isHolding
|
visible: listButtonRect.isHolding
|
||||||
layer.enabled: listButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: listProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -108,16 +108,48 @@ Variants {
|
|||||||
function invalidate() {
|
function invalidate() {
|
||||||
_settleFrames = 3;
|
_settleFrames = 3;
|
||||||
backingWindow?.update();
|
backingWindow?.update();
|
||||||
|
if (!_wedgeBounced)
|
||||||
|
wedgeWatchdog.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRenderActiveChanged: invalidate()
|
onRenderActiveChanged: invalidate()
|
||||||
onBackingWindowChanged: 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 {
|
Connections {
|
||||||
target: root.backingWindow
|
target: root.backingWindow
|
||||||
function onFrameSwapped() {
|
function onFrameSwapped() {
|
||||||
if (root._settleFrames > 0)
|
if (root._settleFrames > 0)
|
||||||
root._settleFrames--;
|
root._settleFrames--;
|
||||||
|
root._wedgeBounced = false;
|
||||||
|
wedgeWatchdog.stop();
|
||||||
}
|
}
|
||||||
function onVisibleChanged() {
|
function onVisibleChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
@@ -142,10 +174,29 @@ Variants {
|
|||||||
function onWallpaperFillModeChanged() {
|
function onWallpaperFillModeChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
function onWallpaperBackgroundColorModeChanged() {
|
function onEffectiveWallpaperBackgroundColorChanged() {
|
||||||
root.invalidate();
|
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();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +221,7 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSourceChanged: {
|
onSourceChanged: {
|
||||||
|
invalidate();
|
||||||
if (!source || source.startsWith("#")) {
|
if (!source || source.startsWith("#")) {
|
||||||
setWallpaperImmediate("");
|
setWallpaperImmediate("");
|
||||||
return;
|
return;
|
||||||
@@ -190,6 +242,7 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaperImmediate(newSource) {
|
function setWallpaperImmediate(newSource) {
|
||||||
|
transitionDelayTimer.stop();
|
||||||
transitionAnimation.stop();
|
transitionAnimation.stop();
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
|
|||||||
@@ -7,15 +7,24 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
readonly property bool hasActiveMedia: activePlayer !== null
|
readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
readonly property bool live: visible && isPlaying
|
||||||
|
|
||||||
width: 20
|
width: 20
|
||||||
height: Theme.iconSize
|
height: Theme.iconSize
|
||||||
|
|
||||||
Loader {
|
readonly property real maxBarHeight: Theme.iconSize - 2
|
||||||
active: isPlaying
|
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 {
|
sourceComponent: Component {
|
||||||
Ref {
|
Ref {
|
||||||
service: CavaService
|
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 {
|
Timer {
|
||||||
id: fallbackTimer
|
running: !CavaService.cavaAvailable && root.live
|
||||||
|
|
||||||
running: !CavaService.cavaAvailable && isPlaying
|
|
||||||
interval: 500
|
interval: 500
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
@@ -41,54 +43,32 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CavaService
|
target: CavaService
|
||||||
|
enabled: root.live
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
if (!root.isPlaying) {
|
const v = CavaService.values;
|
||||||
root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
|
if (v.length < 6)
|
||||||
return;
|
return;
|
||||||
}
|
const n = i => {
|
||||||
|
const x = v[i];
|
||||||
const newHeights = [];
|
return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01);
|
||||||
for (let i = 0; i < 6; i++) {
|
};
|
||||||
if (CavaService.values.length <= i) {
|
bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3));
|
||||||
newHeights.push(root.minBarHeight);
|
bars.bandsB = Qt.vector2d(n(4), n(5));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
ShaderEffect {
|
||||||
anchors.centerIn: parent
|
id: bars
|
||||||
spacing: 1.5
|
anchors.fill: parent
|
||||||
|
|
||||||
Repeater {
|
property real widthPx: width
|
||||||
model: 6
|
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 {
|
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb")
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
@@ -234,16 +233,19 @@ BasePill {
|
|||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Row {
|
||||||
id: contentRow
|
id: contentRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
visible: !root.isVerticalOrientation
|
visible: !root.isVerticalOrientation
|
||||||
|
|
||||||
|
readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: horizontalAppIcon
|
id: horizontalAppIcon
|
||||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
width: contentRow.iconSize
|
||||||
Layout.preferredHeight: Layout.preferredWidth
|
height: contentRow.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showIcon && activeWindow && status === Image.Ready
|
visible: root.showIcon && activeWindow && status === Image.Ready
|
||||||
source: {
|
source: {
|
||||||
if (!activeWindow || !activeWindow.appId)
|
if (!activeWindow || !activeWindow.appId)
|
||||||
@@ -264,8 +266,10 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
id: horizontalSteamIcon
|
||||||
size: Layout.preferredWidth
|
width: contentRow.iconSize
|
||||||
|
size: contentRow.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
|
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)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.maximumWidth: compactMode ? 80 : 180
|
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +297,7 @@ BasePill {
|
|||||||
text: compactMode ? "" : "•"
|
text: compactMode ? "" : "•"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.outlineButton
|
color: Theme.outlineButton
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: !compactMode && appText.text && titleText.text
|
visible: !compactMode && appText.text && titleText.text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,9 +325,24 @@ BasePill {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
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
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ Item {
|
|||||||
function mapWorkspace(ws) {
|
function mapWorkspace(ws) {
|
||||||
return {
|
return {
|
||||||
"num": ws.number,
|
"num": ws.number,
|
||||||
"name": ws.name,
|
"name": stripSwayWorkspaceNumber(ws.number, ws.name),
|
||||||
"focused": ws.focused,
|
"focused": ws.focused,
|
||||||
"active": ws.active,
|
"active": ws.active,
|
||||||
"urgent": ws.urgent,
|
"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
|
// Numbered workspaces first in ascending order; purely-named workspaces (sway reports num -1) after, by name
|
||||||
function swayWorkspaceOrder(a, b) {
|
function swayWorkspaceOrder(a, b) {
|
||||||
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
|
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
|
||||||
|
|||||||
@@ -240,8 +240,10 @@ DankPopout {
|
|||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
if (root.shouldBeVisible)
|
if (!root.shouldBeVisible)
|
||||||
|
return;
|
||||||
mainContainer.forceActiveFocus();
|
mainContainer.forceActiveFocus();
|
||||||
|
tabBar.snapIndicator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +413,7 @@ DankPopout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentTabId === "media"
|
active: root.currentTabId === "media"
|
||||||
visible: active
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
MediaPlayerTab {
|
MediaPlayerTab {
|
||||||
targetScreen: root.screen
|
targetScreen: root.screen
|
||||||
@@ -462,7 +465,7 @@ DankPopout {
|
|||||||
DankSpinner {
|
DankSpinner {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: 40
|
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 {
|
Loader {
|
||||||
@@ -470,6 +473,7 @@ DankPopout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentTabId === "weather"
|
active: root.currentTabId === "weather"
|
||||||
visible: active
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
WeatherTab {}
|
WeatherTab {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ Item {
|
|||||||
|
|
||||||
property int __panelHoverCount: 0
|
property int __panelHoverCount: 0
|
||||||
|
|
||||||
|
readonly property bool overlayBlurActive: dropdownBlur.active
|
||||||
|
|
||||||
onDropdownTypeChanged: {
|
onDropdownTypeChanged: {
|
||||||
if (dropdownType === 0) {
|
if (dropdownType === 0) {
|
||||||
__panelHoverCount = 0;
|
__panelHoverCount = 0;
|
||||||
@@ -75,8 +77,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
|
id: dropdownBlur
|
||||||
targetWindow: root.targetWindow
|
targetWindow: root.targetWindow
|
||||||
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
|
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
|
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
|
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
|
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -24,6 +25,11 @@ Item {
|
|||||||
property string section: ""
|
property string section: ""
|
||||||
property int barPosition: SettingsData.Position.Top
|
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 showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||||
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
||||||
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||||
@@ -70,7 +76,7 @@ Item {
|
|||||||
property bool _switchHold: false
|
property bool _switchHold: false
|
||||||
Timer {
|
Timer {
|
||||||
id: _switchHoldTimer
|
id: _switchHoldTimer
|
||||||
interval: 650
|
interval: 1500
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: _switchHold = false
|
onTriggered: _switchHold = false
|
||||||
}
|
}
|
||||||
@@ -78,7 +84,8 @@ Item {
|
|||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
if (!activePlayer) {
|
if (!activePlayer) {
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
_switchHold = true;
|
||||||
|
_switchHoldTimer.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSwitching = true;
|
isSwitching = true;
|
||||||
@@ -117,16 +124,12 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: MprisController
|
target: MprisController
|
||||||
function onAvailablePlayersChanged() {
|
function onAvailablePlayersChanged() {
|
||||||
const count = (MprisController.availablePlayers?.length || 0);
|
if ((MprisController.availablePlayers?.length || 0) === 0)
|
||||||
if (count === 0) {
|
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
|
||||||
} else {
|
|
||||||
_switchHold = true;
|
_switchHold = true;
|
||||||
_switchHoldTimer.restart();
|
_switchHoldTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getAudioDeviceIcon(device) {
|
function getAudioDeviceIcon(device) {
|
||||||
if (!device || !device.name)
|
if (!device || !device.name)
|
||||||
@@ -290,34 +293,95 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
id: bgContainer
|
id: bgContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: TrackArtService.resolvedArtUrl !== ""
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
antialiasing: true
|
||||||
|
opacity: front ? 0.7 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 350
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: bgImage
|
id: layerImg
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.max(parent.width, parent.height) * 1.1
|
width: Math.max(parent.width, parent.height) * 1.1
|
||||||
height: width
|
height: width
|
||||||
source: TrackArtService.resolvedArtUrl
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: true
|
cache: true
|
||||||
visible: false
|
visible: false
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status === Image.Ready)
|
if (status === Image.Ready && source != "")
|
||||||
maybeFinishSwitch();
|
layer.loaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: blurredBg
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
MultiEffect {
|
MultiEffect {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: bgImage.width
|
width: layerImg.width
|
||||||
height: bgImage.height
|
height: layerImg.height
|
||||||
source: bgImage
|
source: layerImg
|
||||||
blurEnabled: true
|
blurEnabled: true
|
||||||
blurMax: 64
|
blurMax: 64
|
||||||
blur: 0.8
|
blur: 0.8
|
||||||
@@ -326,22 +390,16 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
BgBlurLayer {
|
||||||
id: bgMask
|
id: layerA
|
||||||
anchors.fill: parent
|
front: bgContainer._showA
|
||||||
radius: Theme.cornerRadius
|
onLoaded: bgContainer.promote(layerA)
|
||||||
visible: false
|
|
||||||
layer.enabled: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiEffect {
|
BgBlurLayer {
|
||||||
anchors.fill: parent
|
id: layerB
|
||||||
source: blurredBg
|
front: !bgContainer._showA
|
||||||
maskEnabled: true
|
onLoaded: bgContainer.promote(layerB)
|
||||||
maskSource: bgMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1.0
|
|
||||||
opacity: 0.7
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -442,7 +500,8 @@ Item {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
maximumLineCount: 1
|
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
|
height: 40
|
||||||
radius: 20
|
radius: 20
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "shuffle"
|
name: "shuffle"
|
||||||
size: 20
|
size: 20
|
||||||
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
|
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -593,13 +652,13 @@ Item {
|
|||||||
height: 50
|
height: 50
|
||||||
radius: 25
|
radius: 25
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Theme.primary
|
color: root.accent
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
size: 28
|
size: 28
|
||||||
color: Theme.background
|
color: root.onAccent
|
||||||
weight: 500
|
weight: 500
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +722,7 @@ Item {
|
|||||||
height: 40
|
height: 40
|
||||||
radius: 20
|
radius: 20
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -680,7 +739,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
size: 20
|
size: 20
|
||||||
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
|
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -719,7 +778,7 @@ Item {
|
|||||||
radius: 20
|
radius: 20
|
||||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||||
y: 185
|
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.color: Theme.outlineStrong
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 100
|
z: 100
|
||||||
@@ -786,7 +845,7 @@ Item {
|
|||||||
radius: 20
|
radius: 20
|
||||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||||
y: 130
|
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.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 101
|
z: 101
|
||||||
@@ -798,7 +857,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: getVolumeIcon()
|
name: getVolumeIcon()
|
||||||
size: 18
|
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 {
|
MouseArea {
|
||||||
@@ -849,7 +908,7 @@ Item {
|
|||||||
radius: 20
|
radius: 20
|
||||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||||
y: 240
|
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.color: Theme.outlineStrong
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 100
|
z: 100
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ Card {
|
|||||||
width: 32
|
width: 32
|
||||||
height: 32
|
height: 32
|
||||||
radius: 16
|
radius: 16
|
||||||
color: Theme.primary
|
color: MediaAccentService.accent
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
size: 16
|
size: 16
|
||||||
color: Theme.background
|
color: MediaAccentService.onAccent
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.DankDash.Overview
|
import qs.Modules.DankDash.Overview
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,7 +18,7 @@ Item {
|
|||||||
signal navFocusRequested
|
signal navFocusRequested
|
||||||
|
|
||||||
function handleKeyEvent(event) {
|
function handleKeyEvent(event) {
|
||||||
return calendarCard.handleKeyEvent(event);
|
return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -57,17 +58,27 @@ Item {
|
|||||||
height: 220
|
height: 220
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calendar - bottom middle (wider and taller)
|
// Calendar - bottom middle; deferred so the grid stays off the emerge frame.
|
||||||
CalendarOverviewCard {
|
Loader {
|
||||||
id: calendarCard
|
id: calendarLoader
|
||||||
x: parent.width * 0.2 - Theme.spacingM
|
x: parent.width * 0.2 - Theme.spacingM
|
||||||
y: 100 + Theme.spacingM
|
y: 100 + Theme.spacingM
|
||||||
width: parent.width * 0.6
|
width: parent.width * 0.6
|
||||||
height: 300
|
height: 300
|
||||||
|
asynchronous: true
|
||||||
|
sourceComponent: Component {
|
||||||
|
CalendarOverviewCard {
|
||||||
onCloseDash: root.closeDash()
|
onCloseDash: root.closeDash()
|
||||||
onNavFocusRequested: root.navFocusRequested()
|
onNavFocusRequested: root.navFocusRequested()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSpinner {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: 32
|
||||||
|
visible: calendarLoader.status === Loader.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Media - bottom right (narrow and taller)
|
// Media - bottom right (narrow and taller)
|
||||||
MediaOverviewCard {
|
MediaOverviewCard {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -245,17 +244,6 @@ Item {
|
|||||||
size: Theme.iconSize * 2
|
size: Theme.iconSize * 2
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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 {
|
Column {
|
||||||
@@ -562,6 +550,36 @@ Item {
|
|||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
property real latitude: WeatherService.getLocation()?.latitude ?? 0
|
||||||
|
property real sunDeclination: WeatherService.getSunDeclination(dateStepper.currentDate)
|
||||||
|
|
||||||
|
readonly property bool solarNoonIsSouth: latitude > sunDeclination
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
asynchronous: true
|
||||||
|
sourceComponent: skyContentComponent
|
||||||
|
opacity: status === Loader.Ready ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: skyContentComponent
|
||||||
|
|
||||||
|
Item {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
opacity: skyBox.backgroundOpacity
|
opacity: skyBox.backgroundOpacity
|
||||||
@@ -598,14 +616,8 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
StyledText {
|
||||||
text: parent.sunTime?.period ?? ""
|
text: skyBox.sunTime?.period ?? ""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||||
x: 0
|
x: 0
|
||||||
@@ -708,39 +720,26 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas {
|
Shape {
|
||||||
id: ecliptic
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
property var points: WeatherService.getEcliptic(dateStepper.currentDate)
|
|
||||||
|
|
||||||
function getX(index) {
|
ShapePath {
|
||||||
return points[index].h * skyBox.effectiveWidth + skyBox.hMargin;
|
strokeColor: Theme.withAlpha(Theme.outline, 0.2)
|
||||||
}
|
strokeWidth: 1
|
||||||
function getY(index) {
|
fillColor: "transparent"
|
||||||
return points[index].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPointsChanged: requestPaint()
|
PathPolyline {
|
||||||
|
path: {
|
||||||
onPaint: {
|
const points = WeatherService.getEcliptic(dateStepper.currentDate) ?? [];
|
||||||
var ctx = getContext("2d");
|
const out = [];
|
||||||
ctx.clearRect(0, 0, width, height);
|
for (let i = 0; i < points.length; i++) {
|
||||||
if (!points || points.length === 0)
|
out.push(Qt.point(points[i].h * skyBox.effectiveWidth + skyBox.hMargin, points[i].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin));
|
||||||
return;
|
}
|
||||||
ctx.beginPath();
|
return out;
|
||||||
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 {
|
StyledText {
|
||||||
id: middle
|
id: middle
|
||||||
@@ -774,7 +773,7 @@ Item {
|
|||||||
anchors.leftMargin: Theme.spacingS
|
anchors.leftMargin: Theme.spacingS
|
||||||
anchors.rightMargin: Theme.spacingS
|
anchors.rightMargin: Theme.spacingS
|
||||||
anchors.left: right.right
|
anchors.left: right.right
|
||||||
anchors.right: skyBox.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: middle.verticalCenter
|
anchors.verticalCenter: middle.verticalCenter
|
||||||
color: Theme.outline
|
color: Theme.outline
|
||||||
}
|
}
|
||||||
@@ -803,7 +802,7 @@ Item {
|
|||||||
height: 1
|
height: 1
|
||||||
anchors.leftMargin: Theme.spacingS
|
anchors.leftMargin: Theme.spacingS
|
||||||
anchors.rightMargin: Theme.spacingS
|
anchors.rightMargin: Theme.spacingS
|
||||||
anchors.left: skyBox.left
|
anchors.left: parent.left
|
||||||
anchors.right: left.left
|
anchors.right: left.left
|
||||||
anchors.verticalCenter: middle.verticalCenter
|
anchors.verticalCenter: middle.verticalCenter
|
||||||
color: Theme.outline
|
color: Theme.outline
|
||||||
@@ -820,16 +819,6 @@ Item {
|
|||||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
||||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
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
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -842,15 +831,6 @@ Item {
|
|||||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
||||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
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
|
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -993,6 +973,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: hourlyList
|
id: hourlyList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
reuseItems: true
|
||||||
orientation: ListView.Horizontal
|
orientation: ListView.Horizontal
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
@@ -1079,6 +1060,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: dailyList
|
id: dailyList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
reuseItems: true
|
||||||
orientation: ListView.Horizontal
|
orientation: ListView.Horizontal
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
|
|||||||
@@ -807,22 +807,23 @@ PanelWindow {
|
|||||||
|
|
||||||
function _notifBodyScene() {
|
function _notifBodyScene() {
|
||||||
const isHoriz = SurfaceGeometry.isHorizontal(win._notifDescriptor.barSide);
|
const isHoriz = SurfaceGeometry.isHorizontal(win._notifDescriptor.barSide);
|
||||||
|
const body = win._notifBodyGeometry;
|
||||||
const start = win._notifStartUnderlapValue;
|
const start = win._notifStartUnderlapValue;
|
||||||
const end = win._notifEndUnderlapValue;
|
const end = win._notifEndUnderlapValue;
|
||||||
const side = win._notifSideUnderlapValue;
|
const side = win._notifSideUnderlapValue;
|
||||||
if (isHoriz) {
|
if (isHoriz) {
|
||||||
return {
|
return {
|
||||||
"x": _notifBodyBlurAnchor.x - start,
|
"x": body.x - start,
|
||||||
"y": _notifBodyBlurAnchor.y,
|
"y": body.y,
|
||||||
"width": _notifBodyBlurAnchor.width + start + end,
|
"width": body.width + start + end,
|
||||||
"height": _notifBodyBlurAnchor.height
|
"height": body.height
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"x": _notifBodyBlurAnchor.x - (win._notifDescriptor.barSide === "left" ? side : 0),
|
"x": body.x - (win._notifDescriptor.barSide === "left" ? side : 0),
|
||||||
"y": _notifBodyBlurAnchor.y - start,
|
"y": body.y - start,
|
||||||
"width": _notifBodyBlurAnchor.width + side,
|
"width": body.width + side,
|
||||||
"height": _notifBodyBlurAnchor.height + start + end
|
"height": body.height + start + end
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,8 +867,9 @@ PanelWindow {
|
|||||||
"surfaceRadius": win._surfaceRadius
|
"surfaceRadius": win._surfaceRadius
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const n = win._notifBodyGeometry;
|
const n = win._notifBodyScene();
|
||||||
if (win._frameActive && win._notifDescriptor.visible && n.width > 0 && n.height > 0)
|
const nb = win._notifBodyGeometry;
|
||||||
|
if (win._frameActive && win._notifDescriptor.visible && nb.width > 0 && nb.height > 0)
|
||||||
arr.push({
|
arr.push({
|
||||||
"side": win._notifDescriptor.barSide,
|
"side": win._notifDescriptor.barSide,
|
||||||
"body": {"x": n.x, "y": n.y, "width": n.width, "height": n.height},
|
"body": {"x": n.x, "y": n.y, "width": n.width, "height": n.height},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -567,24 +567,11 @@ Rectangle {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: gridProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: gridButtonRect.isHolding
|
visible: gridButtonRect.isHolding
|
||||||
layer.enabled: gridButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: gridProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -700,24 +687,11 @@ Rectangle {
|
|||||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||||
border.width: isSelected ? 2 : 0
|
border.width: isSelected ? 2 : 0
|
||||||
|
|
||||||
Rectangle {
|
ClippingRectangle {
|
||||||
id: listProgressMask
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
visible: false
|
color: "transparent"
|
||||||
layer.enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: listButtonRect.isHolding
|
visible: listButtonRect.isHolding
|
||||||
layer.enabled: listButtonRect.isHolding
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: listProgressMask
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -847,6 +847,34 @@ Item {
|
|||||||
cursorPosition = pos;
|
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) {
|
function isPrintableText(value) {
|
||||||
if (value.length === 0)
|
if (value.length === 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -912,6 +940,49 @@ Item {
|
|||||||
return;
|
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) {
|
switch (event.key) {
|
||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
case Qt.Key_Enter:
|
case Qt.Key_Enter:
|
||||||
|
|||||||
@@ -13,12 +13,19 @@ Column {
|
|||||||
property int draggedIndex: -1
|
property int draggedIndex: -1
|
||||||
property int dropTargetIndex: -1
|
property int dropTargetIndex: -1
|
||||||
property bool suppressShiftAnimation: false
|
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 tabSwitched(int tabIndex)
|
||||||
signal tabClosed(int tabIndex)
|
signal tabClosed(int tabIndex)
|
||||||
signal newTabRequested
|
signal newTabRequested
|
||||||
|
|
||||||
|
function commitRename(index, newTitle) {
|
||||||
|
if (index >= 0)
|
||||||
|
NotepadStorageService.renameTab(index, newTitle);
|
||||||
|
editingIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
function hasUnsavedChangesForTab(tab) {
|
function hasUnsavedChangesForTab(tab) {
|
||||||
if (!tab)
|
if (!tab)
|
||||||
return false;
|
return false;
|
||||||
@@ -32,11 +39,19 @@ Column {
|
|||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
id: tabRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 36
|
height: 36
|
||||||
spacing: Theme.spacingXS
|
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 {
|
ScrollView {
|
||||||
|
id: tabScroll
|
||||||
width: parent.width - newTabButton.width - Theme.spacingXS
|
width: parent.width - newTabButton.width - Theme.spacingXS
|
||||||
height: parent.height
|
height: parent.height
|
||||||
clip: true
|
clip: true
|
||||||
@@ -57,7 +72,8 @@ Column {
|
|||||||
|
|
||||||
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
|
||||||
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
|
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 longPressing: false
|
||||||
property bool dragging: false
|
property bool dragging: false
|
||||||
property point dragStartPos: Qt.point(0, 0)
|
property point dragStartPos: Qt.point(0, 0)
|
||||||
@@ -138,6 +154,7 @@ Column {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: tabText
|
id: tabText
|
||||||
|
visible: !delegateItem.editing
|
||||||
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
|
||||||
text: {
|
text: {
|
||||||
var prefix = "";
|
var prefix = "";
|
||||||
@@ -155,13 +172,54 @@ Column {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
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 {
|
Rectangle {
|
||||||
id: tabCloseButton
|
id: tabCloseButton
|
||||||
width: 20
|
width: 20
|
||||||
height: 20
|
height: 20
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
|
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
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -195,11 +253,24 @@ Column {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
id: tabMouseArea
|
id: tabMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
enabled: !delegateItem.editing
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: dragging || longPressing
|
preventStealing: dragging || longPressing
|
||||||
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||||
acceptedButtons: Qt.LeftButton
|
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 => {
|
onPressed: mouse => {
|
||||||
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
|
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
|
||||||
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
|
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||||
@@ -279,4 +350,8 @@ Column {
|
|||||||
onClicked: root.newTabRequested()
|
onClicked: root.newTabRequested()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankTooltipV2 {
|
||||||
|
id: tabTooltip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import qs.Common
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Widgets
|
||||||
|
|
||||||
DankOSD {
|
DankOSD {
|
||||||
id: root
|
id: root
|
||||||
@@ -185,10 +186,11 @@ DankOSD {
|
|||||||
visible: false
|
visible: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
ClippingRectangle {
|
||||||
id: blurredBg
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
opacity: 0.7
|
||||||
|
|
||||||
MultiEffect {
|
MultiEffect {
|
||||||
anchors.centerIn: parent
|
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 {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import QtCore
|
import QtCore
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.FileBrowser
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -520,9 +520,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = Theme.wallpaperPath;
|
var wp = Theme.wallpaperPath;
|
||||||
if (!wp || wp === "" || wp.startsWith("#"))
|
if (!wp || wp === "" || wp.startsWith("#"))
|
||||||
@@ -536,12 +541,6 @@ Item {
|
|||||||
sourceSize.width: 120
|
sourceSize.width: 120
|
||||||
sourceSize.height: 120
|
sourceSize.height: 120
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: autoWallpaperMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,16 +552,6 @@ Item {
|
|||||||
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
|
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 {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
|
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.FileBrowser
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -75,9 +75,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = root.currentWallpaper;
|
var wp = root.currentWallpaper;
|
||||||
if (wp === "" || wp.startsWith("#"))
|
if (wp === "" || wp.startsWith("#"))
|
||||||
@@ -91,12 +96,6 @@ Item {
|
|||||||
sourceSize.width: 160
|
sourceSize.width: 160
|
||||||
sourceSize.height: 160
|
sourceSize.height: 160
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: wallpaperMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,16 +107,6 @@ Item {
|
|||||||
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
|
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 {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "image"
|
name: "image"
|
||||||
@@ -421,9 +410,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = SessionData.wallpaperPathLight;
|
var wp = SessionData.wallpaperPathLight;
|
||||||
if (wp === "" || wp.startsWith("#"))
|
if (wp === "" || wp.startsWith("#"))
|
||||||
@@ -440,12 +434,6 @@ Item {
|
|||||||
sourceSize.width: 160
|
sourceSize.width: 160
|
||||||
sourceSize.height: 160
|
sourceSize.height: 160
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: lightMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "light_mode"
|
name: "light_mode"
|
||||||
@@ -611,9 +589,14 @@ Item {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceVariant
|
color: Theme.surfaceVariant
|
||||||
|
|
||||||
Image {
|
ClippingRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
radius: Theme.cornerRadius - 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
var wp = SessionData.wallpaperPathDark;
|
var wp = SessionData.wallpaperPathDark;
|
||||||
if (wp === "" || wp.startsWith("#"))
|
if (wp === "" || wp.startsWith("#"))
|
||||||
@@ -630,12 +613,6 @@ Item {
|
|||||||
sourceSize.width: 160
|
sourceSize.width: 160
|
||||||
sourceSize.height: 160
|
sourceSize.height: 160
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: darkMask
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "dark_mode"
|
name: "dark_mode"
|
||||||
|
|||||||
@@ -141,16 +141,37 @@ Variants {
|
|||||||
function invalidate() {
|
function invalidate() {
|
||||||
_settleFrames = 3;
|
_settleFrames = 3;
|
||||||
backingWindow?.update();
|
backingWindow?.update();
|
||||||
|
if (!_wedgeBounced)
|
||||||
|
wedgeWatchdog.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRenderActiveChanged: invalidate()
|
onRenderActiveChanged: invalidate()
|
||||||
onBackingWindowChanged: 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 {
|
Connections {
|
||||||
target: root.backingWindow
|
target: root.backingWindow
|
||||||
function onFrameSwapped() {
|
function onFrameSwapped() {
|
||||||
if (root._settleFrames > 0)
|
if (root._settleFrames > 0)
|
||||||
root._settleFrames--;
|
root._settleFrames--;
|
||||||
|
root._wedgeBounced = false;
|
||||||
|
wedgeWatchdog.stop();
|
||||||
}
|
}
|
||||||
function onVisibleChanged() {
|
function onVisibleChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
@@ -176,10 +197,29 @@ Variants {
|
|||||||
function onWallpaperFillModeChanged() {
|
function onWallpaperFillModeChanged() {
|
||||||
root.invalidate();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
function onWallpaperBackgroundColorModeChanged() {
|
function onEffectiveWallpaperBackgroundColorChanged() {
|
||||||
root.invalidate();
|
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();
|
root.invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,13 +542,13 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSourceChanged: {
|
onSourceChanged: {
|
||||||
|
invalidate();
|
||||||
if (!source || source.startsWith("#")) {
|
if (!source || source.startsWith("#")) {
|
||||||
setWallpaperImmediate("");
|
setWallpaperImmediate("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
root.changePending = true;
|
root.changePending = true;
|
||||||
invalidate();
|
|
||||||
|
|
||||||
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
||||||
|
|
||||||
@@ -528,10 +568,14 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setWallpaperImmediate(newSource) {
|
function setWallpaperImmediate(newSource) {
|
||||||
|
transitionDelayTimer.stop();
|
||||||
transitionAnimation.stop();
|
transitionAnimation.stop();
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
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;
|
currentWallpaper.source = newSource;
|
||||||
nextWallpaper.source = "";
|
nextWallpaper.source = "";
|
||||||
|
|
||||||
@@ -567,10 +611,14 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeWallpaper(newPath, force) {
|
function changeWallpaper(newPath, force) {
|
||||||
if (!force && newPath === currentWallpaper.source)
|
if (!force && newPath === currentWallpaper.source.toString()) {
|
||||||
|
root.changePending = false;
|
||||||
return;
|
return;
|
||||||
if (!newPath || newPath.startsWith("#"))
|
}
|
||||||
|
if (!newPath || newPath.startsWith("#")) {
|
||||||
|
root.changePending = false;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
root.screenScale = CompositorService.getScreenScale(modelData);
|
||||||
if (root.transitioning || root.effectActive) {
|
if (root.transitioning || root.effectActive) {
|
||||||
root.pendingWallpaper = newPath;
|
root.pendingWallpaper = newPath;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -58,23 +58,6 @@ Item {
|
|||||||
visible: intersectsViewport
|
visible: intersectsViewport
|
||||||
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
|
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 {
|
Behavior on x {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
@@ -104,6 +87,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClippingRectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
ScreencopyView {
|
ScreencopyView {
|
||||||
id: windowPreview
|
id: windowPreview
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -113,9 +101,7 @@ Item {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
|
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
||||||
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
|
|
||||||
Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
|
||||||
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
}
|
}
|
||||||
@@ -129,7 +115,7 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
id: windowIcon
|
id: windowIcon
|
||||||
property var iconSize: {
|
property var iconSize: {
|
||||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
|
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
|
||||||
}
|
}
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
source: root.iconPath
|
source: root.iconPath
|
||||||
@@ -154,4 +140,5 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,11 @@ Singleton {
|
|||||||
function getBuiltInLauncherItems(pluginId, query) {
|
function getBuiltInLauncherItems(pluginId, query) {
|
||||||
if (pluginId === "dms_clipboard_search") {
|
if (pluginId === "dms_clipboard_search") {
|
||||||
const trimmed = (query || "").toString().trim();
|
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 => ({
|
return entries.map(entry => ({
|
||||||
type: "clipboard",
|
type: "clipboard",
|
||||||
data: entry
|
data: entry
|
||||||
|
|||||||
@@ -841,13 +841,32 @@ EOFCONFIG
|
|||||||
function setVolume(percentage) {
|
function setVolume(percentage) {
|
||||||
if (!root.sink?.audio)
|
if (!root.sink?.audio)
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
|
if (isNaN(percentage))
|
||||||
|
return "Invalid percentage";
|
||||||
|
|
||||||
const maxVol = root.sinkMaxVolume;
|
const maxVol = root.sinkMaxVolume;
|
||||||
const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
|
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}%`;
|
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() {
|
function toggleMute() {
|
||||||
if (!root.sink?.audio) {
|
if (!root.sink?.audio) {
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
@@ -939,15 +958,7 @@ EOFCONFIG
|
|||||||
if (!root.sink?.audio)
|
if (!root.sink?.audio)
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
|
|
||||||
if (root.sink.audio.muted)
|
const newVolume = root.adjustDefaultSinkVolume(step, 1);
|
||||||
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;
|
|
||||||
return `Volume increased to ${newVolume}%`;
|
return `Volume increased to ${newVolume}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,15 +966,7 @@ EOFCONFIG
|
|||||||
if (!root.sink?.audio)
|
if (!root.sink?.audio)
|
||||||
return "No audio sink available";
|
return "No audio sink available";
|
||||||
|
|
||||||
if (root.sink.audio.muted)
|
const newVolume = root.adjustDefaultSinkVolume(step, -1);
|
||||||
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;
|
|
||||||
return `Volume decreased to ${newVolume}%`;
|
return `Volume decreased to ${newVolume}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("ChangelogService")
|
readonly property var log: Log.scoped("ChangelogService")
|
||||||
|
|
||||||
readonly property string currentVersion: "1.4"
|
readonly property string currentVersion: "1.5"
|
||||||
readonly property bool changelogEnabled: false
|
readonly property bool changelogEnabled: true
|
||||||
|
|
||||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
||||||
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Singleton {
|
|||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
property bool keyboardNavigationActive: false
|
property bool keyboardNavigationActive: false
|
||||||
property int refCount: 0
|
property int refCount: 0
|
||||||
property real _launcherLastRefresh: 0
|
|
||||||
property bool _launcherCacheValid: false
|
property bool _launcherCacheValid: false
|
||||||
property string _launcherCachedQuery: ""
|
property string _launcherCachedQuery: ""
|
||||||
property var _launcherCachedEntries: []
|
property var _launcherCachedEntries: []
|
||||||
@@ -85,31 +84,30 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
let filtered = internalEntries;
|
const query = searchText.trim().toLowerCase();
|
||||||
|
const filterAll = activeFilter === "all";
|
||||||
|
const unpinned = [];
|
||||||
|
const pinned = [];
|
||||||
|
|
||||||
if (activeFilter !== "all") {
|
for (let i = 0; i < internalEntries.length; i++) {
|
||||||
filtered = filtered.filter(entry => getEntryType(entry) === activeFilter);
|
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) {
|
pinnedEntries = pinned;
|
||||||
const lowerQuery = query.toLowerCase();
|
unpinnedEntries = unpinned;
|
||||||
filtered = filtered.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
clipboardEntries = pinned.concat(unpinned);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
totalCount = clipboardEntries.length;
|
totalCount = clipboardEntries.length;
|
||||||
|
|
||||||
const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length);
|
const activeCount = Math.max(unpinned.length, pinned.length);
|
||||||
|
|
||||||
if (activeCount === 0) {
|
if (activeCount === 0) {
|
||||||
keyboardNavigationActive = false;
|
keyboardNavigationActive = false;
|
||||||
@@ -117,10 +115,9 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIndex >= activeCount) {
|
if (selectedIndex >= activeCount)
|
||||||
selectedIndex = activeCount - 1;
|
selectedIndex = activeCount - 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (!clipboardAvailable) {
|
if (!clipboardAvailable) {
|
||||||
@@ -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) {
|
function requestLauncherSearch(query, limit) {
|
||||||
if (!clipboardAvailable) {
|
if (!clipboardAvailable) {
|
||||||
return;
|
return;
|
||||||
@@ -207,56 +192,6 @@ Singleton {
|
|||||||
_launcherSearchSeq++;
|
_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() {
|
function reset() {
|
||||||
searchText = "";
|
searchText = "";
|
||||||
selectedIndex = 0;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,45 +29,48 @@ Singleton {
|
|||||||
|
|
||||||
function setSessionBuffer(tabId, content, baseline) {
|
function setSessionBuffer(tabId, content, baseline) {
|
||||||
if (tabId === undefined || tabId === null || tabId < 0)
|
if (tabId === undefined || tabId === null || tabId < 0)
|
||||||
return
|
return;
|
||||||
var next = Object.assign({}, sessionBuffers)
|
var next = Object.assign({}, sessionBuffers);
|
||||||
if (content !== baseline) {
|
if (content !== baseline) {
|
||||||
next[tabId] = { content: content, baseline: baseline }
|
next[tabId] = {
|
||||||
|
content: content,
|
||||||
|
baseline: baseline
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
delete next[tabId]
|
delete next[tabId];
|
||||||
}
|
}
|
||||||
sessionBuffers = next
|
sessionBuffers = next;
|
||||||
sessionBufferRevision++
|
sessionBufferRevision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionBuffer(tabId) {
|
function getSessionBuffer(tabId) {
|
||||||
return sessionBuffers[tabId]
|
return sessionBuffers[tabId];
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSessionBuffer(tabId) {
|
function clearSessionBuffer(tabId) {
|
||||||
if (sessionBuffers[tabId] === undefined)
|
if (sessionBuffers[tabId] === undefined)
|
||||||
return
|
return;
|
||||||
var next = Object.assign({}, sessionBuffers)
|
var next = Object.assign({}, sessionBuffers);
|
||||||
delete next[tabId]
|
delete next[tabId];
|
||||||
sessionBuffers = next
|
sessionBuffers = next;
|
||||||
sessionBufferRevision++
|
sessionBufferRevision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
property var conflictTabId: -1
|
property var conflictTabId: -1
|
||||||
property string conflictDiskContent: ""
|
property string conflictDiskContent: ""
|
||||||
|
|
||||||
function flagConflict(tabId, diskContent) {
|
function flagConflict(tabId, diskContent) {
|
||||||
conflictDiskContent = diskContent
|
conflictDiskContent = diskContent;
|
||||||
conflictTabId = tabId
|
conflictTabId = tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearConflict() {
|
function clearConflict() {
|
||||||
conflictTabId = -1
|
conflictTabId = -1;
|
||||||
conflictDiskContent = ""
|
conflictDiskContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
ensureDirectories()
|
ensureDirectories();
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -78,49 +81,50 @@ Singleton {
|
|||||||
|
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
try {
|
try {
|
||||||
var data = JSON.parse(text())
|
var data = JSON.parse(text());
|
||||||
root.tabs = data.tabs || []
|
root.tabs = data.tabs || [];
|
||||||
root.currentTabIndex = data.currentTabIndex || 0
|
root.currentTabIndex = data.currentTabIndex || 0;
|
||||||
root.metadataLoaded = true
|
root.metadataLoaded = true;
|
||||||
root.validateTabs()
|
root.validateTabs();
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
log.warn("Failed to parse notepad metadata:", e)
|
log.warn("Failed to parse notepad metadata:", e);
|
||||||
root.createDefaultTab()
|
root.createDefaultTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: {
|
onLoadFailed: {
|
||||||
root.createDefaultTab()
|
root.createDefaultTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRefCountChanged: {
|
onRefCountChanged: {
|
||||||
if (refCount === 1 && !metadataLoaded) {
|
if (refCount === 1 && !metadataLoaded) {
|
||||||
metadataFile.path = ""
|
metadataFile.path = "";
|
||||||
metadataFile.path = root.metadataPath
|
metadataFile.path = root.metadataPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDirectories() {
|
function ensureDirectories() {
|
||||||
mkdirProcess.running = true
|
Proc.runCommand("", ["mkdir", "-p", root.baseDir, root.filesDir], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMetadata() {
|
function loadMetadata() {
|
||||||
metadataFile.path = ""
|
metadataFile.path = "";
|
||||||
metadataFile.path = root.metadataPath
|
metadataFile.path = root.metadataPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultTab() {
|
function createDefaultTab() {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var filePath = "notepad-files/untitled-" + id + ".txt"
|
var filePath = "notepad-files/untitled-" + id + ".txt";
|
||||||
var fullPath = baseDir + "/" + filePath
|
var fullPath = baseDir + "/" + filePath;
|
||||||
|
|
||||||
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
newTabsBeingCreated[id] = true
|
newTabsBeingCreated[id] = true;
|
||||||
tabsBeingCreated = newTabsBeingCreated
|
tabsBeingCreated = newTabsBeingCreated;
|
||||||
|
|
||||||
root.createEmptyFile(fullPath, function() {
|
root.createEmptyFile(fullPath, function () {
|
||||||
root.tabs = [{
|
root.tabs = [
|
||||||
|
{
|
||||||
id: id,
|
id: id,
|
||||||
title: I18n.tr("Untitled"),
|
title: I18n.tr("Untitled"),
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@@ -128,14 +132,15 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}]
|
}
|
||||||
root.currentTabIndex = 0
|
];
|
||||||
|
root.currentTabIndex = 0;
|
||||||
|
|
||||||
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
delete updatedTabsBeingCreated[id]
|
delete updatedTabsBeingCreated[id];
|
||||||
tabsBeingCreated = updatedTabsBeingCreated
|
tabsBeingCreated = updatedTabsBeingCreated;
|
||||||
root.saveMetadata()
|
root.saveMetadata();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMetadata() {
|
function saveMetadata() {
|
||||||
@@ -143,82 +148,73 @@ Singleton {
|
|||||||
version: 1,
|
version: 1,
|
||||||
currentTabIndex: currentTabIndex,
|
currentTabIndex: currentTabIndex,
|
||||||
tabs: tabs
|
tabs: tabs
|
||||||
}
|
};
|
||||||
metadataFile.setText(JSON.stringify(metadata, null, 2))
|
metadataFile.setText(JSON.stringify(metadata, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabById(tabId) {
|
function getTabById(tabId) {
|
||||||
for (var i = 0; i < tabs.length; i++) {
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
if (tabs[i].id === tabId)
|
if (tabs[i].id === tabId)
|
||||||
return tabs[i]
|
return tabs[i];
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTabContent(tabIndex, callback) {
|
function loadTabContent(tabIndex, callback) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) {
|
if (tabIndex < 0 || tabIndex >= tabs.length) {
|
||||||
callback("")
|
callback("");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tab = tabs[tabIndex]
|
var tab = tabs[tabIndex];
|
||||||
var requestTabId = tab.id
|
var requestTabId = tab.id;
|
||||||
var fullPath = tab.isTemporary
|
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
|
||||||
? baseDir + "/" + tab.filePath
|
|
||||||
: tab.filePath
|
|
||||||
|
|
||||||
if (tabsBeingCreated[tab.id]) {
|
if (tabsBeingCreated[tab.id]) {
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
loadTabContent(tabIndex, callback)
|
loadTabContent(tabIndex, callback);
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileChecker = fileExistsComponent.createObject(root, {
|
Proc.runCommand("", ["test", "-f", fullPath], (output, exitCode) => {
|
||||||
path: fullPath,
|
var currentTab = root.getTabById(requestTabId);
|
||||||
callback: (exists) => {
|
var currentPath = currentTab ? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath) : "";
|
||||||
var currentTab = root.getTabById(requestTabId)
|
|
||||||
var currentPath = currentTab
|
|
||||||
? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath)
|
|
||||||
: ""
|
|
||||||
|
|
||||||
if (!currentTab || currentPath !== fullPath) {
|
if (!currentTab || currentPath !== fullPath) {
|
||||||
callback("")
|
callback("");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exists) {
|
if (exitCode === 0) {
|
||||||
var loader = tabFileLoaderComponent.createObject(root, {
|
tabFileLoaderComponent.createObject(root, {
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
callback: callback
|
callback: callback
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
log.warn("Tab file does not exist:", fullPath)
|
log.warn("Tab file does not exist:", fullPath);
|
||||||
callback("")
|
callback("");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTabContent(tabIndex, content) {
|
function saveTabContent(tabIndex, content) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var tab = tabs[tabIndex]
|
var tab = tabs[tabIndex];
|
||||||
var fullPath = tab.isTemporary
|
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
|
||||||
? baseDir + "/" + tab.filePath
|
|
||||||
: tab.filePath
|
|
||||||
|
|
||||||
var saver = tabFileSaverComponent.createObject(root, {
|
var saver = tabFileSaverComponent.createObject(root, {
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
content: content,
|
content: content,
|
||||||
tabIndex: tabIndex
|
tabIndex: tabIndex
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewTab() {
|
function createNewTab() {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var filePath = "notepad-files/untitled-" + id + ".txt"
|
var filePath = "notepad-files/untitled-" + id + ".txt";
|
||||||
var fullPath = baseDir + "/" + filePath
|
var fullPath = baseDir + "/" + filePath;
|
||||||
|
|
||||||
var newTab = {
|
var newTab = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -228,29 +224,29 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}
|
};
|
||||||
|
|
||||||
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
newTabsBeingCreated[id] = true
|
newTabsBeingCreated[id] = true;
|
||||||
tabsBeingCreated = newTabsBeingCreated
|
tabsBeingCreated = newTabsBeingCreated;
|
||||||
createEmptyFile(fullPath, function() {
|
createEmptyFile(fullPath, function () {
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
newTabs.push(newTab)
|
newTabs.push(newTab);
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
currentTabIndex = tabs.length - 1
|
currentTabIndex = tabs.length - 1;
|
||||||
|
|
||||||
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
delete updatedTabsBeingCreated[id]
|
delete updatedTabsBeingCreated[id];
|
||||||
tabsBeingCreated = updatedTabsBeingCreated
|
tabsBeingCreated = updatedTabsBeingCreated;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
})
|
});
|
||||||
|
|
||||||
return newTab
|
return newTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTabForFile(path) {
|
function createTabForFile(path) {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var fileName = path.split('/').pop()
|
var fileName = path.split('/').pop();
|
||||||
|
|
||||||
var newTab = {
|
var newTab = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -260,34 +256,34 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}
|
};
|
||||||
|
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
newTabs.push(newTab)
|
newTabs.push(newTab);
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
currentTabIndex = tabs.length - 1
|
currentTabIndex = tabs.length - 1;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
|
||||||
return newTab
|
return newTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTab(tabIndex) {
|
function closeTab(tabIndex) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1
|
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1;
|
||||||
clearSessionBuffer(closedTabId)
|
clearSessionBuffer(closedTabId);
|
||||||
if (conflictTabId === closedTabId)
|
if (conflictTabId === closedTabId)
|
||||||
clearConflict()
|
clearConflict();
|
||||||
|
|
||||||
if (newTabs.length <= 1) {
|
if (newTabs.length <= 1) {
|
||||||
var id = Date.now()
|
var id = Date.now();
|
||||||
var filePath = "notepad-files/untitled-" + id + ".txt"
|
var filePath = "notepad-files/untitled-" + id + ".txt";
|
||||||
|
|
||||||
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
newTabsBeingCreated[id] = true
|
newTabsBeingCreated[id] = true;
|
||||||
tabsBeingCreated = newTabsBeingCreated
|
tabsBeingCreated = newTabsBeingCreated;
|
||||||
createEmptyFile(baseDir + "/" + filePath, function() {
|
createEmptyFile(baseDir + "/" + filePath, function () {
|
||||||
newTabs[0] = {
|
newTabs[0] = {
|
||||||
id: id,
|
id: id,
|
||||||
title: I18n.tr("Untitled"),
|
title: I18n.tr("Untitled"),
|
||||||
@@ -296,110 +292,129 @@ Singleton {
|
|||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
cursorPosition: 0,
|
cursorPosition: 0,
|
||||||
scrollPosition: 0
|
scrollPosition: 0
|
||||||
}
|
};
|
||||||
currentTabIndex = 0
|
currentTabIndex = 0;
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
|
|
||||||
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
|
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
|
||||||
delete updatedTabsBeingCreated[id]
|
delete updatedTabsBeingCreated[id];
|
||||||
tabsBeingCreated = updatedTabsBeingCreated
|
tabsBeingCreated = updatedTabsBeingCreated;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
} else {
|
} else {
|
||||||
var tabToDelete = newTabs[tabIndex]
|
var tabToDelete = newTabs[tabIndex];
|
||||||
if (tabToDelete && tabToDelete.isTemporary) {
|
if (tabToDelete && tabToDelete.isTemporary) {
|
||||||
deleteFile(baseDir + "/" + tabToDelete.filePath)
|
deleteFile(baseDir + "/" + tabToDelete.filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
newTabs.splice(tabIndex, 1)
|
newTabs.splice(tabIndex, 1);
|
||||||
if (currentTabIndex >= newTabs.length) {
|
if (currentTabIndex >= newTabs.length) {
|
||||||
currentTabIndex = newTabs.length - 1
|
currentTabIndex = newTabs.length - 1;
|
||||||
} else if (currentTabIndex > tabIndex) {
|
} else if (currentTabIndex > tabIndex) {
|
||||||
currentTabIndex -= 1
|
currentTabIndex -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToTab(tabIndex) {
|
function switchToTab(tabIndex) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
currentTabIndex = tabIndex
|
currentTabIndex = tabIndex;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderTab(fromIndex, toIndex) {
|
function reorderTab(fromIndex, toIndex) {
|
||||||
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
|
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
|
||||||
return
|
return;
|
||||||
if (fromIndex === toIndex)
|
if (fromIndex === toIndex)
|
||||||
return
|
return;
|
||||||
|
var newTabs = tabs.slice();
|
||||||
var newTabs = tabs.slice()
|
var moved = newTabs.splice(fromIndex, 1)[0];
|
||||||
var moved = newTabs.splice(fromIndex, 1)[0]
|
newTabs.splice(toIndex, 0, moved);
|
||||||
newTabs.splice(toIndex, 0, moved)
|
tabs = newTabs;
|
||||||
tabs = newTabs
|
|
||||||
|
|
||||||
if (currentTabIndex === fromIndex) {
|
if (currentTabIndex === fromIndex) {
|
||||||
currentTabIndex = toIndex
|
currentTabIndex = toIndex;
|
||||||
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
|
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
|
||||||
currentTabIndex--
|
currentTabIndex--;
|
||||||
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
|
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
|
||||||
currentTabIndex++
|
currentTabIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTabAs(tabIndex, userPath) {
|
function saveTabAs(tabIndex, userPath) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var tab = tabs[tabIndex]
|
var tab = tabs[tabIndex];
|
||||||
var fileName = userPath.split('/').pop()
|
var fileName = userPath.split('/').pop();
|
||||||
|
|
||||||
if (tab.isTemporary) {
|
if (tab.isTemporary) {
|
||||||
var tempPath = baseDir + "/" + tab.filePath
|
var tempPath = baseDir + "/" + tab.filePath;
|
||||||
copyFile(tempPath, userPath)
|
copyFile(tempPath, userPath);
|
||||||
deleteFile(tempPath)
|
deleteFile(tempPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
newTabs[tabIndex] = Object.assign({}, tab, {
|
newTabs[tabIndex] = Object.assign({}, tab, {
|
||||||
title: fileName,
|
title: fileName,
|
||||||
filePath: userPath,
|
filePath: userPath,
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
lastModified: new Date().toISOString()
|
lastModified: new Date().toISOString()
|
||||||
})
|
});
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
saveMetadata()
|
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) {
|
function updateTabMetadata(tabIndex, properties) {
|
||||||
if (tabIndex < 0 || tabIndex >= tabs.length) return
|
if (tabIndex < 0 || tabIndex >= tabs.length)
|
||||||
|
return;
|
||||||
var newTabs = tabs.slice()
|
var newTabs = tabs.slice();
|
||||||
var updatedTab = Object.assign({}, newTabs[tabIndex], properties)
|
var updatedTab = Object.assign({}, newTabs[tabIndex], properties);
|
||||||
updatedTab.lastModified = new Date().toISOString()
|
updatedTab.lastModified = new Date().toISOString();
|
||||||
newTabs[tabIndex] = updatedTab
|
newTabs[tabIndex] = updatedTab;
|
||||||
tabs = newTabs
|
tabs = newTabs;
|
||||||
saveMetadata()
|
saveMetadata();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTabs() {
|
function validateTabs() {
|
||||||
var validTabs = []
|
var validTabs = [];
|
||||||
for (var i = 0; i < tabs.length; i++) {
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
var tab = tabs[i]
|
var tab = tabs[i];
|
||||||
validTabs.push(tab)
|
validTabs.push(tab);
|
||||||
}
|
}
|
||||||
tabs = validTabs
|
tabs = validTabs;
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
if (tabs.length === 0) {
|
||||||
root.createDefaultTab()
|
root.createDefaultTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,29 +426,13 @@ Singleton {
|
|||||||
preload: true
|
preload: true
|
||||||
|
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
callback(text())
|
callback(text());
|
||||||
destroy()
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: {
|
onLoadFailed: {
|
||||||
callback("")
|
callback("");
|
||||||
destroy()
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,94 +451,46 @@ Singleton {
|
|||||||
|
|
||||||
onSaved: {
|
onSaved: {
|
||||||
if (tabIndex >= 0) {
|
if (tabIndex >= 0) {
|
||||||
root.updateTabMetadata(tabIndex, {})
|
root.updateTabMetadata(tabIndex, {});
|
||||||
}
|
}
|
||||||
if (creationCallback) {
|
if (creationCallback) {
|
||||||
creationCallback()
|
creationCallback();
|
||||||
}
|
}
|
||||||
destroy()
|
destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaveFailed: {
|
onSaveFailed: {
|
||||||
log.error("Failed to save tab content")
|
log.error("Failed to save tab content");
|
||||||
if (creationCallback) {
|
if (creationCallback) {
|
||||||
creationCallback()
|
creationCallback();
|
||||||
}
|
}
|
||||||
destroy()
|
destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyFile(path, callback) {
|
function createEmptyFile(path, callback) {
|
||||||
var cleanPath = decodeURI(path.toString())
|
var cleanPath = decodeURI(path.toString());
|
||||||
|
|
||||||
if (!cleanPath.startsWith("/")) {
|
if (!cleanPath.startsWith("/")) {
|
||||||
cleanPath = baseDir + "/" + cleanPath
|
cleanPath = baseDir + "/" + cleanPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
var creator = fileCreatorComponent.createObject(root, {
|
Proc.runCommand("", ["touch", cleanPath], (output, exitCode) => {
|
||||||
filePath: cleanPath,
|
if (callback)
|
||||||
creationCallback: callback
|
callback();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyFile(source, destination) {
|
function copyFile(source, destination) {
|
||||||
copyProcess.source = source
|
Proc.runCommand("", ["cp", source, destination], null);
|
||||||
copyProcess.destination = destination
|
|
||||||
copyProcess.running = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFile(path) {
|
function deleteFile(path) {
|
||||||
deleteProcess.filePath = path
|
Proc.runCommand("", ["rm", "-f", path], null);
|
||||||
deleteProcess.running = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
function moveFile(source, destination) {
|
||||||
id: fileCreatorComponent
|
Proc.runCommand("", ["mv", source, destination], null);
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
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() {
|
function openNotepadSlideout() {
|
||||||
|
SessionData.setNotepadLastMode("slideout");
|
||||||
notepadPopout?.hide();
|
notepadPopout?.hide();
|
||||||
if (notepadSlideouts.length > 0) {
|
if (notepadSlideouts.length > 0) {
|
||||||
notepadSlideouts[0]?.show();
|
notepadSlideoutForFocusedScreen()?.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +880,7 @@ Singleton {
|
|||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onNotepadDefaultModeChanged() {
|
function onNotepadDefaultModeChanged() {
|
||||||
|
SessionData.setNotepadLastMode(SettingsData.notepadDefaultMode);
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (SettingsData.notepadDefaultMode === "popout") {
|
||||||
var hadSlideout = false;
|
var hadSlideout = false;
|
||||||
for (var i = 0; i < root.notepadSlideouts.length; i++) {
|
for (var i = 0; i < root.notepadSlideouts.length; i++) {
|
||||||
@@ -879,7 +899,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNotepad() {
|
function openNotepad() {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (notepadResolvedMode === "popout") {
|
||||||
openNotepadPopout();
|
openNotepadPopout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -887,22 +907,22 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeNotepad() {
|
function closeNotepad() {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (notepadResolvedMode === "popout") {
|
||||||
notepadPopout?.hide();
|
notepadPopout?.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (notepadSlideouts.length > 0) {
|
if (notepadSlideouts.length > 0) {
|
||||||
notepadSlideouts[0]?.hide();
|
notepadSlideoutForFocusedScreen()?.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNotepad() {
|
function toggleNotepad() {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (notepadResolvedMode === "popout") {
|
||||||
toggleNotepadPopout();
|
toggleNotepadPopout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (notepadSlideouts.length > 0) {
|
if (notepadSlideouts.length > 0) {
|
||||||
notepadSlideouts[0]?.toggle();
|
notepadSlideoutForFocusedScreen()?.toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +932,7 @@ Singleton {
|
|||||||
property string _notepadPendingOpenFilePath: ""
|
property string _notepadPendingOpenFilePath: ""
|
||||||
|
|
||||||
function openNotepadPopout() {
|
function openNotepadPopout() {
|
||||||
|
SessionData.setNotepadLastMode("popout");
|
||||||
closeNotepadSlideouts();
|
closeNotepadSlideouts();
|
||||||
if (notepadPopout) {
|
if (notepadPopout) {
|
||||||
notepadPopout.show();
|
notepadPopout.show();
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Singleton {
|
|||||||
|
|
||||||
property bool hasUwsm: false
|
property bool hasUwsm: false
|
||||||
property bool isElogind: false
|
property bool isElogind: false
|
||||||
|
property bool loginctlCommandAvailable: false
|
||||||
|
property bool systemctlCommandAvailable: false
|
||||||
property bool hibernateSupported: false
|
property bool hibernateSupported: false
|
||||||
property bool inhibitorAvailable: true
|
property bool inhibitorAvailable: true
|
||||||
property bool idleInhibited: false
|
property bool idleInhibited: false
|
||||||
@@ -52,6 +54,8 @@ Singleton {
|
|||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
detectElogindProcess.running = true;
|
detectElogindProcess.running = true;
|
||||||
|
detectLoginctlProcess.running = true;
|
||||||
|
detectSystemctlProcess.running = true;
|
||||||
detectHibernateProcess.running = true;
|
detectHibernateProcess.running = true;
|
||||||
detectPrimeRunProcess.running = true;
|
detectPrimeRunProcess.running = true;
|
||||||
if (!SettingsData.loginctlLockIntegration) {
|
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 {
|
Process {
|
||||||
id: detectHibernateProcess
|
id: detectHibernateProcess
|
||||||
running: false
|
running: false
|
||||||
@@ -325,9 +349,14 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function powerManagerCommand(action) {
|
||||||
|
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
|
||||||
|
return [useLoginctl ? "loginctl" : "systemctl", action];
|
||||||
|
}
|
||||||
|
|
||||||
function suspend() {
|
function suspend() {
|
||||||
if (SettingsData.customPowerActionSuspend.length === 0) {
|
if (SettingsData.customPowerActionSuspend.length === 0) {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]);
|
Quickshell.execDetached(powerManagerCommand("suspend"));
|
||||||
} else {
|
} else {
|
||||||
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
|
||||||
}
|
}
|
||||||
@@ -338,13 +367,13 @@ Singleton {
|
|||||||
if (SettingsData.customPowerActionHibernate.length > 0) {
|
if (SettingsData.customPowerActionHibernate.length > 0) {
|
||||||
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
|
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
|
||||||
} else {
|
} else {
|
||||||
hibernateProcess.command = [isElogind ? "loginctl" : "systemctl", "hibernate"];
|
hibernateProcess.command = powerManagerCommand("hibernate");
|
||||||
}
|
}
|
||||||
hibernateProcess.running = true;
|
hibernateProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function suspendThenHibernate() {
|
function suspendThenHibernate() {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend-then-hibernate"]);
|
Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function suspendWithBehavior(behavior) {
|
function suspendWithBehavior(behavior) {
|
||||||
@@ -359,7 +388,7 @@ Singleton {
|
|||||||
|
|
||||||
function reboot() {
|
function reboot() {
|
||||||
if (SettingsData.customPowerActionReboot.length === 0) {
|
if (SettingsData.customPowerActionReboot.length === 0) {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]);
|
Quickshell.execDetached(powerManagerCommand("reboot"));
|
||||||
} else {
|
} else {
|
||||||
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]);
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]);
|
||||||
}
|
}
|
||||||
@@ -367,7 +396,7 @@ Singleton {
|
|||||||
|
|
||||||
function poweroff() {
|
function poweroff() {
|
||||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||||
Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"]);
|
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
||||||
} else {
|
} else {
|
||||||
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]);
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user