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

Compare commits

..

78 Commits

Author SHA1 Message Date
bbedward 4af3225f21 network: avoid dropping to legacy service when loading 2026-07-14 22:56:28 -04:00
bbedward 3c5245914f qs: use asynchronous loaders to load shell core 2026-07-14 22:35:58 -04:00
bbedward 89814a2c65 i18n: sync 2026-07-14 22:28:58 -04:00
bbedward 5d2093e54a gamma: fix scheduler losing track during suspend
port 1.5
2026-07-14 22:26:44 -04:00
purian23 3cd52ca327 greeter: update success message 2026-07-14 20:01:06 -04:00
purian23 5b41d699fa fix(greeter): reimplement regression to hide debug logs during login
- Provide more status feedback
- Allow 2 fprint tries within 10 seconds for DMS-managed PAM
- Updated dms greeter docs

Note: distro-managed PAM takes precedence and may use different limits.
DMS auth changes remain conditional on greetd being installed.

Fixes #2853
Port 1.5
2026-07-14 19:41:36 -04:00
bbedward 544599bf1c media: fix track art flash from chrome changing media art sizes on track
change

port 1.5
2026-07-14 18:44:42 -04:00
bbedward 296b3a3d7e mango: add separate dispatch socket
port 1.5
2026-07-14 17:09:34 -04:00
bbedward 729a990fa7 dbar: missing show on overview from search index
port 1.5
2026-07-14 16:35:26 -04:00
bbedward c3fa7b2e1d cava: more optimizations for visualizer
related #2863
2026-07-14 14:29:18 -04:00
bbedward 2c5a1a2804 cava: optimize CPU burn
related #631
2026-07-14 14:16:05 -04:00
bbedward 1973526c4e plugins: prevent churn of daemon plugins by not re-creating the entire map
fixes #2860

port 1.5
2026-07-14 11:46:40 -04:00
Callum Wong bba5502960 Smooth battery time remaining estimate (#2854)
* feat(battery): smooth time remaining estimate with moving average

* feat(battery): use a time-weighted EMA for time remaining estimate

port 1.5
2026-07-14 11:32:48 -04:00
Alexis Corporal 52740290b2 chore: fix indentation in niri media key bindings. (#2858) 2026-07-14 11:26:11 -04:00
bbedward 27703575bc plugins: use grid-style plugin browser
port 1.5
2026-07-14 11:25:10 -04:00
bbedward e6504add7b core/cli: expose QR code CLI
port 1.5
2026-07-14 10:22:17 -04:00
Huỳnh Thiện Lộc f1e9121295 fix(Modal): respect targetScreen property instead of always using focused screen (#2861)
The open() method in both DankModalStandalone and DankModalConnected unconditionally set contentWindow.screen to the focused screen, ignoring the targetScreen property.

Fix: use root.targetScreen ?? CompositorService.getFocusedScreen() so the modal appears on the configured screen when set, and falls back to focused screen otherwise.
2026-07-14 08:42:08 -04:00
bbedward 9ff751b82a dock: fix dock s howing with no apps
port 1.5
2026-07-13 22:47:22 -04:00
Kilian Mio 21eaaef056 Feature/split move size hyprland windowrules (#2824)
* windowrules: add split move/size fields for Lua table syntax

* windowrules: remove deprecated Move/Size fields, switch QML to split fields

* fix: use resolved dms binary path in Proc.runCommand
2026-07-13 18:30:26 -04:00
bbedward bb0be2b215 clipboard: store text alongside image when both are offered
related #2849
port 1.5
2026-07-13 18:23:00 -04:00
bbedward 25847c3f03 config/hyprland: set empty kb layout string by default
fixes #2851
port 1.5
2026-07-13 17:43:09 -04:00
bbedward 7b5c25c50f launcher/spotlight: improve height-change animation
port 1.5
2026-07-13 16:38:40 -04:00
bbedward 7535b70fa6 dash: allow hiding all tabs
port 1.5
2026-07-13 16:11:15 -04:00
bbedward e3034e4e94 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2026-07-13 16:03:56 -04:00
bbedward 3da19e5c15 launcher: add option to choose spotlight style on niri overview 2026-07-13 16:01:08 -04:00
Rafi ca89e12963 core: fix security and concurrency issues found in a backend audit (#2805)
* core: fix security and concurrency issues found in backend audit

Security:
- privesc: pipe the sudo password via stdin (sudo -S) instead of
  embedding it in the command string, so it no longer appears in argv
  (readable by any local user via /proc/<pid>/cmdline or ps)
- greeter: tokenize a session .desktop Exec= line into argv and execve
  directly instead of running it through /bin/sh -c, closing a command-
  injection path via user-writable ~/.local/share/wayland-sessions
- plugins: reject path-separator/.. in plugin id/name before joining
  into a filesystem path, closing an arbitrary-directory-delete in the
  uninstall/update fallback
- keybinds/hyprland: always quote unrecognized bind actions/keys when
  writing generated Lua; only re-emit genuine round-tripped custom Lua
  verbatim (tracked via an explicit flag), closing a Lua-injection path
- desktop/mimeapps: reject newline/bracket in mime/desktop-id fields so
  they can't inject fake sections into the shared mimeapps.list

Robustness / concurrency:
- server: recover panics in the request-dispatch path so one bad
  handler can't crash the daemon and drop every client
- go-wayland: recover panics in the shared dispatch choke point so a
  malformed compositor event can't crash CLI tools / the daemon
- server: per-connection D-Bus client ID instead of a shared constant,
  fixing cross-client signal delivery and subscription teardown
- network: guard the NetworkManager device maps with a mutex (a
  concurrent map read/write here is an unrecoverable fatal error)
- cups: close the event channel on Stop() so Unsubscribe() of the last
  subscriber no longer deadlocks; allocate the fresh channel in Start()
- freedesktop: reuse the shared session conn for the settings watcher
  and tear it down in Close(), fixing a per-Manager conn+goroutine leak
- clipboard: mutex-guard lazy dbusConn creation
- geolocation: use WithMatchMember for the GeoClue2 LocationUpdated
  signal (was WithMatchSender with an interface.member string, so the
  match never fired and live location updates never arrived)
- screenshot: set failed=true on buffer/pool creation errors so the
  dispatch loop doesn't wait forever for a ready/failed that never comes

* apply code review comments

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-07-13 15:44:42 -04:00
arfan 4fb6995796 workspace switcher: fix apps icon won't re focus (#2830)
* fix(workspace): update delegate data on window focus change

* fix(workspace): update delegate data handling specific for hyprland event
2026-07-13 14:59:53 -04:00
Huỳnh Thiện Lộc 3f5a54aa88 fix(dash): resolve pointer cursor hover issue in connected mode (#2845)
* fix(dash): cursor not changing to pointer in Connected Mode (#2831)

Replaces the full-screen background dismissal MouseArea in
DankPopoutConnected.qml with four edge strips that exclude
the popup body. The full-screen MouseArea at z:-1 was
suppressing child cursorShape propagation on Wayland when
combined with the full-screen input mask.

Blame: the connected popout architecture itself (the issue
does not repro in Separate Mode where background dismissal
lives in a separate PanelWindow).

* fix(dash): resolve pointer cursor hover issue in connected mode
2026-07-13 14:58:02 -04:00
Arsenijs Kitajevs d379d251b9 Fixed bluetooth UI bug mentioned in #2627 (#2850) 2026-07-13 14:57:41 -04:00
purian23 7ab0e01573 refactor(dms-updater): remove hidden updates display in popout view 2026-07-13 12:37:04 -04:00
bbedward 2cb48aaf6b listview: workaround delegates overlapping and make spotlight launcher
have a stable anchor

port 1.5
2026-07-13 12:30:49 -04:00
purian23 e4657aa5f9 feat(dms updater): add support for ignoring specific packages during system updates
- Added UI component & popout settings to manage ignored packages
- Added CLI support available in danklinux docs

Fixes: #2827
Closes: #2344, #1741
Port 1.5
2026-07-13 12:02:13 -04:00
bbedward 197d17ac4e launcher: add IPC feature-parity to spotlight-bar and improve list view
transitions

port 1.5
2026-07-13 11:25:01 -04:00
bbedward 31ea83584b i18n: add arabic 2026-07-13 10:46:38 -04:00
bbedward ea66b136ba launcher: don't select pre-filled queries, exclude from memory
port 1.5
2026-07-13 10:31:17 -04:00
bbedward 06c0ea2afb i18n: sync 2026-07-13 09:47:21 -04:00
bbedward f4f47c0bc5 time: add follow-locale option and squash separate greeter time options
port 1.5
2026-07-13 08:53:37 -04:00
purian23 8a0ed8a50f fix(port-audit): update git fetch command to include prune opt
port 1.5
2026-07-13 01:14:33 -04:00
purian23 63eea01243 refactor(debian): prefer native Debian Quickshell stable package support in dankinstall
Port 1.5
2026-07-13 00:39:06 -04:00
purian23 f590a2965a fix(dash-tabs): update visibility logic for dash tab elements
Fixes #2822

Port 1.5
2026-07-12 23:57:29 -04:00
purian23 4ab03deded refactor(settings): allow search settings to remain on sidebar until dismissed
Closes #2780

Port 1.5
2026-07-12 23:30:40 -04:00
sweenu 846d07d86a feat(ipc): add settings dump to print full live config as JSON (#2817)
Adds `dms ipc call settings dump`, returning the complete live
configuration via SettingsData.getCurrentSettingsJson() — the same
serialization used by the read-only banner's copy button.

Useful when settings.json is a read-only symlink (NixOS/home-manager):
changes made in the Settings UI only exist in the running shell's
memory, and this makes them retrievable from the CLI instead of only
through the clipboard button in the Settings modal.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Port 1.5
2026-07-12 22:57:51 -04:00
AiraNadih 55d88d8dfb fix(process-list): defer IPC actions until modal is loaded (#2829)
Port 1.5
2026-07-12 22:57:24 -04:00
Mati7235 a803f96f41 dms doctor: detect missing XDG_MENU_PREFIX when KDE apps are installed (#2840)
* dms doctor: warn when XDG_MENU_PREFIX is missing with KDE apps

Under non-Plasma compositors (niri, Hyprland, Sway), Dolphin's
'Open with...' dialog appears empty because KService/KApplicationTrader
requires a populated XDG menu tree, which depends on XDG_MENU_PREFIX.

This check warns the user when kde-cli-tools (keditfiletype) is
installed but XDG_MENU_PREFIX is not set.

Ref: https://specifications.freedesktop.org/menu-spec/latest/

* fix(dms doctor): improve warning message for missing XDG_MENU_PREFIX

Port 1.5

---------

Co-authored-by: Matias <git@matias7235.com.ar>
2026-07-12 22:54:28 -04:00
A042 da7cc5648b fix: use sleep_monitor/wakeup_monitor for DPMS to preserve monitor layout (#2842)
Since Mango v0.15.0, disable_monitor sets only_sleep=0 which removes the
output from the layout entirely. On NVIDIA (proprietary driver), the DRM
handshake fails on re-enable, leaving a black screen recoverable only via
TTY switch.

sleep_monitor/wakeup_monitor set only_sleep=1, keeping the output in the
layout while powered off. This matches the pre-v0.15 behavior and fixes
DPMS wake on NVIDIA.

These commands are available since Mango v0.15.0 (commit b0326d7).
2026-07-12 18:28:56 -04:00
bbedward 75443758f9 fix(printers): fix add by address with manually entered host
port 1.5
2026-07-12 18:27:51 -04:00
purian23 eaecff93a4 add(workflows): update notify_issues input to release flows
Port 1.5
2026-07-12 16:27:32 -04:00
purian23 6e7c20c59c feat(release): add issue notification script for retesting on new releases
Port 1.5
2026-07-12 15:58:17 -04:00
purian23 406dcfe110 refactor(workflows): update release refs to stable in CI configurations
Port 1.5
2026-07-12 15:23:04 -04:00
Huỳnh Thiện Lộc e2b3a2e3ca feat(media): add toggle for album art accent colors (#2831) (#2832)
* fix(media): use system colours in player instead of album cover accent

Closes #2831

Removes ColorQuantizer-based album art accent extraction from
MediaAccentService. All accent properties now return Theme.primary
and Theme.onPrimary directly, so the Dank Dash player always
matches the system colour scheme.

Blame: ee6f7b47 (introduced MediaAccentService & ColorQuantizer)
       d799175c (tweaked seekbar accent colours)
       a62ae336 (further integrated accent into player/album art)
       c44ffae7 (monochrome art edge case fixes)

* feat(media): add toggle for album art accent colours (#2831)

Adds mediaUseAlbumArtAccent setting (default: off) to Settings.
When enabled, MediaAccentService extracts accent from album art
via ColorQuantizer. When disabled, uses Theme.primary.
Toggle is in Settings > Media Player.

* fix: use american english spelling (colour -> color)

Port 1.5
2026-07-11 22:29:35 -04:00
purian23 71ab752e1b fix(void): updated dms-greeter stability support
- Updates will land in DMS v1.5.1 and users are advised to hold off on v1.5.0

Related #2788

Port 1.5
2026-07-11 01:07:00 -04:00
purian23 204ecd0461 feat(void): switch package repositories to R2 2026-07-10 18:14:13 -04:00
purian23 0fdfac366e ci(void): publish XBPS repository to R2 2026-07-10 17:57:28 -04:00
bbedward 52123458c7 widgets: fix dropdown/icon picker referencing a dead window
port 1.5
2026-07-10 17:21:10 -04:00
Artem Timofeev 26b2955cf3 fix(notifications): dismiss popups when senders close notifications (#2815)
When a sender calls CloseNotification (per freedesktop spec) for a
persistent notification (expire-timeout=0), the onDropped handler
removed the wrapper from internal arrays but did not dismiss the
visible popup. The popup remained on screen forever for non-critical
notifications like YubiKey touch prompts.

Set wrapper.popup=false in onDropped so the popup exits via the
normal signal chain. This is minimal; it does not change timeout
handling, queue management, or the isPersistent marker.

Also reverts the previous workaround that special-cased non-critical
expireTimeout=0 to use DMS timeout. Upstream spec-correct behavior
is to honor the sender timeout, and fix the dismissal bug instead.

Closes #2814
2026-07-10 17:08:03 -04:00
bbedward d82d86df5c theme: prevent failed portal writes from reverting theme mode
related: #2786

port 1.5
2026-07-10 15:44:36 -04:00
Kangheng Liu c445597f83 fix(player): combine trackid with text identity (#2808)
fixes #2807
2026-07-10 13:37:16 -04:00
bbedward 6a58adfb29 osd/media playback: delay showing until album art is ready and avoid
re-showing when duplicate metadata comes from mpris
related #2787
port 1.5
2026-07-10 13:35:51 -04:00
bbedward 05feb211ba audio: only show mic volume OSD through DMS IPCs, not external source
changes
fixes #2790
port 1.5
2026-07-10 12:43:40 -04:00
purian23 a3b2167e58 workflow: enhance porting logic to support inline verbiage & audits 2026-07-10 11:57:36 -04:00
bbedward 0b69feaa1a settings: restore lost geometric centering option
port 1.5
2026-07-10 11:12:08 -04:00
purian23 494144a7c7 workflow: updated target extraction regex options 2026-07-10 11:09:05 -04:00
bbedward 2a2c1ca9e6 calendar: add action button to events for opening links
fixes #2799
port 1.5
2026-07-10 11:02:25 -04:00
bbedward 56b7ecb008 qs/common: fix path decoding
fixes #2802
port 1.5
2026-07-10 10:49:50 -04:00
bbedward 867102b82c network/iwd: improve bad credential handling
related #2804

port: 1.5
2026-07-10 10:36:22 -04:00
Scott McKendry 4bdb7d17b2 fix(settings): battery tab items not in search (#2794) 2026-07-10 10:17:51 -04:00
bbedward c44ffae751 fix(media): resolve monochrome album art accents
port: 1.5
2026-07-09 19:40:21 -04:00
14Do ce1595d62d fix(ListViewTransitions): null transitions when duration truncates to 0ms (#2791)
A zero-duration ViewTransition still engages ListView's transition
machinery but resolves within the same frame, so removed delegates are
never released and displaced items are never repositioned. On a filtered
ScriptModel that re-filters on every keystroke this leaves stale rows and
empty gaps behind, visible in any DankListView-backed list.

The shortest sub-duration (remove/add = expressiveDurations.fast =
base * 0.4) is coerced to an int, so it truncates to 0ms for any
animation base duration below 3: animation speed None (0), or a Custom
duration of 1-2ms (1*0.4=0.4->0, 2*0.4=0.8->0, only 3*0.4=1.2->1
survives). Presets (250/500/750) are unaffected, which is why this only
reproduces with animations disabled or a very small custom value.

Gate the four transitions to null whenever that shortest sub-duration
would truncate to 0, so the view takes the correct instant path instead
of a broken 0ms transition. Base >= 3 is unchanged.
2026-07-09 18:47:26 -04:00
Evan Maddock 45f6232e32 build: Add support for DESTDIR (#2783)
This makes it easier for distros to use the Makefile when creating
packages. It enables us to specify a base destination directory to
install the project files to. E.g., on Solus, when creating eopkgs, files
must be installed to a special directory path, which becomes the package
files. I believe Fedora packages, and others, are the same.

Signed-off-by: Evan Maddock <maddock.evan@vivaldi.net>
2026-07-09 18:39:06 -04:00
bbedward 9cf2ca7196 auth: add some more intelligent pam config resolution for lock screen
and greeter
related #2789

port: 1.5
2026-07-09 15:07:22 -04:00
purian23 c0eeed4e89 workflows: update deps
port/1.5
2026-07-09 12:53:57 -04:00
bbedward d0a4c1c56e default apps: add configuration for geo: URIs
port: 1.5
2026-07-09 12:23:44 -04:00
purian23 cb0dc9c68d workflow: update porting logic 2026-07-09 12:02:14 -04:00
bbedward 0439d017b9 fix(notifications): handle sound-name hint 2026-07-09 12:00:36 -04:00
purian23 8008238ca0 Add GitHub workflows for release management 2026-07-09 11:26:30 -04:00
bbedward a095d0ed90 changelog: disable 2026-07-08 16:42:30 -04:00
purian23 a48cce59d4 fix(workflow): void stable packages 2026-07-08 13:52:12 -04:00
bbedward 4c806f83f1 bump to 1.6-beta 2026-07-08 13:10:39 -04:00
42 changed files with 1181 additions and 2093 deletions
-100
View File
@@ -1,10 +1,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -59,107 +56,10 @@ var authResolveLockCmd = &cobra.Command{
}, },
} }
var authListServicesCmd = &cobra.Command{
Use: "list-services",
Short: "List candidate lock-screen PAM services available on this system",
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
Run: func(cmd *cobra.Command, args []string) {
asJSON, _ := cmd.Flags().GetBool("json")
services := sharedpam.ListLockscreenPamServices()
if asJSON {
payload := struct {
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
}{Services: services}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("Error encoding services: %v", err)
}
fmt.Println(string(data))
return
}
if len(services) == 0 {
fmt.Println("No candidate lock-screen PAM services found.")
return
}
for _, s := range services {
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
}
},
}
var authValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate a PAM service file for use as the DMS lock-screen password stack",
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password stack. Exits 1 when the file is not usable.",
Run: func(cmd *cobra.Command, args []string) {
path, _ := cmd.Flags().GetString("path")
service, _ := cmd.Flags().GetString("service")
asJSON, _ := cmd.Flags().GetBool("json")
if (path == "") == (service == "") {
log.Fatalf("Error: exactly one of --path or --service is required")
}
var result sharedpam.LockscreenPamValidation
switch {
case service != "":
result = sharedpam.ValidateLockscreenPamService(service)
case !filepath.IsAbs(path):
result = sharedpam.LockscreenPamValidation{
Path: path,
MissingModules: []string{},
Warnings: []string{},
Errors: []string{"--path must be an absolute file path"},
}
default:
result = sharedpam.ValidateLockscreenPamPath(path)
}
if asJSON {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("Error encoding validation: %v", err)
}
fmt.Println(string(data))
} else {
printLockscreenPamValidation(result)
}
if !result.Valid {
os.Exit(1)
}
},
}
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
fmt.Printf("Path: %s\n", result.Path)
fmt.Printf("Valid: %t\n", result.Valid)
fmt.Printf("Has auth: %t\n", result.HasAuth)
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
if len(result.MissingModules) > 0 {
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
}
for _, w := range result.Warnings {
fmt.Println("⚠ " + w)
}
for _, e := range result.Errors {
fmt.Println("✗ " + e)
}
}
func init() { func init() {
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts") authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)") authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path") authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
} }
func syncAuth(nonInteractive bool) error { func syncAuth(nonInteractive bool) error {
+10 -69
View File
@@ -2,7 +2,6 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -28,19 +27,8 @@ var (
ssNoConfirm bool ssNoConfirm bool
ssReset bool ssReset bool
ssStdout bool ssStdout bool
ssJSON bool
) )
type screenshotMetadata struct {
Status string `json:"status"`
Path string `json:"path,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Scale float64 `json:"scale,omitempty"`
Mime string `json:"mime,omitempty"`
Error string `json:"error,omitempty"`
}
var screenshotCmd = &cobra.Command{ var screenshotCmd = &cobra.Command{
Use: "screenshot", Use: "screenshot",
Short: "Capture screenshots", Short: "Capture screenshots",
@@ -71,8 +59,7 @@ Examples:
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --no-confirm # Region capture on mouse release dms screenshot --no-confirm # Region capture on mouse release
dms screenshot --cursor=on # Include cursor dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85 dms screenshot -f jpg -q 85 # JPEG with quality 85`,
dms screenshot --json # Print capture metadata as JSON`,
} }
var ssRegionCmd = &cobra.Command{ var ssRegionCmd = &cobra.Command{
@@ -141,7 +128,6 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation") screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing") screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
screenshotCmd.AddCommand(ssRegionCmd) screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssFullCmd) screenshotCmd.AddCommand(ssFullCmd)
@@ -217,36 +203,7 @@ func setPopoutScreenshotMode(begin bool) {
_ = exec.Command("qs", cmdArgs...).Run() _ = exec.Command("qs", cmdArgs...).Run()
} }
func writeScreenshotJSON(meta screenshotMetadata) {
_ = json.NewEncoder(os.Stdout).Encode(meta)
}
func exitScreenshotError(context string, err error) {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()})
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err)
os.Exit(1)
}
func formatMime(format screenshot.Format) string {
switch format {
case screenshot.FormatJPEG:
return "image/jpeg"
case screenshot.FormatPPM:
return "image/x-portable-pixmap"
default:
return "image/png"
}
}
func runScreenshot(config screenshot.Config) { func runScreenshot(config screenshot.Config) {
if ssJSON && config.Stdout {
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
os.Exit(1)
}
// Region select needs the keyboard; drop popout grabs for its duration. // Region select needs the keyboard; drop popout grabs for its duration.
result, err := func() (*screenshot.CaptureResult, error) { result, err := func() (*screenshot.CaptureResult, error) {
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion
@@ -258,13 +215,11 @@ func runScreenshot(config screenshot.Config) {
}() }()
if err != nil { if err != nil {
exitScreenshotError("", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
if result == nil { if result == nil {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
}
os.Exit(0) os.Exit(0)
} }
@@ -276,7 +231,8 @@ func runScreenshot(config screenshot.Config) {
if config.Stdout { if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil { if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
exitScreenshotError(" writing to stdout", err) fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
os.Exit(1)
} }
return return
} }
@@ -296,37 +252,22 @@ func runScreenshot(config screenshot.Config) {
filePath = filepath.Join(outputDir, filename) filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil { if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
exitScreenshotError(" writing file", err) fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
os.Exit(1)
} }
if !ssJSON {
fmt.Println(filePath) fmt.Println(filePath)
} }
}
if config.Clipboard { if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil { if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
exitScreenshotError(" copying to clipboard", err) fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
os.Exit(1)
} }
if !ssJSON && !config.SaveFile { if !config.SaveFile {
fmt.Println("Copied to clipboard") fmt.Println("Copied to clipboard")
} }
} }
if ssJSON {
scale := result.Scale
if scale <= 0 {
scale = 1.0
}
writeScreenshotJSON(screenshotMetadata{
Status: "success",
Path: filePath,
Width: result.Buffer.Width,
Height: result.Buffer.Height,
Scale: scale,
Mime: formatMime(config.Format),
})
}
if config.Notify { if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format) thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{ screenshot.SendNotification(screenshot.NotifyResult{
+1 -1
View File
@@ -20,7 +20,7 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
+1 -1
View File
@@ -20,7 +20,7 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
-263
View File
@@ -74,7 +74,6 @@ type AuthSettings struct {
EnableU2f bool `json:"enableU2f"` EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"` GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"` GreeterEnableU2f bool `json:"greeterEnableU2f"`
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
} }
type SyncAuthOptions struct { type SyncAuthOptions struct {
@@ -237,14 +236,6 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err) return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
} }
if settings.GreeterPamExternallyManaged {
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
return err
}
logFunc(" /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
return nil
}
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil { if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
return err return err
} }
@@ -592,260 +583,6 @@ func buildManagedLockscreenPamContent(baseDirs []string, readFile func(string) (
return b.String(), nil return b.String(), nil
} }
var lockscreenPamCandidateServices = []string{
"login",
"system-auth",
"system-login",
"system-local-login",
"common-auth",
"base-auth",
}
type LockscreenPamServiceInfo struct {
Name string `json:"name"`
Dir string `json:"dir"`
Path string `json:"path"`
HasAuth bool `json:"hasAuth"`
InlineFingerprint bool `json:"inlineFingerprint"`
InlineU2f bool `json:"inlineU2f"`
}
type LockscreenPamValidation struct {
Valid bool `json:"valid"`
Path string `json:"path"`
HasAuth bool `json:"hasAuth"`
InlineFingerprint bool `json:"inlineFingerprint"`
InlineU2f bool `json:"inlineU2f"`
MissingModules []string `json:"missingModules"`
Warnings []string `json:"warnings"`
Errors []string `json:"errors"`
}
type lockscreenPamValidateDeps struct {
baseDirs []string
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
pamModuleExists func(string) bool
}
func defaultValidateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: lockscreenPamBaseDirs,
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: pamModuleExists,
}
}
// lockscreenPamAnalysis is a non-destructive walk of a PAM service. Unlike
// resolveService it detects (rather than strips) pam_fprintd/pam_u2f and
// records unknown directives instead of hard-failing on them.
type lockscreenPamAnalysis struct {
lines []string
hasAuth bool
inlineFingerprint bool
inlineU2f bool
modules []string
unknownDirectives []string
err error
}
func (r lockscreenPamResolver) analyzePath(path string) lockscreenPamAnalysis {
var acc lockscreenPamAnalysis
if err := r.analyzeInto(filepath.Clean(path), "", nil, &acc); err != nil {
acc.err = err
}
return acc
}
func (r lockscreenPamResolver) analyzeInto(path string, filterType string, stack []string, acc *lockscreenPamAnalysis) error {
for _, seen := range stack {
if seen == path {
chain := append(append([]string{}, stack...), path)
display := make([]string, 0, len(chain))
for _, item := range chain {
display = append(display, filepath.Base(item))
}
return fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
}
}
data, err := r.readFile(path)
if err != nil {
return fmt.Errorf("failed to read PAM file %s: %w", path, err)
}
for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
rawLine = strings.TrimRight(rawLine, "\r")
trimmed := strings.TrimSpace(rawLine)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
lineType := pamDirectiveType(trimmed)
if filterType != "" && lineType != "" && lineType != filterType {
continue
}
nestedPath := include.target
if filepath.IsAbs(nestedPath) {
nestedPath = filepath.Clean(nestedPath)
} else {
located, err := r.locate(include.target)
if err != nil {
return fmt.Errorf("failed to read PAM file %s: %w", include.target, err)
}
nestedPath = located
}
if err := r.analyzeInto(nestedPath, include.filterType, append(stack, path), acc); err != nil {
return err
}
continue
}
lineType := pamDirectiveType(trimmed)
if lineType == "" {
acc.unknownDirectives = append(acc.unknownDirectives, trimmed)
continue
}
if filterType != "" && lineType != filterType {
continue
}
acc.lines = append(acc.lines, rawLine)
if lineType == "auth" {
acc.hasAuth = true
}
foundModule := false
for _, field := range strings.Fields(trimmed) {
if strings.HasPrefix(field, "#") {
break
}
if strings.Contains(field, "pam_fprintd") {
acc.inlineFingerprint = true
}
if strings.Contains(field, "pam_u2f") {
acc.inlineU2f = true
}
if !foundModule && strings.HasSuffix(field, ".so") {
acc.modules = append(acc.modules, field)
foundModule = true
}
}
}
return nil
}
// Earlier base dir wins per name (libpam precedence).
func ListLockscreenPamServices() []LockscreenPamServiceInfo {
return listLockscreenPamServices(lockscreenPamBaseDirs, os.ReadFile)
}
func listLockscreenPamServices(baseDirs []string, readFile func(string) ([]byte, error)) []LockscreenPamServiceInfo {
resolver := lockscreenPamResolver{baseDirs: baseDirs, readFile: readFile}
out := make([]LockscreenPamServiceInfo, 0, len(lockscreenPamCandidateServices))
for _, name := range lockscreenPamCandidateServices {
path, err := resolver.locate(name)
if err != nil {
continue
}
info := LockscreenPamServiceInfo{
Name: name,
Dir: filepath.Dir(path),
Path: path,
}
if analysis := resolver.analyzePath(path); analysis.err == nil {
info.HasAuth = analysis.hasAuth
info.InlineFingerprint = analysis.inlineFingerprint
info.InlineU2f = analysis.inlineU2f
}
out = append(out, info)
}
return out
}
func ValidateLockscreenPamService(name string) LockscreenPamValidation {
return validateLockscreenPam(name, "", defaultValidateDeps())
}
func ValidateLockscreenPamPath(path string) LockscreenPamValidation {
return validateLockscreenPam("", path, defaultValidateDeps())
}
func validateLockscreenPam(serviceName string, path string, deps lockscreenPamValidateDeps) LockscreenPamValidation {
result := LockscreenPamValidation{
MissingModules: []string{},
Warnings: []string{},
Errors: []string{},
}
resolver := lockscreenPamResolver{baseDirs: deps.baseDirs, readFile: deps.readFile}
var analysis lockscreenPamAnalysis
if path != "" {
result.Path = path
analysis = resolver.analyzePath(path)
} else {
located, err := resolver.locate(serviceName)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("PAM service %q not found: %v", serviceName, err))
return result
}
result.Path = located
analysis = resolver.analyzePath(located)
}
if analysis.err != nil {
result.Errors = append(result.Errors, analysis.err.Error())
return result
}
result.HasAuth = analysis.hasAuth
result.InlineFingerprint = analysis.inlineFingerprint
result.InlineU2f = analysis.inlineU2f
if !analysis.hasAuth {
result.Errors = append(result.Errors, "no auth directives found after include resolution")
}
for _, directive := range analysis.unknownDirectives {
result.Warnings = append(result.Warnings, "unsupported PAM directive (libpam may still handle it at runtime): "+directive)
}
seen := map[string]bool{}
for _, ref := range analysis.modules {
name := filepath.Base(ref)
if seen[name] {
continue
}
seen[name] = true
if moduleReferenceExists(ref, deps) {
continue
}
result.MissingModules = append(result.MissingModules, name)
result.Warnings = append(result.Warnings, "referenced PAM module not found: "+name)
}
if analysis.inlineFingerprint {
result.Warnings = append(result.Warnings, "pam_fprintd is present in the resolved stack; may double-prompt with DMS's separate fingerprint context")
}
if analysis.inlineU2f {
result.Warnings = append(result.Warnings, "pam_u2f is present in the resolved stack; may double-prompt with DMS's separate U2F context")
}
result.Valid = len(result.Errors) == 0
return result
}
func moduleReferenceExists(ref string, deps lockscreenPamValidateDeps) bool {
if filepath.IsAbs(ref) {
_, err := deps.stat(ref)
return err == nil
}
return deps.pamModuleExists(ref)
}
const UserLockscreenPamService = "dankshell" const UserLockscreenPamService = "dankshell"
func UserLockscreenPamDir() string { func UserLockscreenPamDir() string {
-250
View File
@@ -786,223 +786,6 @@ func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
} }
} }
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestListLockscreenPamServices(t *testing.T) {
t.Parallel()
t.Run("dedupes by name with earlier base dir winning", func(t *testing.T) {
t.Parallel()
etcDir := t.TempDir()
vendorDir := t.TempDir()
// login exists in both dirs; system-auth only in the vendor dir.
writeTestFile(t, filepath.Join(etcDir, "login"), "#%PAM-1.0\nauth required pam_unix.so\naccount required pam_unix.so\n")
writeTestFile(t, filepath.Join(vendorDir, "login"), "#%PAM-1.0\nauth required pam_deny.so\n")
writeTestFile(t, filepath.Join(vendorDir, "system-auth"), "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n")
services := listLockscreenPamServices([]string{etcDir, vendorDir}, os.ReadFile)
if len(services) != 2 {
t.Fatalf("expected 2 services (login, system-auth), got %d: %+v", len(services), services)
}
byName := map[string]LockscreenPamServiceInfo{}
for _, s := range services {
byName[s.Name] = s
}
login, ok := byName["login"]
if !ok {
t.Fatalf("expected login service, got %+v", services)
}
if login.Dir != etcDir || login.Path != filepath.Join(etcDir, "login") {
t.Fatalf("expected login to resolve in earlier dir %s, got dir=%s path=%s", etcDir, login.Dir, login.Path)
}
if !login.HasAuth {
t.Fatalf("expected login to report hasAuth")
}
if byName["system-auth"].Dir != vendorDir {
t.Fatalf("expected system-auth to resolve in vendor dir, got %s", byName["system-auth"].Dir)
}
})
t.Run("include resolution sets hasAuth and detects inline fprintd/u2f", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\nauth sufficient pam_fprintd.so\nauth sufficient pam_u2f.so cue\naccount required pam_unix.so\n")
services := listLockscreenPamServices([]string{env.pamDir}, os.ReadFile)
var login LockscreenPamServiceInfo
for _, s := range services {
if s.Name == "login" {
login = s
}
}
if !login.HasAuth {
t.Fatalf("expected hasAuth via resolved include")
}
if !login.InlineFingerprint || !login.InlineU2f {
t.Fatalf("expected inline fingerprint and u2f detection, got %+v", login)
}
})
}
func TestValidateLockscreenPam(t *testing.T) {
t.Parallel()
t.Run("valid service with resolved auth", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("login", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid result, got %+v", result)
}
if !result.HasAuth {
t.Fatalf("expected hasAuth true")
}
if len(result.Errors) != 0 {
t.Fatalf("expected no errors, got %v", result.Errors)
}
})
t.Run("path outside base dirs is read directly", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
outside := filepath.Join(t.TempDir(), "custom-pam")
writeTestFile(t, outside, "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("", outside, env.validateDeps())
if !result.Valid || result.Path != outside {
t.Fatalf("expected valid result for outside path, got %+v", result)
}
})
t.Run("missing module produces warning and missingModules but stays valid", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nauth required pam_absent.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid despite missing module, got %+v", result)
}
if len(result.MissingModules) != 1 || result.MissingModules[0] != "pam_absent.so" {
t.Fatalf("expected missing pam_absent.so, got %v", result.MissingModules)
}
if !containsSubstr(result.Warnings, "pam_absent.so") {
t.Fatalf("expected warning about missing module, got %v", result.Warnings)
}
})
t.Run("unknown directive is a warning not an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.availableModules["pam_foo.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nbadtype required pam_foo.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid with unknown directive, got %+v", result)
}
if len(result.Errors) != 0 {
t.Fatalf("expected no errors, got %v", result.Errors)
}
if !containsSubstr(result.Warnings, "unsupported PAM directive") {
t.Fatalf("expected unsupported directive warning, got %v", result.Warnings)
}
})
t.Run("cyclic include is an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\n")
env.writePamFile(t, "system-auth", "auth include login\n")
result := validateLockscreenPam("login", "", env.validateDeps())
if result.Valid {
t.Fatalf("expected invalid on cyclic include, got %+v", result)
}
if !containsSubstr(result.Errors, "cyclic PAM include detected") {
t.Fatalf("expected cyclic include error, got %v", result.Errors)
}
})
t.Run("no auth directives is an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if result.Valid {
t.Fatalf("expected invalid when no auth directives, got %+v", result)
}
if !containsSubstr(result.Errors, "no auth directives") {
t.Fatalf("expected no-auth error, got %v", result.Errors)
}
})
t.Run("missing file is an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
result := validateLockscreenPam("", filepath.Join(env.pamDir, "does-not-exist"), env.validateDeps())
if result.Valid || len(result.Errors) == 0 {
t.Fatalf("expected invalid for missing file, got %+v", result)
}
})
t.Run("inline fingerprint and u2f produce warnings", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nauth sufficient pam_fprintd.so\nauth sufficient pam_u2f.so cue\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid, got %+v", result)
}
if !result.InlineFingerprint || !result.InlineU2f {
t.Fatalf("expected inline flags set, got %+v", result)
}
if !containsSubstr(result.Warnings, "pam_fprintd") || !containsSubstr(result.Warnings, "pam_u2f") {
t.Fatalf("expected double-prompt warnings, got %v", result.Warnings)
}
})
}
func containsSubstr(items []string, substr string) bool {
for _, item := range items {
if strings.Contains(item, substr) {
return true
}
}
return false
}
func TestSyncAuthConfigWithDeps(t *testing.T) { func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1065,39 +848,6 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
} }
}) })
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":true}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
GreeterPamManagedBlockEnd+"\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
greetd := readFileString(t, env.greetdPath)
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
}
if !strings.Contains(greetd, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
}
if !containsSubstr(logs, "externally managed") {
t.Fatalf("expected externally-managed skip log, got %v", logs)
}
})
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) { t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
t.Parallel() t.Parallel()
-5
View File
@@ -178,13 +178,9 @@ func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
yInverted := false yInverted := false
var format uint32 var format uint32
scale := 1.0
if r.selection.surface != nil { if r.selection.surface != nil {
yInverted = r.selection.surface.yInverted yInverted = r.selection.surface.yInverted
format = r.selection.surface.screenFormat format = r.selection.surface.screenFormat
if s := r.selection.surface.output.fractionalScale; s > 0 {
scale = s
}
} }
return &CaptureResult{ return &CaptureResult{
@@ -192,7 +188,6 @@ func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
Region: r.result, Region: r.result,
YInverted: yInverted, YInverted: yInverted,
Format: format, Format: format,
Scale: scale,
}, false, nil }, false, nil
} }
+21 -28
View File
@@ -28,21 +28,6 @@ type CaptureResult struct {
Region Region Region Region
YInverted bool YInverted bool
Format uint32 Format uint32
Scale float64
}
func (o *WaylandOutput) effectiveScale() float64 {
scale := o.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(o.name)
}
if scale <= 0 {
scale = float64(o.scale)
}
if scale <= 0 {
return 1.0
}
return scale
} }
type Screenshoter struct { type Screenshoter struct {
@@ -270,7 +255,6 @@ func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region,
Region: region, Region: region,
YInverted: false, YInverted: false,
Format: result.Format, Format: result.Format,
Scale: scale,
}, nil }, nil
} }
@@ -446,7 +430,6 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
Buffer: composite, Buffer: composite,
Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(totalH)}, Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(totalH)},
Format: format, Format: format,
Scale: maxScale,
}, nil }, nil
} }
@@ -519,7 +502,6 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
if err != nil { if err != nil {
return nil, err return nil, err
} }
result.Scale = output.effectiveScale()
if result.YInverted { if result.YInverted {
result.Buffer.FlipVertical() result.Buffer.FlipVertical()
@@ -622,7 +604,6 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
Region: region, Region: region,
YInverted: false, YInverted: false,
Format: result.Format, Format: result.Format,
Scale: scale,
}, nil }, nil
} }
@@ -631,7 +612,16 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return s.captureRegionOnTransformedOutput(output, region) return s.captureRegionOnTransformedOutput(output, region)
} }
scale := output.effectiveScale() scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int32(float64(region.X-output.x) * scale) localX := int32(float64(region.X-output.x) * scale)
localY := int32(float64(region.Y-output.y) * scale) localY := int32(float64(region.Y-output.y) * scale)
@@ -670,12 +660,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return nil, fmt.Errorf("capture region: %w", err) return nil, fmt.Errorf("capture region: %w", err)
} }
result, err := s.processFrame(frame, region) return s.processFrame(frame, region)
if err != nil {
return nil, err
}
result.Scale = scale
return result, nil
} }
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
@@ -684,7 +669,16 @@ func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, r
return nil, err return nil, err
} }
scale := output.effectiveScale() scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-output.x) * scale) localX := int(float64(region.X-output.x) * scale)
localY := int(float64(region.Y-output.y) * scale) localY := int(float64(region.Y-output.y) * scale)
@@ -736,7 +730,6 @@ func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, r
Region: region, Region: region,
YInverted: false, YInverted: false,
Format: result.Format, Format: result.Format,
Scale: scale,
}, nil }, nil
} }
+1 -1
View File
@@ -1 +1 @@
The Wolverine Marble Tabby
-4
View File
@@ -895,10 +895,6 @@ Singleton {
readonly property bool greeterU2fReady: Processes.greeterU2fReady readonly property bool greeterU2fReady: Processes.greeterU2fReady
readonly property string greeterU2fReason: Processes.greeterU2fReason readonly property string greeterU2fReason: Processes.greeterU2fReason
readonly property string greeterU2fSource: Processes.greeterU2fSource readonly property string greeterU2fSource: Processes.greeterU2fSource
property string lockPamPath: ""
property bool lockPamInlineFprint: false
property bool lockPamInlineU2f: false
property bool greeterPamExternallyManaged: false
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
property bool lockScreenVideoEnabled: false property bool lockScreenVideoEnabled: false
@@ -233,7 +233,6 @@ var SPEC = {
greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" }, greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" },
greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" }, greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" },
greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" }, greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" },
greeterPamExternallyManaged: { def: false, onChange: "markGreeterSyncPending" },
greeterSyncPending: { def: false }, greeterSyncPending: { def: false },
greeterSyncBaseline: { def: {} }, greeterSyncBaseline: { def: {} },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
@@ -456,9 +455,6 @@ var SPEC = {
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
enableU2f: { def: false, onChange: "scheduleAuthApply" }, enableU2f: { def: false, onChange: "scheduleAuthApply" },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
lockPamPath: { def: "" },
lockPamInlineFprint: { def: false },
lockPamInlineU2f: { def: false },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
lockScreenVideoEnabled: { def: false }, lockScreenVideoEnabled: { def: false },
+1 -1
View File
@@ -213,7 +213,7 @@ Item {
} }
Component.onCompleted: { Component.onCompleted: {
dockEnabled = true; dockRecreateDebounce.start();
loginSoundTimer.start(); loginSoundTimer.start();
osdStartupTimer.start(); osdStartupTimer.start();
+3 -1
View File
@@ -531,7 +531,9 @@ Item {
} }
function next(): void { function next(): void {
MprisController.next(); if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
MprisController.activePlayer.next();
}
} }
function stop(): void { function stop(): void {
@@ -33,8 +33,6 @@ Variants {
color: "transparent" color: "transparent"
visible: root.contentReady && !root.surfaceBounce
updatesEnabled: root.renderActive || root._settleFrames > 0 updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
@@ -58,8 +56,6 @@ Variants {
property string source: SessionData.getMonitorWallpaper(modelData.name) || "" property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
property bool isColorSource: source.startsWith("#") property bool isColorSource: source.startsWith("#")
property bool contentReady: false
property bool surfaceBounce: false
Connections { Connections {
target: SessionData target: SessionData
@@ -98,9 +94,6 @@ Variants {
Component.onCompleted: { Component.onCompleted: {
isInitialized = true; isInitialized = true;
if (!source || isColorSource) {
contentReady = true;
}
} }
property bool isInitialized: false property bool isInitialized: false
@@ -143,9 +136,9 @@ Variants {
interval: 0 interval: 0
repeat: false repeat: false
onTriggered: { onTriggered: {
root.surfaceBounce = true; blurWallpaperWindow.visible = false;
Qt.callLater(() => { Qt.callLater(() => {
root.surfaceBounce = false; blurWallpaperWindow.visible = true;
}); });
} }
} }
@@ -253,8 +246,6 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
if (!newSource)
root.contentReady = true;
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -327,9 +318,6 @@ Variants {
if (status === Image.Error) { if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source); log.warn("failed to load active wallpaper for", modelData.name + ":", source);
} }
if (status === Image.Ready || status === Image.Error) {
root.contentReady = true;
}
} }
} }
@@ -44,9 +44,7 @@ PluginComponent {
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2 implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Column { Column {
id: detailColumn id: detailColumn
@@ -134,11 +132,7 @@ PluginComponent {
width: connButtonRow.implicitWidth + Theme.spacingM * 2 width: connButtonRow.implicitWidth + Theme.spacingM * 2
readonly property bool isConnected: TailscaleService.connected readonly property bool isConnected: TailscaleService.connected
color: { color: isConnected ? (connButtonArea.containsMouse ? Theme.errorHover : Theme.surfaceLight) : (connButtonArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
if (!connButtonArea.containsMouse)
return Theme.surfaceLight;
return isConnected ? Theme.errorHover : Theme.primaryHoverLight;
}
Row { Row {
id: connButtonRow id: connButtonRow
@@ -336,15 +330,13 @@ PluginComponent {
required property var modelData required property var modelData
required property int index required property int index
readonly property bool isSelf: modelData.hostname === (TailscaleService.selfNode ? TailscaleService.selfNode.hostname : "")
readonly property bool isExpanded: detailRoot.expandedHostname === modelData.hostname
width: peerListColumn.width width: peerListColumn.width
height: peerCardColumn.implicitHeight + Theme.spacingS * 2 height: peerCardColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: peerMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight color: modelData.hostname === (TailscaleService.selfNode ? TailscaleService.selfNode.hostname : "") ? Theme.primaryHoverLight : Theme.surfaceContainerHighest
border.color: isSelf ? Theme.primary : Theme.outlineLight
border.width: isSelf ? 2 : 1 property bool isSelf: modelData.hostname === (TailscaleService.selfNode ? TailscaleService.selfNode.hostname : "")
property bool isExpanded: detailRoot.expandedHostname === modelData.hostname
Column { Column {
id: peerCardColumn id: peerCardColumn
@@ -362,14 +354,14 @@ PluginComponent {
width: 8 width: 8
height: 8 height: 8
radius: 4 radius: 4
color: modelData.online ? Theme.success : Theme.surfaceVariantText color: modelData.online ? "#4caf50" : Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
StyledText { StyledText {
text: modelData.hostname || "" text: modelData.hostname || ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeSmall
font.weight: isSelf ? Font.Medium : Font.Normal font.weight: Font.Bold
color: Theme.surfaceText color: Theme.surfaceText
Layout.fillWidth: true Layout.fillWidth: true
elide: Text.ElideRight elide: Text.ElideRight
@@ -378,7 +370,7 @@ PluginComponent {
StyledText { StyledText {
visible: isSelf visible: isSelf
text: I18n.tr("This device", "Label for the user's own device in Tailscale") text: I18n.tr("This device", "Label for the user's own device in Tailscale")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: 10
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
} }
@@ -417,7 +409,7 @@ PluginComponent {
} }
return parts.join(" \u2022 "); return parts.join(" \u2022 ");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: 10
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
@@ -437,7 +429,7 @@ PluginComponent {
StyledText { StyledText {
text: modelData.dnsName || "" text: modelData.dnsName || ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: 10
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.fillWidth: true Layout.fillWidth: true
elide: Text.ElideRight elide: Text.ElideRight
@@ -455,14 +447,14 @@ PluginComponent {
StyledText { StyledText {
visible: (modelData.tags || []).length > 0 visible: (modelData.tags || []).length > 0
text: I18n.tr("Tags: %1", "Tailscale device tags").arg((modelData.tags || []).join(", ")) text: I18n.tr("Tags: %1", "Tailscale device tags").arg((modelData.tags || []).join(", "))
font.pixelSize: Theme.fontSizeSmall font.pixelSize: 10
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
StyledText { StyledText {
visible: (modelData.owner || "").length > 0 visible: (modelData.owner || "").length > 0
text: I18n.tr("Owner: %1", "Tailscale device owner").arg(modelData.owner || "") text: I18n.tr("Owner: %1", "Tailscale device owner").arg(modelData.owner || "")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: 10
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
+27 -39
View File
@@ -129,7 +129,7 @@ BasePill {
if (deltaY > 0) { if (deltaY > 0) {
MprisController.previousOrRewind(); MprisController.previousOrRewind();
} else { } else {
MprisController.next(); activePlayer.next();
} }
} else { } else {
scrollAccumulatorY += deltaY; scrollAccumulatorY += deltaY;
@@ -137,7 +137,7 @@ BasePill {
if (scrollAccumulatorY > 0) { if (scrollAccumulatorY > 0) {
MprisController.previousOrRewind(); MprisController.previousOrRewind();
} else { } else {
MprisController.next(); activePlayer.next();
} }
scrollAccumulatorY = 0; scrollAccumulatorY = 0;
} }
@@ -266,7 +266,7 @@ BasePill {
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
MprisController.previousOrRewind(); MprisController.previousOrRewind();
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
MprisController.next(); activePlayer.next();
} }
} }
} }
@@ -358,50 +358,38 @@ BasePill {
onTextChanged: { onTextChanged: {
scrollOffset = 0; scrollOffset = 0;
textShift = 0; textShift = 0;
scrollTimer.reset();
textChangeAnimation.restart(); textChangeAnimation.restart();
} }
// Timer stepping, not NumberAnimation a running animation commits frames every vsync (#2863) SequentialAnimation {
Timer { id: scrollAnimation
id: scrollTimer
readonly property real maxOffset: Math.max(0, mediaText.implicitWidth - textContainer.width + 5)
property int direction: 1
property int holdTicks: 33
interval: 60
repeat: true
running: mediaText.needsScrolling && textContainer.visible && mediaText.onScreen && root._isPlaying running: mediaText.needsScrolling && textContainer.visible && mediaText.onScreen && root._isPlaying
loops: Animation.Infinite
onStopped: mediaText.scrollOffset = 0
function reset() { PauseAnimation {
mediaText.scrollOffset = 0; duration: 2000
direction = 1;
holdTicks = 33;
} }
onRunningChanged: { NumberAnimation {
if (!running) target: mediaText
reset(); property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
} }
onTriggered: {
if (holdTicks > 0) { PauseAnimation {
holdTicks--; duration: 2000
return;
} }
const next = mediaText.scrollOffset + direction;
if (next >= maxOffset) { NumberAnimation {
mediaText.scrollOffset = maxOffset; target: mediaText
direction = -1; property: "scrollOffset"
holdTicks = 33; to: 0
return; duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
} easing.type: Easing.Linear
if (next <= 0) {
mediaText.scrollOffset = 0;
direction = 1;
holdTicks = 33;
return;
}
mediaText.scrollOffset = next;
} }
} }
@@ -532,7 +520,7 @@ BasePill {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (activePlayer) { if (activePlayer) {
MprisController.next(); activePlayer.next();
} }
} }
} }
@@ -1752,11 +1752,7 @@ BasePill {
id: trayMenuContainer id: trayMenuContainer
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2)) readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
readonly property real rawHeight: { readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
const desiredHeight = Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2);
const maxHeight = Math.max(40, menuWindow.maskHeight - 20);
return Math.min(desiredHeight, maxHeight);
}
readonly property real alignedWidth: Theme.px(rawWidth, menuWindow.dpr) readonly property real alignedWidth: Theme.px(rawWidth, menuWindow.dpr)
readonly property real alignedHeight: Theme.px(rawHeight, menuWindow.dpr) readonly property real alignedHeight: Theme.px(rawHeight, menuWindow.dpr)
@@ -1856,19 +1852,13 @@ BasePill {
} }
} }
DankFlickable {
id: menuFlickable
anchors.fill: parent
anchors.margins: Theme.spacingS
contentWidth: width
contentHeight: menuColumn.implicitHeight
clip: true
interactive: contentHeight > height
Column { Column {
id: menuColumn id: menuColumn
width: menuFlickable.width width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1 spacing: 1
Rectangle { Rectangle {
@@ -1891,7 +1881,7 @@ BasePill {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
elide: Text.ElideMiddle elide: Text.ElideMiddle
width: parent.width - Theme.spacingS * 2 - (Theme.iconSizeSmall + Theme.spacingS) width: parent.width - Theme.spacingS * 2 - 24
} }
DankIcon { DankIcon {
@@ -1903,7 +1893,7 @@ BasePill {
return "push_pin"; return "push_pin";
return root.isManualHiddenTrayItem(menuRoot.trayItem) ? "visibility" : "visibility_off"; return root.isManualHiddenTrayItem(menuRoot.trayItem) ? "visibility" : "visibility_off";
} }
size: Theme.iconSizeSmall size: 16
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
@@ -1950,7 +1940,7 @@ BasePill {
DankIcon { DankIcon {
name: "arrow_back" name: "arrow_back"
size: Theme.iconSizeSmall size: 16
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -2028,11 +2018,11 @@ BasePill {
visible: !menuEntry?.isSeparator visible: !menuEntry?.isSeparator
Rectangle { Rectangle {
width: Theme.iconSizeSmall width: 16
height: Theme.iconSizeSmall height: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0 visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
radius: menuEntry?.buttonType === 2 ? width / 2 : 2 radius: menuEntry?.buttonType === 2 ? 8 : 2
border.width: 1 border.width: 1
border.color: Theme.outline border.color: Theme.outline
color: "transparent" color: "transparent"
@@ -2049,23 +2039,23 @@ BasePill {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "check" name: "check"
size: Theme.iconSizeSmall - 6 size: 10
color: Theme.primaryText color: Theme.primaryText
visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2 visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
} }
} }
Item { Item {
width: Theme.iconSizeSmall width: 16
height: Theme.iconSizeSmall height: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: (menuEntry?.icon ?? "") !== "" visible: (menuEntry?.icon ?? "") !== ""
Image { Image {
anchors.fill: parent anchors.fill: parent
source: menuEntry?.icon || "" source: menuEntry?.icon || ""
sourceSize.width: Theme.iconSizeSmall sourceSize.width: 16
sourceSize.height: Theme.iconSizeSmall sourceSize.height: 16
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
smooth: true smooth: true
} }
@@ -2082,14 +2072,14 @@ BasePill {
} }
Item { Item {
width: Theme.iconSizeSmall width: 16
height: Theme.iconSizeSmall height: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "chevron_right" name: "chevron_right"
size: Theme.iconSizeSmall - 2 size: 14
color: Theme.widgetTextColor color: Theme.widgetTextColor
visible: menuEntry?.hasChildren ?? false visible: menuEntry?.hasChildren ?? false
} }
@@ -2102,7 +2092,6 @@ BasePill {
} }
} }
} }
}
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
if (!screen) if (!screen)
@@ -662,12 +662,11 @@ Item {
weight: 500 weight: 500
} }
StateLayer { MouseArea {
id: playPauseArea anchors.fill: parent
disabled: !root.activePlayer || !root.activePlayer.canTogglePlaying hoverEnabled: true
stateColor: root.onAccent cursorShape: Qt.PointingHandCursor
cornerRadius: parent.radius onClicked: activePlayer && activePlayer.togglePlaying()
onClicked: root.activePlayer.togglePlaying()
} }
ElevationShadow { ElevationShadow {
@@ -707,7 +706,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: MprisController.next() onClicked: activePlayer && activePlayer.next()
} }
} }
} }
@@ -190,7 +190,7 @@ Card {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: MprisController.next() onClicked: activePlayer?.next()
} }
} }
} }
-13
View File
@@ -340,19 +340,7 @@ Variants {
return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide); return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide);
} }
property bool startupRevealDone: false
Timer {
id: startupRevealTimer
interval: 200
running: true
onTriggered: dock.startupRevealDone = true
}
property bool reveal: { property bool reveal: {
if (!startupRevealDone)
return false;
if (_modalRetractActive) if (_modalRetractActive)
return false; return false;
@@ -640,7 +628,6 @@ Variants {
id: dockContainer id: dockContainer
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: dock.startupRevealDone ? 1 : 0
transform: Translate { transform: Translate {
id: dockSlide id: dockSlide
@@ -1635,7 +1635,7 @@ Item {
enabled: MprisController.activePlayer?.canGoNext ?? false enabled: MprisController.activePlayer?.canGoNext ?? false
hoverEnabled: enabled hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.next() onClicked: MprisController.activePlayer?.next()
} }
} }
} }
+4 -34
View File
@@ -90,17 +90,6 @@ Scope {
fprint.checkAvail(); fprint.checkAvail();
} }
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
readonly property bool fprintSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineFprint
readonly property bool u2fSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineU2f
FileView {
id: customPamWatcher
path: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : ""
printErrors: false
}
FileView { FileView {
id: dankshellConfigWatcher id: dankshellConfigWatcher
@@ -159,8 +148,6 @@ Scope {
id: passwd id: passwd
config: { config: {
if (root.customPamActive)
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
if (dankshellConfigWatcher.loaded) if (dankshellConfigWatcher.loaded)
return "dankshell"; return "dankshell";
if (nixosMarker.loaded || root.runningFromNixStore) if (nixosMarker.loaded || root.runningFromNixStore)
@@ -170,10 +157,6 @@ Scope {
return "login"; return "login";
} }
configDirectory: { configDirectory: {
if (root.customPamActive) {
const idx = SettingsData.lockPamPath.lastIndexOf("/");
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
}
if (dankshellConfigWatcher.loaded) if (dankshellConfigWatcher.loaded)
return "/etc/pam.d"; return "/etc/pam.d";
if (nixosMarker.loaded || root.runningFromNixStore) if (nixosMarker.loaded || root.runningFromNixStore)
@@ -265,7 +248,7 @@ Scope {
property int errorTries property int errorTries
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByCustomPam) { if (!available || !SettingsData.enableFprint || !root.lockSecured) {
abort(); abort();
return; return;
} }
@@ -325,7 +308,7 @@ Scope {
property bool available: SettingsData.lockU2fReady property bool available: SettingsData.lockU2fReady
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByCustomPam) { if (!available || !SettingsData.enableU2f || !root.lockSecured) {
abort(); abort();
return; return;
} }
@@ -335,7 +318,7 @@ Scope {
} }
function startForSecondFactor(): void { function startForSecondFactor(): void {
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam) { if (!available || !SettingsData.enableU2f) {
root.completeUnlock(); root.completeUnlock();
return; return;
} }
@@ -348,7 +331,7 @@ Scope {
} }
function startForAlternativeAuth(): void { function startForAlternativeAuth(): void {
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active) if (!available || !SettingsData.enableU2f || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
return; return;
abort(); abort();
root.u2fPending = true; root.u2fPending = true;
@@ -503,19 +486,6 @@ Scope {
u2f.checkAvail(); u2f.checkAvail();
} }
function onLockPamPathChanged(): void {
fprint.checkAvail();
u2f.checkAvail();
}
function onLockPamInlineFprintChanged(): void {
fprint.checkAvail();
}
function onLockPamInlineU2fChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void { function onU2fModeChanged(): void {
if (root.lockSecured) { if (root.lockSecured) {
u2f.abort(); u2f.abort();
@@ -359,17 +359,6 @@ Item {
anchors.fill: parent anchors.fill: parent
active: root.widgetEnabled && root.activeComponent !== null active: root.widgetEnabled && root.activeComponent !== null
sourceComponent: root.activeComponent sourceComponent: root.activeComponent
opacity: 0
NumberAnimation {
id: revealFade
target: contentLoader
property: "opacity"
from: 0
to: 1
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
function reloadComponent() { function reloadComponent() {
active = false; active = false;
@@ -395,8 +384,6 @@ Item {
if (!item) if (!item)
return; return;
revealFade.restart();
if (item.pluginService !== undefined) { if (item.pluginService !== undefined) {
item.pluginService = root.isInstance ? instanceScopedPluginService : root.pluginService; item.pluginService = root.isInstance ? instanceScopedPluginService : root.pluginService;
} }
+43 -39
View File
@@ -18,40 +18,57 @@ Item {
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
function greeterFingerprintDescription() { function greeterFingerprintDescription() {
if (SettingsData.greeterPamExternallyManaged) const source = SettingsData.greeterFingerprintSource;
return "greetd PAM is externally managed"; const reason = SettingsData.greeterFingerprintReason;
if (SettingsData.greeterFingerprintSource === "pam")
return I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.", "greeter fingerprint login setting");
switch (SettingsData.greeterFingerprintReason) { if (source === "pam") {
case "ready": switch (reason) {
return I18n.tr("Authentication changes apply automatically.", "greeter auth setting description"); case "configured_externally":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.");
case "missing_enrollment": case "missing_enrollment":
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.", "greeter fingerprint login setting"); return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet.");
case "missing_reader": case "missing_reader":
return I18n.tr("No fingerprint reader detected.", "fingerprint setting status"); return I18n.tr("PAM provides fingerprint auth, but no reader was detected.");
case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.", "greeter fingerprint login setting");
default: default:
return I18n.tr("Fingerprint availability could not be confirmed.", "fingerprint setting status"); return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed.");
}
}
switch (reason) {
case "ready":
return SettingsData.greeterEnableFprint ? I18n.tr("Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.");
case "missing_enrollment":
if (SettingsData.greeterEnableFprint)
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.");
case "missing_reader":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.");
default:
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
} }
} }
function greeterU2fDescription() { function greeterU2fDescription() {
if (SettingsData.greeterPamExternallyManaged) const source = SettingsData.greeterU2fSource;
return "greetd PAM is externally managed"; const reason = SettingsData.greeterU2fReason;
if (SettingsData.greeterU2fSource === "pam")
return I18n.tr("PAM already provides security-key auth. Enable this to show it at login.", "greeter security key login setting");
switch (SettingsData.greeterU2fReason) { if (source === "pam") {
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login.");
}
switch (reason) {
case "ready": case "ready":
return I18n.tr("Authentication changes apply automatically.", "greeter auth setting description"); return SettingsData.greeterEnableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Available.");
case "missing_key_registration": case "missing_key_registration":
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status"); if (SettingsData.greeterEnableU2f)
return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.", "greeter security key login setting"); return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.");
default: default:
return I18n.tr("Security-key availability could not be confirmed.", "security key setting status"); return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
} }
} }
@@ -482,15 +499,6 @@ Item {
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
} }
SettingsToggleRow {
settingKey: "greeterPamExternallyManaged"
tags: ["greeter", "pam", "managed", "external", "greetd", "auth"]
text: "greetd PAM is externally managed"
description: "DMS removes its managed block from /etc/pam.d/greetd and stops writing to it"
checked: SettingsData.greeterPamExternallyManaged
onToggled: checked => SettingsData.set("greeterPamExternallyManaged", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "greeterEnableFprint" settingKey: "greeterEnableFprint"
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"] tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
@@ -498,7 +506,7 @@ Item {
description: root.greeterFingerprintDescription() description: root.greeterFingerprintDescription()
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableFprint checked: SettingsData.greeterEnableFprint
enabled: root.greeterFprintToggleAvailable && !SettingsData.greeterPamExternallyManaged enabled: root.greeterFprintToggleAvailable
onToggled: checked => SettingsData.set("greeterEnableFprint", checked) onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
} }
@@ -509,7 +517,7 @@ Item {
description: root.greeterU2fDescription() description: root.greeterU2fDescription()
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableU2f checked: SettingsData.greeterEnableU2f
enabled: root.greeterU2fToggleAvailable && !SettingsData.greeterPamExternallyManaged enabled: root.greeterU2fToggleAvailable
onToggled: checked => SettingsData.set("greeterEnableU2f", checked) onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
} }
} }
@@ -710,13 +718,9 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.AllButtons enabled: !root.greeterSyncRunning && !root.greeterInstallActionRunning
cursorShape: !root.greeterSyncRunning && !root.greeterInstallActionRunning ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: root.runGreeterSync()
onClicked: mouse => {
if (mouse.button === Qt.LeftButton && !root.greeterSyncRunning && !root.greeterInstallActionRunning)
root.runGreeterSync();
}
} }
Row { Row {
+16 -214
View File
@@ -1,5 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Io import Quickshell
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -12,81 +12,35 @@ Item {
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
property var authServices: []
property bool authValidateRunning: false
property bool authValidateOk: false
property bool authValidateWarn: false
property string authValidateMessage: ""
property string authPendingApplyPath: ""
property bool authShowCustom: false
readonly property string authAutoLabel: I18n.tr("Auto", "automatic PAM authentication source option")
readonly property string authCustomLabel: I18n.tr("Custom...", "custom PAM authentication source option")
function authServiceLabel(service) {
const label = service.name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("-");
return service.dir === "/etc/pam.d" ? label : label + " (" + service.dir + ")";
}
readonly property var authOptions: [authAutoLabel, ...authServices.map(s => authServiceLabel(s)), authCustomLabel]
readonly property string authCurrentValue: {
if (SettingsData.lockPamPath === "")
return authAutoLabel;
const svc = authServices.find(s => s.path === SettingsData.lockPamPath);
return svc ? authServiceLabel(svc) : authCustomLabel;
}
function refreshAuthServices() {
authListServicesProcess.running = true;
}
function applyAutoAuthSource() {
SettingsData.set("lockPamPath", "");
SettingsData.set("lockPamInlineFprint", false);
SettingsData.set("lockPamInlineU2f", false);
root.authValidateOk = false;
root.authValidateWarn = false;
root.authValidateMessage = "";
}
function validateAndApplyAuthSource(path) {
if (!path)
return;
root.authPendingApplyPath = path;
root.authValidateMessage = "";
root.authValidateOk = false;
root.authValidateWarn = false;
root.authValidateRunning = true;
authValidateProcess.command = ["dms", "auth", "validate", "--path", path, "--json"];
authValidateProcess.running = true;
}
function lockFingerprintDescription() { function lockFingerprintDescription() {
switch (SettingsData.lockFingerprintReason) { switch (SettingsData.lockFingerprintReason) {
case "ready": case "ready":
return I18n.tr("Use fingerprint authentication for the lock screen.", "lock screen fingerprint setting"); return SettingsData.enableFprint ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use fingerprint authentication for the lock screen.");
case "missing_enrollment": case "missing_enrollment":
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.", "lock screen fingerprint setting"); if (SettingsData.enableFprint)
return I18n.tr("Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
case "missing_reader": case "missing_reader":
return I18n.tr("No fingerprint reader detected.", "fingerprint setting status"); return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd.", "lock screen fingerprint setting"); return I18n.tr("Not available — install fprintd and pam_fprintd.");
default: default:
return I18n.tr("Fingerprint availability could not be confirmed.", "fingerprint setting status"); return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
} }
} }
function lockU2fDescription() { function lockU2fDescription() {
switch (SettingsData.lockU2fReason) { switch (SettingsData.lockU2fReason) {
case "ready": case "ready":
return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting"); return SettingsData.enableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
case "missing_key_registration": case "missing_key_registration":
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status"); if (SettingsData.enableU2f)
return I18n.tr("Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f.", "lock screen security key setting"); return I18n.tr("Not available — install or configure pam_u2f.");
default: default:
return I18n.tr("Security-key availability could not be confirmed.", "security key setting status"); return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
} }
} }
@@ -94,15 +48,10 @@ Item {
SettingsData.refreshAuthAvailability(); SettingsData.refreshAuthAvailability();
} }
Component.onCompleted: { Component.onCompleted: refreshAuthDetection()
refreshAuthDetection();
refreshAuthServices();
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible)
refreshAuthDetection(); refreshAuthDetection();
refreshAuthServices();
}
} }
FileBrowserModal { FileBrowserModal {
@@ -115,71 +64,6 @@ Item {
onFileSelected: path => SettingsData.set("lockScreenVideoPath", path) onFileSelected: path => SettingsData.set("lockScreenVideoPath", path)
} }
Process {
id: authListServicesProcess
command: ["dms", "auth", "list-services", "--json"]
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: authListServicesProcess.collected = text || ""
}
onExited: exitCode => {
if (exitCode !== 0) {
root.authServices = [];
return;
}
try {
const data = JSON.parse(authListServicesProcess.collected);
root.authServices = (data && Array.isArray(data.services)) ? data.services.filter(s => s.hasAuth) : [];
} catch (e) {
root.authServices = [];
}
}
}
Process {
id: authValidateProcess
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: authValidateProcess.collected = text || ""
}
onExited: exitCode => {
root.authValidateRunning = false;
root.authValidateOk = false;
root.authValidateWarn = false;
let data = null;
try {
data = JSON.parse(authValidateProcess.collected);
} catch (e) {}
if (!data) {
root.authValidateMessage = "validation failed — is dms in PATH?";
return;
}
if (!data.valid) {
const errs = Array.isArray(data.errors) ? data.errors : [];
root.authValidateMessage = ["not applied:", ...errs].join("\n");
return;
}
SettingsData.set("lockPamPath", root.authPendingApplyPath);
SettingsData.set("lockPamInlineFprint", data.inlineFingerprint === true);
SettingsData.set("lockPamInlineU2f", data.inlineU2f === true);
const warns = Array.isArray(data.warnings) ? data.warnings : [];
root.authValidateOk = true;
root.authValidateWarn = warns.length > 0;
root.authValidateMessage = warns.length > 0 ? ["applied with warnings:", ...warns].join("\n") : "applied";
}
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -323,88 +207,6 @@ Item {
} }
} }
SettingsCard {
width: parent.width
iconName: "key"
title: "Authentication Source"
settingKey: "lockAuthSource"
SettingsDropdownRow {
settingKey: "lockPamPath"
tags: ["lock", "screen", "pam", "authentication", "source", "service"]
text: "Authentication Source"
description: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : "Which PAM service the lock screen uses to authenticate"
options: root.authOptions
currentValue: root.authCurrentValue
onValueChanged: value => {
if (value === root.authAutoLabel) {
root.authShowCustom = false;
root.applyAutoAuthSource();
return;
}
if (value === root.authCustomLabel) {
root.authShowCustom = true;
return;
}
root.authShowCustom = false;
const svc = root.authServices.find(s => root.authServiceLabel(s) === value);
if (svc)
root.validateAndApplyAuthSource(svc.path);
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: root.authShowCustom || root.authCurrentValue === root.authCustomLabel
DankTextField {
id: customPamField
width: parent.width - validatePamButton.width - Theme.spacingS
placeholderText: "/etc/pam.d/my-service"
text: SettingsData.lockPamPath
backgroundColor: Theme.surfaceContainerHighest
}
DankButton {
id: validatePamButton
text: I18n.tr("Apply Changes", "validate and apply custom PAM authentication source")
enabled: !root.authValidateRunning && customPamField.text.trim() !== ""
onClicked: root.validateAndApplyAuthSource(customPamField.text.trim())
}
}
Rectangle {
width: parent.width
height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
visible: root.authValidateMessage !== ""
StyledText {
id: authStatusText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.authValidateMessage
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
StyledText {
visible: (SettingsData.lockPamInlineFprint && SettingsData.enableFprint) || (SettingsData.lockPamInlineU2f && SettingsData.enableU2f)
text: "The pinned PAM stack already prompts for fingerprint or security key, so DMS skips its own prompts"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
}
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "lock" iconName: "lock"
+3 -14
View File
@@ -37,8 +37,6 @@ Variants {
color: "transparent" color: "transparent"
visible: root.contentReady && !root.surfaceBounce
updatesEnabled: root.renderActive || root._settleFrames > 0 updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
@@ -66,9 +64,6 @@ Variants {
property string actualTransitionType: transitionType property string actualTransitionType: transitionType
property bool isInitialized: false property bool isInitialized: false
property bool contentReady: false
property bool surfaceBounce: false
property string scrollMode: SettingsData.wallpaperFillMode property string scrollMode: SettingsData.wallpaperFillMode
property bool scrollingEnabled: scrollMode === "Scrolling" property bool scrollingEnabled: scrollMode === "Scrolling"
property int currentWorkspaceIndex: 0 property int currentWorkspaceIndex: 0
@@ -268,9 +263,9 @@ Variants {
interval: 0 interval: 0
repeat: false repeat: false
onTriggered: { onTriggered: {
root.surfaceBounce = true; wallpaperWindow.visible = false;
Qt.callLater(() => { Qt.callLater(() => {
root.surfaceBounce = false; wallpaperWindow.visible = true;
}); });
} }
} }
@@ -511,9 +506,6 @@ Variants {
Component.onCompleted: { Component.onCompleted: {
isInitialized = true; isInitialized = true;
if (!source || isColorSource) {
contentReady = true;
}
if (scrollingEnabled) { if (scrollingEnabled) {
updateWorkspaceData(); updateWorkspaceData();
@@ -582,10 +574,8 @@ Variants {
root.effectActive = false; root.effectActive = false;
root.screenScale = CompositorService.getScreenScale(modelData); root.screenScale = CompositorService.getScreenScale(modelData);
// No status change coming to clear the flag // No status change coming to clear the flag
if (!newSource || currentWallpaper.source.toString() === newSource) { if (!newSource || currentWallpaper.source.toString() === newSource)
root.changePending = false; root.changePending = false;
root.contentReady = true;
}
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
@@ -756,7 +746,6 @@ Variants {
} }
if (status === Image.Ready || status === Image.Error) { if (status === Image.Ready || status === Image.Error) {
root.changePending = false; root.changePending = false;
root.contentReady = true;
} }
} }
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
@@ -79,6 +80,7 @@ Item {
readonly property int displayWorkspaceCount: displayedWorkspaceIds.length readonly property int displayWorkspaceCount: displayedWorkspaceIds.length
readonly property int effectiveColumns: SettingsData.overviewColumns readonly property int effectiveColumns: SettingsData.overviewColumns
readonly property int effectiveRows: Math.max(SettingsData.overviewRows, Math.ceil(displayWorkspaceCount / effectiveColumns))
function getWorkspaceMonitorName(workspaceId) { function getWorkspaceMonitorName(workspaceId) {
if (!allWorkspaces || !workspaceId) if (!allWorkspaces || !workspaceId)
@@ -105,53 +107,21 @@ Item {
} }
} }
function monitorIpcForWorkspace(workspaceId) { function getWorkspaceViewportBounds(workspaceId) {
const workspace = allWorkspaces?.find(ws => ws?.id === workspaceId); const workspace = allWorkspaces?.find(ws => ws?.id === workspaceId);
return workspace?.monitor?.lastIpcObject ?? monitor?.lastIpcObject ?? null; const mon = workspace?.monitor?.lastIpcObject || monitor?.lastIpcObject || {};
} const reserved = mon.reserved || [0, 0, 0, 0];
function monitorLogicalSize(ipc) { const x = (mon.x ?? 0) + (reserved[0] ?? 0);
if (!ipc || !ipc.width || !ipc.height) const y = (mon.y ?? 0) + (reserved[1] ?? 0);
return { const width = Math.max((mon.width ?? monitorPhysicalWidth) - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1);
"width": monitorPhysicalWidth, const height = Math.max((mon.height ?? monitorPhysicalHeight) - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1);
"height": monitorPhysicalHeight const scale = Math.min(root.workspaceImplicitWidth / width, root.workspaceImplicitHeight / height);
};
const monScale = ipc.scale > 0 ? ipc.scale : 1;
const rotated = ((ipc.transform ?? 0) % 2) === 1;
return {
"width": (rotated ? ipc.height : ipc.width) / monScale,
"height": (rotated ? ipc.width : ipc.height) / monScale
};
}
function cellForWorkspace(workspaceId) {
const cell = gridLayout.cells.find(c => c.id === workspaceId);
if (cell)
return cell;
return {
"id": workspaceId,
"x": 0,
"y": 0,
"width": workspaceImplicitWidth,
"height": workspaceImplicitHeight
};
}
function getWorkspaceViewportBounds(workspaceId, cellWidth, cellHeight) {
const ipc = monitorIpcForWorkspace(workspaceId) ?? {};
const logical = monitorLogicalSize(ipc);
const reserved = ipc.reserved || [0, 0, 0, 0];
const x = (ipc.x ?? 0) + (reserved[0] ?? 0);
const y = (ipc.y ?? 0) + (reserved[1] ?? 0);
const width = Math.max(logical.width - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1);
const height = Math.max(logical.height - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1);
return { return {
"x": x, "x": x,
"y": y, "y": y,
"scale": Math.min(cellWidth / width, cellHeight / height) "scale": scale
}; };
} }
@@ -173,56 +143,6 @@ Item {
property int draggingFromWorkspace: -1 property int draggingFromWorkspace: -1
property int draggingTargetWorkspace: -1 property int draggingTargetWorkspace: -1
readonly property var gridLayout: {
const ids = displayedWorkspaceIds;
const columns = effectiveColumns;
if (!ids || ids.length === 0 || columns < 1)
return {
"cells": [],
"width": 0,
"height": 0
};
const rows = [];
for (let i = 0; i < ids.length; i += columns) {
rows.push(ids.slice(i, i + columns).map(id => {
const logical = monitorLogicalSize(monitorIpcForWorkspace(id));
return {
"id": id,
"width": logical.width * scale,
"height": logical.height * scale
};
}));
}
const rowWidth = row => row.reduce((acc, cell) => acc + cell.width, 0) + workspaceSpacing * (row.length - 1);
const totalWidth = rows.reduce((acc, row) => Math.max(acc, rowWidth(row)), 0);
const cells = [];
let cursorY = 0;
for (const row of rows) {
const rowHeight = row.reduce((acc, cell) => Math.max(acc, cell.height), 0);
let cursorX = (totalWidth - rowWidth(row)) / 2;
for (const cell of row) {
cells.push({
"id": cell.id,
"x": cursorX,
"y": cursorY + (rowHeight - cell.height) / 2,
"width": cell.width,
"height": cell.height
});
cursorX += cell.width + workspaceSpacing;
}
cursorY += rowHeight + workspaceSpacing;
}
return {
"cells": cells,
"width": totalWidth,
"height": cursorY - workspaceSpacing
};
}
implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2 implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2
implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2 implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2
@@ -246,8 +166,8 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
implicitWidth: workspaceGrid.implicitWidth + padding * 2 implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
implicitHeight: workspaceGrid.implicitHeight + padding * 2 implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainer color: Theme.surfaceContainer
@@ -262,27 +182,33 @@ Item {
shadowEnabled: Theme.elevationEnabled shadowEnabled: Theme.elevationEnabled
} }
Item { ColumnLayout {
id: workspaceGrid id: workspaceColumnLayout
z: root.workspaceZ z: root.workspaceZ
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: root.gridLayout.width spacing: workspaceSpacing
implicitHeight: root.gridLayout.height
Repeater { Repeater {
model: root.gridLayout.cells.length model: root.effectiveRows
delegate: RowLayout {
id: row
property int rowIndex: index
spacing: workspaceSpacing
Repeater {
model: root.effectiveColumns
Rectangle { Rectangle {
id: workspace id: workspace
required property int index property int colIndex: index
readonly property var cell: root.gridLayout.cells[index] ?? null property int workspaceIndex: rowIndex * root.effectiveColumns + colIndex
property int workspaceValue: cell?.id ?? -1 property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null
property bool isActive: workspaceObj?.active ?? false property bool isActive: workspaceObj?.active ?? false
property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true
property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3) property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3)
property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1) property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1)
property color hoveredBorderColor: Theme.surfaceVariant property color hoveredBorderColor: Theme.surfaceVariant
@@ -291,10 +217,8 @@ Item {
visible: workspaceValue !== -1 visible: workspaceValue !== -1
x: cell?.x ?? 0 implicitWidth: root.workspaceImplicitWidth
y: cell?.y ?? 0 implicitHeight: root.workspaceImplicitHeight
width: cell?.width ?? 0
height: cell?.height ?? 0
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 2 border.width: 2
@@ -302,10 +226,10 @@ Item {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: workspace.workspaceValue text: workspaceValue
font.pixelSize: Theme.fontSizeXLarge * 6 font.pixelSize: Theme.fontSizeXLarge * 6
font.weight: Font.DemiBold font.weight: Font.DemiBold
color: Theme.withAlpha(Theme.surfaceText, workspace.workspaceExists ? 0.2 : 0.1) color: Theme.withAlpha(Theme.surfaceText, workspaceExists ? 0.2 : 0.1)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@@ -317,7 +241,7 @@ Item {
onClicked: { onClicked: {
if (root.draggingTargetWorkspace === -1) { if (root.draggingTargetWorkspace === -1) {
root.overviewOpen = false; root.overviewOpen = false;
HyprlandService.focusWorkspace(workspace.workspaceValue); HyprlandService.focusWorkspace(workspaceValue);
} }
} }
} }
@@ -325,26 +249,28 @@ Item {
DropArea { DropArea {
anchors.fill: parent anchors.fill: parent
onEntered: { onEntered: {
root.draggingTargetWorkspace = workspace.workspaceValue; root.draggingTargetWorkspace = workspaceValue;
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) if (root.draggingFromWorkspace == root.draggingTargetWorkspace)
return; return;
workspace.hoveredWhileDragging = true; hoveredWhileDragging = true;
} }
onExited: { onExited: {
workspace.hoveredWhileDragging = false; hoveredWhileDragging = false;
if (root.draggingTargetWorkspace == workspace.workspaceValue) if (root.draggingTargetWorkspace == workspaceValue)
root.draggingTargetWorkspace = -1; root.draggingTargetWorkspace = -1;
} }
} }
} }
} }
} }
}
}
Item { Item {
id: windowSpace id: windowSpace
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: workspaceGrid.implicitWidth implicitWidth: workspaceColumnLayout.implicitWidth
implicitHeight: workspaceGrid.implicitHeight implicitHeight: workspaceColumnLayout.implicitHeight
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
@@ -380,21 +306,41 @@ Item {
overviewOpen: root.overviewOpen overviewOpen: root.overviewOpen
readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1 readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1
readonly property var workspaceCell: root.cellForWorkspace(windowWorkspaceId)
readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId, workspaceCell.width, workspaceCell.height) function getWorkspaceIndex() {
if (!root.displayedWorkspaceIds || root.displayedWorkspaceIds.length === 0)
return 0;
if (!windowWorkspaceId || windowWorkspaceId < 0)
return 0;
try {
for (let i = 0; i < root.displayedWorkspaceIds.length; i++) {
if (root.displayedWorkspaceIds[i] === windowWorkspaceId) {
return i;
}
}
return 0;
} catch (e) {
return 0;
}
}
readonly property int workspaceIndex: getWorkspaceIndex()
readonly property int workspaceColIndex: workspaceIndex % root.effectiveColumns
readonly property int workspaceRowIndex: Math.floor(workspaceIndex / root.effectiveColumns)
readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId)
toplevel: modelData toplevel: modelData
scale: root.scale scale: root.scale
monitorDpr: root.dpr monitorDpr: root.dpr
availableWorkspaceWidth: workspaceCell.width availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: workspaceCell.height availableWorkspaceHeight: root.workspaceImplicitHeight
contentOriginX: workspaceBounds.x contentOriginX: workspaceBounds.x
contentOriginY: workspaceBounds.y contentOriginY: workspaceBounds.y
contentScale: workspaceBounds.scale contentScale: workspaceBounds.scale
widgetMonitorId: root.monitor.id widgetMonitorId: root.monitor.id
xOffset: workspaceCell.x xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
yOffset: workspaceCell.y yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
z: atInitPosition ? root.windowZ : root.windowDraggingZ z: atInitPosition ? root.windowZ : root.windowDraggingZ
property bool atInitPosition: (initX == x && initY == y) property bool atInitPosition: (initX == x && initY == y)
@@ -463,24 +409,32 @@ Item {
Item { Item {
id: monitorLabelSpace id: monitorLabelSpace
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: workspaceGrid.implicitWidth implicitWidth: workspaceColumnLayout.implicitWidth
implicitHeight: workspaceGrid.implicitHeight implicitHeight: workspaceColumnLayout.implicitHeight
z: root.monitorLabelZ z: root.monitorLabelZ
Repeater { Repeater {
model: root.gridLayout.cells.length model: root.effectiveRows
delegate: Item {
id: labelRow
property int rowIndex: index
y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex
width: parent.width
height: root.workspaceImplicitHeight
Repeater {
model: root.effectiveColumns
delegate: Item { delegate: Item {
id: labelItem id: labelItem
required property int index property int colIndex: index
readonly property var cell: root.gridLayout.cells[index] ?? null property int workspaceIndex: labelRow.rowIndex * root.effectiveColumns + colIndex
property int workspaceValue: cell?.id ?? -1 property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : "" property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
x: cell?.x ?? 0 x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex
y: cell?.y ?? 0 width: root.workspaceImplicitWidth
width: cell?.width ?? 0 height: root.workspaceImplicitHeight
height: cell?.height ?? 0
Rectangle { Rectangle {
anchors.right: parent.right anchors.right: parent.right
@@ -508,3 +462,5 @@ Item {
} }
} }
} }
}
}
+5 -3
View File
@@ -12,6 +12,8 @@ Singleton {
property bool suppressSound: true property bool suppressSound: true
property bool previousPluggedState: false property bool previousPluggedState: false
readonly property var scale: 100 / SettingsData.batteryChargeLimit
Timer { Timer {
id: startupTimer id: startupTimer
interval: 500 interval: 500
@@ -76,7 +78,7 @@ Singleton {
return 0; return 0;
if (batteryCapacity === 0) { if (batteryCapacity === 0) {
if (usePreferred && preferredDeviceKnown) { if (usePreferred && preferredDeviceKnown) {
const val = Math.min(100, Math.round(preferredDevice.percentage * 100)); const val = Math.min(100, Math.round(preferredDevice.percentage * 100 * scale));
_lastBatteryLevel = val; _lastBatteryLevel = val;
return val; return val;
} }
@@ -86,7 +88,7 @@ Singleton {
if (validBatteries.length === 0) if (validBatteries.length === 0)
return _lastBatteryLevel; return _lastBatteryLevel;
const avgPercentage = validBatteries.reduce((sum, b) => sum + b.percentage, 0) / validBatteries.length; const avgPercentage = validBatteries.reduce((sum, b) => sum + b.percentage, 0) / validBatteries.length;
const val = Math.min(100, Math.round(avgPercentage * 100)); const val = Math.min(100, Math.round(avgPercentage * 100 * scale));
_lastBatteryLevel = val; _lastBatteryLevel = val;
return val; return val;
} }
@@ -94,7 +96,7 @@ Singleton {
const cap = batteryCapacity; const cap = batteryCapacity;
if (cap === 0) if (cap === 0)
return _lastBatteryLevel; return _lastBatteryLevel;
const val = Math.min(100, Math.round((energy * 100) / cap)); const val = Math.min(100, Math.round((energy * 100) / cap * scale));
_lastBatteryLevel = val; _lastBatteryLevel = val;
return val; return val;
} }
+1 -1
View File
@@ -13,7 +13,7 @@ Singleton {
readonly property var log: Log.scoped("ChangelogService") readonly property var log: Log.scoped("ChangelogService")
readonly property string currentVersion: "1.5" readonly property string currentVersion: "1.5"
readonly property bool changelogEnabled: true readonly property bool changelogEnabled: false
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
+107 -117
View File
@@ -28,6 +28,7 @@ Singleton {
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
readonly property string swaySocket: Quickshell.env("SWAYSOCK") readonly property string swaySocket: Quickshell.env("SWAYSOCK")
readonly property string scrollSocket: Quickshell.env("SWAYSOCK")
readonly property string miracleSocket: Quickshell.env("MIRACLESOCK") readonly property string miracleSocket: Quickshell.env("MIRACLESOCK")
readonly property string labwcPid: Quickshell.env("LABWC_PID") readonly property string labwcPid: Quickshell.env("LABWC_PID")
readonly property string mangoSignature: Quickshell.env("MANGO_INSTANCE_SIGNATURE") readonly property string mangoSignature: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
@@ -942,133 +943,122 @@ Singleton {
} }
} }
// Primary detection asks the kernel which process owns the $WAYLAND_DISPLAY
// socket the compositor quickshell is actually connected to. Env vars like
// HYPRLAND_INSTANCE_SIGNATURE / MANGO_INSTANCE_SIGNATURE can leak into the
// systemd user environment from previous sessions and lie. Unset
// WAYLAND_DISPLAY falls back to "wayland-0", mirroring wl_display_connect.
// /proc/net/unix: field 6 is state (01 = listening), 7 inode, 8 bound path.
function detectCompositor() { function detectCompositor() {
const script = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"'; if (mangoSignature && mangoSignature.length > 0) {
Proc.runCommand("waylandSocketOwner", ["sh", "-c", script], (output, exitCode) => { isHyprland = false;
const comm = (exitCode === 0 && output) ? output.trim().toLowerCase() : ""; isNiri = false;
const name = _compositorNameFromComm(comm); isMango = true;
if (name) { isSway = false;
_applyCompositor(name); isScroll = false;
log.info("Detected", name, "from Wayland socket owner:", comm); isMiracle = false;
isLabwc = false;
compositor = "mango";
log.info("Detected MangoWM via MANGO_INSTANCE_SIGNATURE");
return; return;
} }
if (comm)
log.info("Unrecognized Wayland socket owner:", comm, "- falling back to env detection"); if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) {
_detectFromEnv(0); isHyprland = true;
}, 0, 3000); isNiri = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "hyprland";
log.info("Detected Hyprland");
return;
} }
function _compositorNameFromComm(comm) { if (niriSocket && niriSocket.length > 0) {
switch (comm) { Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => {
case "niri": if (exitCode === 0) {
return "niri"; isNiri = true;
case "hyprland": isHyprland = false;
return "hyprland"; isMango = false;
case "sway": isSway = false;
return "sway"; isScroll = false;
case "scroll": isMiracle = false;
return "scroll"; isLabwc = false;
case "mango": compositor = "niri";
return "mango"; log.info("Detected Niri with socket:", niriSocket);
case "miracle-wm":
return "miracle";
case "labwc":
return "labwc";
default:
return "";
}
}
function _applyCompositor(name) {
isHyprland = name === "hyprland";
isNiri = name === "niri";
isMango = name === "mango";
isSway = name === "sway";
isScroll = name === "scroll";
isMiracle = name === "miracle";
isLabwc = name === "labwc";
compositor = name;
compositorDetected = true;
if (isNiri)
NiriService.generateNiriBlurrule(); NiriService.generateNiriBlurrule();
} }
// Fallback when the socket owner can't be resolved (no ss, unrecognized
// comm). Same priority order as before, but every candidate must prove
// liveness; a dead socket/PID falls through to the next candidate instead
// of winning on a stale env var.
function _envDetectionCandidates() {
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || "";
return [
{
name: "mango",
present: !!mangoSignature,
test: ["test", "-S", mangoSignature],
detail: "MANGO_INSTANCE_SIGNATURE " + mangoSignature
},
{
name: "niri",
present: !!niriSocket,
test: ["test", "-S", niriSocket],
detail: "NIRI_SOCKET " + niriSocket
},
{
name: "miracle",
present: !!miracleSocket,
test: ["test", "-S", miracleSocket],
detail: "MIRACLESOCK " + miracleSocket
},
{
name: "sway",
present: !!swaySocket,
test: ["test", "-S", swaySocket],
resolve: () => {
const desktop = String(Quickshell.env("XDG_CURRENT_DESKTOP") || "").toLowerCase();
return desktop.includes("sway") ? "sway" : "scroll";
},
detail: "SWAYSOCK " + swaySocket
},
{
name: "labwc",
present: !!labwcPid,
test: ["sh", "-c", "[ \"$(cat /proc/\"$LABWC_PID\"/comm 2>/dev/null)\" = labwc ]"],
detail: "LABWC_PID " + labwcPid
},
{
name: "hyprland",
present: !!hyprlandSignature,
test: ["test", "-S", runtimeDir + "/hypr/" + hyprlandSignature + "/.socket.sock"],
detail: "HYPRLAND_INSTANCE_SIGNATURE " + hyprlandSignature
}
];
}
function _detectFromEnv(index) {
const candidates = _envDetectionCandidates();
for (let i = index; i < candidates.length; i++) {
const c = candidates[i];
if (!c.present)
continue;
const next = i + 1;
Proc.runCommand(c.name + "SocketCheck", c.test, (output, exitCode) => {
if (exitCode !== 0) {
log.warn(c.detail, "is set but not alive, skipping");
_detectFromEnv(next);
return;
}
const name = c.resolve ? c.resolve() : c.name;
_applyCompositor(name);
log.info("Detected", name, "via", c.detail);
}, 0); }, 0);
return; return;
} }
_applyCompositor("unknown");
if (swaySocket && swaySocket.length > 0 && !scrollSocket && scrollSocket.length == 0 && !miracleSocket) {
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isSway = true;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "sway";
log.info("Detected Sway with socket:", swaySocket);
}
}, 0);
return;
}
if (miracleSocket && miracleSocket.length > 0) {
Proc.runCommand("miracleSocketCheck", ["test", "-S", miracleSocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = true;
isLabwc = false;
compositor = "miracle";
log.info("Detected Miracle WM with socket:", miracleSocket);
}
}, 0);
return;
}
if (scrollSocket && scrollSocket.length > 0 && !miracleSocket) {
Proc.runCommand("scrollSocketCheck", ["test", "-S", scrollSocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isMango = false;
isSway = false;
isScroll = true;
isMiracle = false;
isLabwc = false;
compositor = "scroll";
log.info("Detected Scroll with socket:", scrollSocket);
}
}, 0);
return;
}
if (labwcPid && labwcPid.length > 0) {
isHyprland = false;
isNiri = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = true;
compositor = "labwc";
log.info("Detected LabWC with PID:", labwcPid);
return;
}
isHyprland = false;
isNiri = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "unknown";
log.warn("No compositor detected"); log.warn("No compositor detected");
} }
+24 -9
View File
@@ -48,12 +48,22 @@ Singleton {
target: root.activePlayer target: root.activePlayer
function onTrackTitleChanged() { function onTrackTitleChanged() {
root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0; root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0;
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
function onTrackArtistChanged() {
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
} }
function onLengthChanged() { function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) { if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length; root.activePlayerStableLength = root.activePlayer.length;
} }
} }
function onPlaybackStateChanged() {
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
} }
onActivePlayerChanged: { onActivePlayerChanged: {
@@ -63,6 +73,18 @@ Singleton {
onAvailablePlayersChanged: _resolveActivePlayer() onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer() Component.onCompleted: _resolveActivePlayer()
Instantiator {
model: root.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
function onIsPlayingChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
}
}
}
function isIdle(player: MprisPlayer): bool { function isIdle(player: MprisPlayer): bool {
return player return player
&& player.playbackState === MprisPlaybackState.Stopped && player.playbackState === MprisPlaybackState.Stopped
@@ -71,15 +93,14 @@ Singleton {
} }
function _resolveActivePlayer(): void { function _resolveActivePlayer(): void {
// Keep the selected player stable across transient metadata changes.
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return;
const playing = availablePlayers.find(p => p.isPlaying); const playing = availablePlayers.find(p => p.isPlaying);
if (playing) { if (playing) {
activePlayer = playing; activePlayer = playing;
_persistIdentity(playing.identity); _persistIdentity(playing.identity);
return; return;
} }
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0 && !isIdle(activePlayer))
return;
const savedId = SessionData.lastPlayerIdentity; const savedId = SessionData.lastPlayerIdentity;
if (savedId) { if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId); const match = availablePlayers.find(p => p.identity === savedId);
@@ -129,10 +150,4 @@ Singleton {
else if (activePlayer.canGoPrevious) else if (activePlayer.canGoPrevious)
activePlayer.previous(); activePlayer.previous();
} }
function next(): void {
const player = activePlayer;
if (player?.canGoNext)
player.next();
}
} }
+24 -28
View File
@@ -56,13 +56,13 @@ Singleton {
return ""; return "";
} }
function _commit(u, artKey, srcUrl) { function _commit(u) {
resolvedArtUrl = u; resolvedArtUrl = u;
_committedArtKey = u !== "" ? artKey : ""; _committedArtKey = u !== "" ? _pendingArtKey : "";
_committedSrcUrl = u !== "" ? srcUrl : ""; _committedSrcUrl = u !== "" ? _lastArtUrl : "";
} }
function loadArtwork(url, artKey, requestSerial) { function loadArtwork(url) {
if (!url || url === "") { if (!url || url === "") {
// Keep stale art; only blank once the empty url debounce settles. // Keep stale art; only blank once the empty url debounce settles.
_lastArtUrl = ""; _lastArtUrl = "";
@@ -85,11 +85,11 @@ Singleton {
// 1. First, check if the file already exists locally // 1. First, check if the file already exists locally
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => { Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial) if (_lastArtUrl !== targetUrl)
return; return;
if (exitCode === 0) { if (exitCode === 0) {
_commit(localFileUrl, artKey, targetUrl); _commit(localFileUrl);
loading = false; loading = false;
} else { } else {
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }"; const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
@@ -102,18 +102,18 @@ Singleton {
const tmpPath = filePath + ".tmp"; const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => {
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial) if (_lastArtUrl !== targetUrl)
return; return;
if (maxExitCode === 0) { if (maxExitCode === 0) {
_commit(localFileUrl, artKey, targetUrl); _commit(localFileUrl);
loading = false; loading = false;
} else { } else {
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial) if (_lastArtUrl !== targetUrl)
return; return;
_commit(mqExitCode === 0 ? localFileUrl : targetUrl, artKey, targetUrl); _commit(mqExitCode === 0 ? localFileUrl : targetUrl);
loading = false; loading = false;
}, 50, 15000); }, 50, 15000);
} }
@@ -122,10 +122,10 @@ Singleton {
// Standard curl download for other remote URLs (e.g. SoundCloud) // Standard curl download for other remote URLs (e.g. SoundCloud)
const tmpPath = filePath + ".tmp"; const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial) if (_lastArtUrl !== targetUrl)
return; return;
_commit(dlExitCode === 0 ? localFileUrl : targetUrl, artKey, targetUrl); _commit(dlExitCode === 0 ? localFileUrl : targetUrl);
loading = false; loading = false;
}, 50, 15000); }, 50, 15000);
} }
@@ -141,7 +141,7 @@ Singleton {
// Cover lands after metadata, so poll; commit a content-addressed copy so identical bytes keep an identical url // Cover lands after metadata, so poll; commit a content-addressed copy so identical bytes keep an identical url
const script = "f=\"$1\"; d=\"$2\"; i=0; while [ ! -f \"$f\" ] && [ \"$i\" -lt 20 ]; do sleep 0.15; i=$((i + 1)); done; [ -f \"$f\" ] || exit 1; s=$(sha1sum \"$f\" | cut -c1-40); if [ ! -f \"$d/art_$s\" ]; then mkdir -p \"$d\" && cp \"$f\" \"$d/art_$s.tmp\" && mv \"$d/art_$s.tmp\" \"$d/art_$s\" || exit 1; fi; echo \"$s\""; const script = "f=\"$1\"; d=\"$2\"; i=0; while [ ! -f \"$f\" ] && [ \"$i\" -lt 20 ]; do sleep 0.15; i=$((i + 1)); done; [ -f \"$f\" ] || exit 1; s=$(sha1sum \"$f\" | cut -c1-40); if [ ! -f \"$d/art_$s\" ]; then mkdir -p \"$d\" && cp \"$f\" \"$d/art_$s.tmp\" && mv \"$d/art_$s.tmp\" \"$d/art_$s\" || exit 1; fi; echo \"$s\"";
Proc.runCommand(null, ["sh", "-c", script, "sh", filePath, cacheDir], (output, exitCode) => { Proc.runCommand(null, ["sh", "-c", script, "sh", filePath, cacheDir], (output, exitCode) => {
if (_lastArtUrl !== localUrl || _requestSerial !== requestSerial) if (_lastArtUrl !== localUrl)
return; return;
loading = false; loading = false;
if (exitCode !== 0) if (exitCode !== 0)
@@ -149,7 +149,7 @@ Singleton {
const sha = (output || "").trim(); const sha = (output || "").trim();
if (_artHashDenylist.indexOf(sha) !== -1) if (_artHashDenylist.indexOf(sha) !== -1)
return; return;
_commit("file://" + cacheDir + "/art_" + sha, artKey, localUrl); _commit("file://" + cacheDir + "/art_" + sha);
}, 50, 5000); }, 50, 5000);
} }
@@ -158,7 +158,7 @@ Singleton {
interval: 800 interval: 800
onTriggered: { onTriggered: {
if (root._lastArtUrl === "") if (root._lastArtUrl === "")
root._commit("", "", ""); root._commit("");
} }
} }
@@ -167,7 +167,6 @@ Singleton {
property string _committedArtKey: "" property string _committedArtKey: ""
property string _pendingArtKey: "" property string _pendingArtKey: ""
property string _committedSrcUrl: "" property string _committedSrcUrl: ""
property int _requestSerial: 0
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
@@ -183,10 +182,9 @@ Singleton {
const p = activePlayer; const p = activePlayer;
if (!p) if (!p)
return ""; return "";
// Scope artwork ownership to the MPRIS object and track. // +title/artist: KDEconnect reuses one trackid forever; album excluded: Chrome delivers it late, orphaning the lock
const playerId = p.uniqueId || p.identity || "";
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : ""; const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
return playerId + " " + tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || ""); return tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || "");
} }
function artReadyFor(player) { function artReadyFor(player) {
@@ -196,19 +194,17 @@ Singleton {
function _updateArtUrl() { function _updateArtUrl() {
const key = _trackKey(); const key = _trackKey();
if (key !== _pendingArtKey) { // Skip once real art is committed for this track (dedup Chrome's multi-size
_requestSerial++; // re-publish). The lock is set in _commit(), never optimistically, so a rejected
loading = false; // placeholder or a short-circuited duplicate url can't wedge the real cover out.
}
_pendingArtKey = key;
const url = getArtworkUrl(activePlayer);
// Ignore Chrome's same-track thumbnail size updates.
if (key !== "" && key === _committedArtKey) if (key !== "" && key === _committedArtKey)
return; return;
_pendingArtKey = key;
const url = getArtworkUrl(activePlayer);
if (key !== "" && url !== "" && url === _committedSrcUrl) { if (key !== "" && url !== "" && url === _committedSrcUrl) {
// Chrome can publish track metadata before its new artwork URL. _committedArtKey = key;
return; return;
} }
loadArtwork(url, key, _requestSerial); loadArtwork(url);
} }
} }
+1 -1
View File
@@ -1 +1 @@
v1.5.1 v1.6-beta
+10 -6
View File
@@ -182,12 +182,16 @@ Item {
} }
} }
// Timer, not FrameAnimation a running animation commits frames every vsync (#2863) FrameAnimation {
Timer { running: blobEffect.visible
running: blobEffect.visible && root.onScreen property real pending: 0
interval: 33 onTriggered: {
repeat: true pending += frameTime;
onTriggered: root.stepBlob(0.033) if (pending < 0.03)
return;
root.stepBlob(Math.min(pending, 0.05));
pending = 0;
}
} }
ShaderEffect { ShaderEffect {
-4
View File
@@ -147,10 +147,6 @@ Item {
trackColor: MediaAccentService.accentTrack trackColor: MediaAccentService.accentTrack
actualProgressColor: MediaAccentService.accentSubtle actualProgressColor: MediaAccentService.accentSubtle
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
onFrameTicked: {
if (!root.isSeeking)
activePlayer.positionChanged();
}
MouseArea { MouseArea {
id: waveMouseArea id: waveMouseArea
+3 -7
View File
@@ -50,15 +50,11 @@ Item {
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb") fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
} }
signal frameTicked
FrameAnimation { FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0) && (root.Window.window?.visible ?? false) running: root.visible && (root.isPlaying || root.currentAmp > 0)
onTriggered: { onTriggered: {
if (!root.isPlaying) if (root.isPlaying)
return; root.phase += 0.03 * frameTime * 60;
root.phase = (root.phase + 0.03 * frameTime * 60) % 6.28318530718;
root.frameTicked();
} }
} }
} }
File diff suppressed because it is too large Load Diff
+25 -25
View File
@@ -777,7 +777,7 @@
"Authenticate": "صادِق" "Authenticate": "صادِق"
}, },
"Authenticated!": { "Authenticated!": {
"Authenticated!": "تم التحقق بنجاح!" "Authenticated!": ""
}, },
"Authenticating...": { "Authenticating...": {
"Authenticating...": "جاري المصادقة..." "Authenticating...": "جاري المصادقة..."
@@ -804,13 +804,13 @@
"Authentication error - try again": "خطأ في المصادقة - حاول مرة أخرى" "Authentication error - try again": "خطأ في المصادقة - حاول مرة أخرى"
}, },
"Authentication failed - attempt %1 of %2": { "Authentication failed - attempt %1 of %2": {
"Authentication failed - attempt %1 of %2": "فشل التحقق من الهوية - المحاولة %1 من %2" "Authentication failed - attempt %1 of %2": ""
}, },
"Authentication failed - lockout can occur": { "Authentication failed - lockout can occur": {
"Authentication failed - lockout can occur": "فشل التحقق - قد يؤدي ذلك إلى قفل حسابك" "Authentication failed - lockout can occur": ""
}, },
"Authentication failed - try again": { "Authentication failed - try again": {
"Authentication failed - try again": "فشل التحقق - يرجى المحاولة مرة أخرى" "Authentication failed - try again": ""
}, },
"Authentication failed, please try again": { "Authentication failed, please try again": {
"Authentication failed, please try again": "فشل التحقق، أعد المحاولة" "Authentication failed, please try again": "فشل التحقق، أعد المحاولة"
@@ -987,13 +987,13 @@
"Available.": "متاح." "Available.": "متاح."
}, },
"Awaiting fingerprint authentication": { "Awaiting fingerprint authentication": {
"Awaiting fingerprint authentication": "بانتظار التحقق ببصمة الإصبع" "Awaiting fingerprint authentication": ""
}, },
"Awaiting fingerprint or security key authentication": { "Awaiting fingerprint or security key authentication": {
"Awaiting fingerprint or security key authentication": "بانتظار التحقق ببصمة الإصبع أو مفتاح الأمان" "Awaiting fingerprint or security key authentication": ""
}, },
"Awaiting security key authentication": { "Awaiting security key authentication": {
"Awaiting security key authentication": "بانتظار التحقق بمفتاح الأمان" "Awaiting security key authentication": ""
}, },
"BSSID": { "BSSID": {
"BSSID": "BSSID" "BSSID": "BSSID"
@@ -2175,7 +2175,7 @@
"Custom command and terminal params are split on whitespace; paths with spaces will break.": "يتم فصل الأمر المخصص ومعلمات الجهاز على المسافات البيضاء؛ المسارات التي تحتوي على مسافات ستتسبب في خطأ." "Custom command and terminal params are split on whitespace; paths with spaces will break.": "يتم فصل الأمر المخصص ومعلمات الجهاز على المسافات البيضاء؛ المسارات التي تحتوي على مسافات ستتسبب في خطأ."
}, },
"Custom interval in minutes (minimum 5)": { "Custom interval in minutes (minimum 5)": {
"Custom interval in minutes (minimum 5)": "الفترة الزمنية المخصصة بالدقائق (الحد الأدنى 5)" "Custom interval in minutes (minimum 5)": ""
}, },
"Custom open-trash command": { "Custom open-trash command": {
"Custom open-trash command": "أمر مخصص لفتح المهملات" "Custom open-trash command": "أمر مخصص لفتح المهملات"
@@ -3435,7 +3435,7 @@
"Flags": "الأعلام" "Flags": "الأعلام"
}, },
"Flatpak": { "Flatpak": {
"Flatpak": "Flatpak" "Flatpak": ""
}, },
"Flipped": { "Flipped": {
"Flipped": "معكوس" "Flipped": "معكوس"
@@ -3852,7 +3852,7 @@
"Groups": "المجموعات" "Groups": "المجموعات"
}, },
"H": { "H": {
"H": "H" "H": ""
}, },
"HDR (EDID)": { "HDR (EDID)": {
"HDR (EDID)": "HDR (EDID)" "HDR (EDID)": "HDR (EDID)"
@@ -4149,22 +4149,22 @@
"Ignore Completely": "تجاهل تمامًا" "Ignore Completely": "تجاهل تمامًا"
}, },
"Ignore package": { "Ignore package": {
"Ignore package": "تجاهل البرنامج" "Ignore package": ""
}, },
"Ignore this package": { "Ignore this package": {
"Ignore this package": "تجاهل هذا التطبيق" "Ignore this package": ""
}, },
"Ignored (%1)": { "Ignored (%1)": {
"Ignored (%1)": "تم تجاهله (%1)" "Ignored (%1)": ""
}, },
"Ignored Packages": { "Ignored Packages": {
"Ignored Packages": "التطبيقات المتجاهلة" "Ignored Packages": ""
}, },
"Ignored packages are hidden from the updater and skipped by 'Update All'.": { "Ignored packages are hidden from the updater and skipped by 'Update All'.": {
"Ignored packages are hidden from the updater and skipped by 'Update All'.": "يتم إخفاء التطبيقات المتجاهلة من أداة التحديث ويتخطاها خيار \"تحديث الكل\"." "Ignored packages are hidden from the updater and skipped by 'Update All'.": ""
}, },
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": { "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": {
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": "تنطبق التطبيقات المتجاهلة فقط على أداة التحديث المدمجة. يتحكم أمرك المخصص في الاستثناءات الخاصة به." "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": ""
}, },
"Image": { "Image": {
"Image": "صورة" "Image": "صورة"
@@ -4347,7 +4347,7 @@
"Invalid configuration": "تكوين غير صالح" "Invalid configuration": "تكوين غير صالح"
}, },
"Invalid package name — letters, digits and @._+:- only.": { "Invalid package name — letters, digits and @._+:- only.": {
"Invalid package name — letters, digits and @._+:- only.": "اسم التطبيق غير صالح — يُسمح فقط بالأحرف، والأرقام، والرموز التالية: @._+:-" "Invalid package name — letters, digits and @._+:- only.": ""
}, },
"Invalid password for %1": { "Invalid password for %1": {
"Invalid password for %1": "كلمة مرور غير صالحة لـ %1" "Invalid password for %1": "كلمة مرور غير صالحة لـ %1"
@@ -5451,7 +5451,7 @@
"No output devices found": "لم يتم العثور على أجهزة إخراج" "No output devices found": "لم يتم العثور على أجهزة إخراج"
}, },
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": { "No packages ignored. Add one here or hover an update in the popout and click the hide button.": {
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": "لا توجد تطبيقات متجاهلة. أضف تطبيقًا هنا، أو مرر مؤشر الفأرة فوق التحديث في النافذة المنبثقة وانقر على زر الإخفاء." "No packages ignored. Add one here or hover an update in the popout and click the hide button.": ""
}, },
"No peers found": { "No peers found": {
"No peers found": "لم يتم العثور على أقران" "No peers found": "لم يتم العثور على أقران"
@@ -5937,7 +5937,7 @@
"PIN": "PIN" "PIN": "PIN"
}, },
"Package name (e.g., docker)": { "Package name (e.g., docker)": {
"Package name (e.g., docker)": "اسم التطبيق (مثال: docker)" "Package name (e.g., docker)": ""
}, },
"Pad": { "Pad": {
"Pad": "وسادة" "Pad": "وسادة"
@@ -7812,7 +7812,7 @@
"Status": "الحالة" "Status": "الحالة"
}, },
"Stop ignoring %1": { "Stop ignoring %1": {
"Stop ignoring %1": "إلغاء تجاهل %1" "Stop ignoring %1": ""
}, },
"Stopped": { "Stopped": {
"Stopped": "متوقف" "Stopped": "متوقف"
@@ -8910,7 +8910,7 @@
"Votes": "الأصوات" "Votes": "الأصوات"
}, },
"W": { "W": {
"W": "W" "W": ""
}, },
"WPA/WPA2": { "WPA/WPA2": {
"WPA/WPA2": "WPA/WPA2" "WPA/WPA2": "WPA/WPA2"
@@ -9213,16 +9213,16 @@
"by %1": "بواسطة %1" "by %1": "بواسطة %1"
}, },
"checked %1d ago": { "checked %1d ago": {
"checked %1d ago": "تم الفحص قبل %1 من الأيام" "checked %1d ago": ""
}, },
"checked %1h ago": { "checked %1h ago": {
"checked %1h ago": "تم الفحص قبل %1 من الساعات" "checked %1h ago": ""
}, },
"checked %1m ago": { "checked %1m ago": {
"checked %1m ago": "تم الفحص قبل %1 من الدقائق" "checked %1m ago": ""
}, },
"checked just now": { "checked just now": {
"checked just now": "تم الفحص الآن" "checked just now": ""
}, },
"days": { "days": {
"days": "أيام" "days": "أيام"
+25 -25
View File
@@ -777,7 +777,7 @@
"Authenticate": "Authentifizieren" "Authenticate": "Authentifizieren"
}, },
"Authenticated!": { "Authenticated!": {
"Authenticated!": "Authentifiziert!" "Authenticated!": ""
}, },
"Authenticating...": { "Authenticating...": {
"Authenticating...": "Authentifizierung..." "Authenticating...": "Authentifizierung..."
@@ -804,13 +804,13 @@
"Authentication error - try again": "Authentifizierungsfehler - erneut versuchen" "Authentication error - try again": "Authentifizierungsfehler - erneut versuchen"
}, },
"Authentication failed - attempt %1 of %2": { "Authentication failed - attempt %1 of %2": {
"Authentication failed - attempt %1 of %2": "Authentifizierung fehlgeschlagen - Versuch %1 von %2" "Authentication failed - attempt %1 of %2": ""
}, },
"Authentication failed - lockout can occur": { "Authentication failed - lockout can occur": {
"Authentication failed - lockout can occur": "Authentifizierung fehlgeschlagen - Sperrung kann erfolgen" "Authentication failed - lockout can occur": ""
}, },
"Authentication failed - try again": { "Authentication failed - try again": {
"Authentication failed - try again": "Authentifizierung fehlgeschlagen - erneut versuchen" "Authentication failed - try again": ""
}, },
"Authentication failed, please try again": { "Authentication failed, please try again": {
"Authentication failed, please try again": "Authentifizierung nicht erfolgreich, bitte nochmals probieren" "Authentication failed, please try again": "Authentifizierung nicht erfolgreich, bitte nochmals probieren"
@@ -987,13 +987,13 @@
"Available.": "Verfügbar." "Available.": "Verfügbar."
}, },
"Awaiting fingerprint authentication": { "Awaiting fingerprint authentication": {
"Awaiting fingerprint authentication": "Warten auf Fingerabdruck-Authentifizierung" "Awaiting fingerprint authentication": ""
}, },
"Awaiting fingerprint or security key authentication": { "Awaiting fingerprint or security key authentication": {
"Awaiting fingerprint or security key authentication": "Warten auf Fingerabdruck- oder Sicherheitsschlüssel-Authentifizierung" "Awaiting fingerprint or security key authentication": ""
}, },
"Awaiting security key authentication": { "Awaiting security key authentication": {
"Awaiting security key authentication": "Warten auf Sicherheitsschlüssel-Authentifizierung" "Awaiting security key authentication": ""
}, },
"BSSID": { "BSSID": {
"BSSID": "BSSID" "BSSID": "BSSID"
@@ -2175,7 +2175,7 @@
"Custom command and terminal params are split on whitespace; paths with spaces will break.": "Benutzerdefinierte Befehle und Terminalparameter werden an Leerzeichen getrennt; Pfade mit Leerzeichen funktionieren nicht." "Custom command and terminal params are split on whitespace; paths with spaces will break.": "Benutzerdefinierte Befehle und Terminalparameter werden an Leerzeichen getrennt; Pfade mit Leerzeichen funktionieren nicht."
}, },
"Custom interval in minutes (minimum 5)": { "Custom interval in minutes (minimum 5)": {
"Custom interval in minutes (minimum 5)": "Benutzerdefiniertes Intervall in Minuten (Minimum 5)" "Custom interval in minutes (minimum 5)": ""
}, },
"Custom open-trash command": { "Custom open-trash command": {
"Custom open-trash command": "Benutzerdefinierter Befehl zum Öffnen des Papierkorbs" "Custom open-trash command": "Benutzerdefinierter Befehl zum Öffnen des Papierkorbs"
@@ -3435,7 +3435,7 @@
"Flags": "Flags" "Flags": "Flags"
}, },
"Flatpak": { "Flatpak": {
"Flatpak": "Flatpak" "Flatpak": ""
}, },
"Flipped": { "Flipped": {
"Flipped": "Gespiegelt" "Flipped": "Gespiegelt"
@@ -3852,7 +3852,7 @@
"Groups": "Gruppen" "Groups": "Gruppen"
}, },
"H": { "H": {
"H": "H" "H": ""
}, },
"HDR (EDID)": { "HDR (EDID)": {
"HDR (EDID)": "HDR (EDID)" "HDR (EDID)": "HDR (EDID)"
@@ -4149,22 +4149,22 @@
"Ignore Completely": "Vollständig ignorieren" "Ignore Completely": "Vollständig ignorieren"
}, },
"Ignore package": { "Ignore package": {
"Ignore package": "Paket ignorieren" "Ignore package": ""
}, },
"Ignore this package": { "Ignore this package": {
"Ignore this package": "Dieses Paket ignorieren" "Ignore this package": ""
}, },
"Ignored (%1)": { "Ignored (%1)": {
"Ignored (%1)": "Ignoriert (%1)" "Ignored (%1)": ""
}, },
"Ignored Packages": { "Ignored Packages": {
"Ignored Packages": "Ignorierte Pakete" "Ignored Packages": ""
}, },
"Ignored packages are hidden from the updater and skipped by 'Update All'.": { "Ignored packages are hidden from the updater and skipped by 'Update All'.": {
"Ignored packages are hidden from the updater and skipped by 'Update All'.": "Ignorierte Pakete werden im Updater ausgeblendet und bei \"Alle aktualisieren\" übersprungen." "Ignored packages are hidden from the updater and skipped by 'Update All'.": ""
}, },
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": { "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": {
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": "Ignorierte Pakete gelten nur für den integrierten Updater. Ihr benutzerdefinierter Befehl steuert seine eigenen Ausschlüsse." "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": ""
}, },
"Image": { "Image": {
"Image": "Bild" "Image": "Bild"
@@ -4347,7 +4347,7 @@
"Invalid configuration": "Invalide Konfiguration" "Invalid configuration": "Invalide Konfiguration"
}, },
"Invalid package name — letters, digits and @._+:- only.": { "Invalid package name — letters, digits and @._+:- only.": {
"Invalid package name — letters, digits and @._+:- only.": "Ungültiger Paketname — nur Buchstaben, Ziffern und @._+:- zulässig." "Invalid package name — letters, digits and @._+:- only.": ""
}, },
"Invalid password for %1": { "Invalid password for %1": {
"Invalid password for %1": "Ungültiges Passwort für %1" "Invalid password for %1": "Ungültiges Passwort für %1"
@@ -5451,7 +5451,7 @@
"No output devices found": "Keine Ausgabegeräte gefunden" "No output devices found": "Keine Ausgabegeräte gefunden"
}, },
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": { "No packages ignored. Add one here or hover an update in the popout and click the hide button.": {
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": "Keine Pakete ignoriert. Fügen Sie hier eines hinzu oder bewegen Sie den Mauszeiger über ein Update im Popout und klicken Sie auf die Schaltfläche Ausblenden." "No packages ignored. Add one here or hover an update in the popout and click the hide button.": ""
}, },
"No peers found": { "No peers found": {
"No peers found": "Keine Peers gefunden" "No peers found": "Keine Peers gefunden"
@@ -5937,7 +5937,7 @@
"PIN": "PIN" "PIN": "PIN"
}, },
"Package name (e.g., docker)": { "Package name (e.g., docker)": {
"Package name (e.g., docker)": "Paketname (z. B. docker)" "Package name (e.g., docker)": ""
}, },
"Pad": { "Pad": {
"Pad": "Auffüllen" "Pad": "Auffüllen"
@@ -7812,7 +7812,7 @@
"Status": "Status" "Status": "Status"
}, },
"Stop ignoring %1": { "Stop ignoring %1": {
"Stop ignoring %1": "Ignorieren von %1 beenden" "Stop ignoring %1": ""
}, },
"Stopped": { "Stopped": {
"Stopped": "Gestoppt" "Stopped": "Gestoppt"
@@ -8910,7 +8910,7 @@
"Votes": "Stimmen" "Votes": "Stimmen"
}, },
"W": { "W": {
"W": "W" "W": ""
}, },
"WPA/WPA2": { "WPA/WPA2": {
"WPA/WPA2": "WPA/WPA2" "WPA/WPA2": "WPA/WPA2"
@@ -9213,16 +9213,16 @@
"by %1": "von %1" "by %1": "von %1"
}, },
"checked %1d ago": { "checked %1d ago": {
"checked %1d ago": "vor %1 T geprüft" "checked %1d ago": ""
}, },
"checked %1h ago": { "checked %1h ago": {
"checked %1h ago": "vor %1 Std. geprüft" "checked %1h ago": ""
}, },
"checked %1m ago": { "checked %1m ago": {
"checked %1m ago": "vor %1 Min. geprüft" "checked %1m ago": ""
}, },
"checked just now": { "checked just now": {
"checked just now": "gerade eben geprüft" "checked just now": ""
}, },
"days": { "days": {
"days": "Tage" "days": "Tage"
@@ -4634,41 +4634,6 @@
"zenbrowser" "zenbrowser"
] ]
}, },
{
"section": "lockAuthSource",
"label": "Authentication Source",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"lock",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source"
],
"icon": "key"
},
{
"section": "lockPamPath",
"label": "Authentication Source",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"lock",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source"
]
},
{ {
"section": "lockScreenVideoCycling", "section": "lockScreenVideoCycling",
"label": "Automatic Cycling", "label": "Automatic Cycling",
@@ -7989,20 +7954,14 @@
"keywords": [ "keywords": [
"auth", "auth",
"authentication", "authentication",
"block",
"display manager", "display manager",
"external", "fingerprint",
"fprintd",
"greetd", "greetd",
"greeter", "greeter",
"login", "login"
"managed",
"pam",
"removes",
"stops",
"writing"
], ],
"icon": "fingerprint", "icon": "fingerprint"
"description": "DMS removes its managed block from /etc/pam.d/greetd and stops writing to it"
}, },
{ {
"section": "greeterRememberLastSession", "section": "greeterRememberLastSession",
@@ -8040,28 +7999,6 @@
], ],
"description": "Pre-fill the last successful username on the greeter" "description": "Pre-fill the last successful username on the greeter"
}, },
{
"section": "greeterPamExternallyManaged",
"label": "greetd PAM is externally managed",
"tabIndex": 31,
"category": "Greeter",
"keywords": [
"auth",
"block",
"display manager",
"external",
"externally",
"greetd",
"greeter",
"login",
"managed",
"pam",
"removes",
"stops",
"writing"
],
"description": "DMS removes its managed block from /etc/pam.d/greetd and stops writing to it"
},
{ {
"section": "muxType", "section": "muxType",
"label": "Multiplexer", "label": "Multiplexer",
+129 -17
View File
@@ -1528,7 +1528,7 @@
{ {
"term": "Apply Changes", "term": "Apply Changes",
"translation": "", "translation": "",
"context": "validate and apply custom PAM authentication source", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -1836,7 +1836,14 @@
{ {
"term": "Authentication changes apply automatically.", "term": "Authentication changes apply automatically.",
"translation": "", "translation": "",
"context": "greeter auth setting description", "context": "",
"reference": "",
"comment": ""
},
{
"term": "Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.",
"translation": "",
"context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -1906,7 +1913,7 @@
{ {
"term": "Auto", "term": "Auto",
"translation": "", "translation": "",
"context": "automatic PAM authentication source option | calendar backend option | theme category option", "context": "calendar backend option | theme category option",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -2274,6 +2281,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Available.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Awaiting fingerprint authentication", "term": "Awaiting fingerprint authentication",
"translation": "", "translation": "",
@@ -4979,7 +4993,7 @@
{ {
"term": "Custom...", "term": "Custom...",
"translation": "", "translation": "",
"context": "custom PAM authentication source option | date format option", "context": "date format option",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -6537,6 +6551,76 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Enabled, but fingerprint availability could not be confirmed.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no fingerprint reader was detected.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no registered security key was found yet. Register a key and run Sync.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but security-key availability could not be confirmed.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled. PAM already provides fingerprint auth.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled. PAM already provides security-key auth.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Enabling WiFi...", "term": "Enabling WiFi...",
"translation": "", "translation": "",
@@ -7716,7 +7800,7 @@
{ {
"term": "Fingerprint availability could not be confirmed.", "term": "Fingerprint availability could not be confirmed.",
"translation": "", "translation": "",
"context": "fingerprint setting status", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -7737,14 +7821,14 @@
{ {
"term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.", "term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.",
"translation": "", "translation": "",
"context": "lock screen fingerprint setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.", "term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.",
"translation": "", "translation": "",
"context": "greeter fingerprint login setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -12308,7 +12392,7 @@
{ {
"term": "No fingerprint reader detected.", "term": "No fingerprint reader detected.",
"translation": "", "translation": "",
"context": "fingerprint setting status", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -12749,28 +12833,28 @@
{ {
"term": "Not available — install fprintd and pam_fprintd, or configure greetd PAM.", "term": "Not available — install fprintd and pam_fprintd, or configure greetd PAM.",
"translation": "", "translation": "",
"context": "greeter fingerprint login setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Not available — install fprintd and pam_fprintd.", "term": "Not available — install fprintd and pam_fprintd.",
"translation": "", "translation": "",
"context": "lock screen fingerprint setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Not available — install or configure pam_u2f, or configure greetd PAM.", "term": "Not available — install or configure pam_u2f, or configure greetd PAM.",
"translation": "", "translation": "",
"context": "greeter security key login setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Not available — install or configure pam_u2f.", "term": "Not available — install or configure pam_u2f.",
"translation": "", "translation": "",
"context": "lock screen security key setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -13082,6 +13166,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Only on Battery", "term": "Only on Battery",
"translation": "", "translation": "",
@@ -13512,14 +13603,35 @@
{ {
"term": "PAM already provides fingerprint auth. Enable this to show it at login.", "term": "PAM already provides fingerprint auth. Enable this to show it at login.",
"translation": "", "translation": "",
"context": "greeter fingerprint login setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "PAM already provides security-key auth. Enable this to show it at login.", "term": "PAM already provides security-key auth. Enable this to show it at login.",
"translation": "", "translation": "",
"context": "greeter security key login setting", "context": "",
"reference": "",
"comment": ""
},
{
"term": "PAM provides fingerprint auth, but availability could not be confirmed.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "PAM provides fingerprint auth, but no prints are enrolled yet.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "PAM provides fingerprint auth, but no reader was detected.",
"translation": "",
"context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -16116,14 +16228,14 @@
{ {
"term": "Security-key availability could not be confirmed.", "term": "Security-key availability could not be confirmed.",
"translation": "", "translation": "",
"context": "security key setting status", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "term": "Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.",
"translation": "", "translation": "",
"context": "security key setting status", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -19875,7 +19987,7 @@
{ {
"term": "Use fingerprint authentication for the lock screen.", "term": "Use fingerprint authentication for the lock screen.",
"translation": "", "translation": "",
"context": "lock screen fingerprint setting", "context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
+4 -21
View File
@@ -20,7 +20,6 @@ import re
import subprocess import subprocess
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path
BOT_RE = re.compile(r"\[bot\]$|^github-actions$|^dependabot$", re.I) BOT_RE = re.compile(r"\[bot\]$|^github-actions$|^dependabot$", re.I)
# default blog-table exclusions # default blog-table exclusions
@@ -312,13 +311,6 @@ def main():
help="github format: omit heading and Full Changelog footer") help="github format: omit heading and Full Changelog footer")
ap.add_argument("--no-api", action="store_true", ap.add_argument("--no-api", action="store_true",
help="skip GitHub API, use git data only (degraded attribution)") help="skip GitHub API, use git data only (degraded attribution)")
ap.add_argument("-o", "--output", default=None, metavar="PATH",
help="write the output to PATH instead of stdout (parent dirs "
"are created)")
ap.add_argument("--version", default=None, metavar="X.Y.Z",
help="release version being cut; if set and -o/--output isn't, "
"writes to ~/Documents/dms-vX.Y.Z-changelog.md instead of "
"stdout")
args = ap.parse_args() args = ap.parse_args()
repo = args.repo repo = args.repo
@@ -337,25 +329,16 @@ def main():
drop = {x.lower() for x in excludes if x} drop = {x.lower() for x in excludes if x}
if args.format == "blog": if args.format == "blog":
# blog excludes from tables only; fixes list keeps everyone # blog excludes from tables only; fixes list keeps everyone
text = format_blog(repo, entries, args.range, exclude=frozenset(drop)) print(format_blog(repo, entries, args.range, exclude=drop))
else: return 0
if drop: if drop:
entries = [e for e in entries entries = [e for e in entries
if (e["login"] or "").lower() not in drop if (e["login"] or "").lower() not in drop
and e["author_name"].lower() not in drop] and e["author_name"].lower() not in drop]
if args.format == "github": if args.format == "github":
text = format_github(repo, entries, args.range, bare=args.bare) print(format_github(repo, entries, args.range, bare=args.bare))
else: else:
text = format_checklist(entries) print(format_checklist(entries))
out_target = args.output or (f"~/Documents/dms-v{args.version}-changelog.md" if args.version else None)
if out_target:
out_path = Path(out_target).expanduser()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(text + "\n")
print(f"written to {out_path}", file=sys.stderr)
else:
print(text)
return 0 return 0