1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-30 09:32:05 -04:00

Compare commits

...

1053 Commits

Author SHA1 Message Date
Amaan Qureshi
eb5afcdc40 flake: let module callers supply pkgs so overlays reach the build (#2244)
The nixosModule/homeModule path previously called `buildDmsPkgs pkgs` but
internally referenced `self.packages.${system}.default`, which was
instantiated via `nixpkgs.legacyPackages`, an unoverlayed pkgs. That
meant downstream flakes couldn't reach through their own overlays to
the dms-shell build (e.g. to swap `kdePackages.sonnet` or trim perl
out of the aspell closure).

Extract the derivation as `mkDmsShell = pkgs: ...` at the top-level
`let`, and call it from both `packages.${system}.dms-shell` (for
direct consumers of the flake) and `buildDmsPkgs pkgs` (for module
consumers, which now pass in the system's overlayed pkgs).

Also re-checks overrideAttrs / .override still work: `mkDmsShell pkgs`
is the same `pkgs.lib.makeOverridable` wrapper as before, just
parameterized on the caller's pkgs instance.

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>
2026-04-28 22:59:59 -04:00
Youseffo13
dd2a3f3efc Fixed mux tab having same id as locale tab (#2031)
* Feat: fix mux tab having same id as locale tab

* Feat: updated some icon

* Update KeybindsModal.qml
2026-04-28 16:34:31 -04:00
Walid Salah
161fa2dba5 Add terminal multiplexer launcher (#1687)
* Add tmux

* Add mux modal

* Restore the settings config version

* Revert typo

* Use DankModal for InputModal

* Simplify terminal flags

* use showWithOptions for inputModals instead

* Fix translation

* use Quickshell.env("TERMINAL") to choose terminal

* Fix typo

* Hide muxModal after creating new session

* Add mux check, moved exclusion to service, And use ScriptModel

* Revert unrelated change

* Add blank line
2026-04-28 16:34:29 -04:00
mihem
f5f50f7add feat(running-apps): stronger active app highlight + indicator bar (#2190)
The focused app background used 20% primary opacity which was barely
visible. Increase to 45% to make the active window unambiguous at a glance.
2026-04-28 16:26:33 -04:00
bbedward
372cf2f566 core: add privesc package for privilege escalation
- Adds support for run0 and doas
fixes #998
2026-04-28 16:03:24 -04:00
bbedward
b70acbc283 revert greeter clock keys 2026-04-28 15:05:55 -04:00
purian23
571c1158bf (dms): Add input group to dms setup
- Suppress fix/warnings
2026-04-28 14:34:24 -04:00
bbedward
ac03a2e4b2 niri overlay: fix state binding
fixes #2301
2026-04-28 13:19:57 -04:00
Nic Ficca
07460dc3b7 Fix: close notification center after clicking action buttons (#2276)
* Close notification center after clicking action buttons

When clicking action buttons (e.g., "View", "Activate") in the
notification center, the action fires but the popout stays open. Since
the center is a layer-shell surface, it blocks focus changes on Wayland
compositors like niri, making the action appear to do nothing.

The keyboard navigation path already closes the center after invoking
actions; this brings the mouse click path in line.

Also fix closeNotificationCenter() in PopoutService to set
notificationHistoryVisible = false (matching PopoutManager._closePopout)
instead of calling close() directly, which left the visibility property
stale and caused the bell toggle to require two presses to reopen.

Fixes #2178

* Sync notificationHistoryVisible with shouldBeVisible

NotificationCenterPopout has its own notificationHistoryVisible property
that drives open/close, but the PopoutService public API (open, close,
toggle) calls DankPopout methods directly, bypassing that property. This
leaves notificationHistoryVisible stale, causing the bell toggle to
require two presses to reopen after a programmatic close.

Sync the property from onShouldBeVisibleChanged so any caller going
through open()/close() gets the state corrected automatically.
2026-04-28 11:50:40 -04:00
Kangheng Liu
7412fee590 feat(desktop): expose accept keyboard focus to desktop widgets (#2285)
Opt in by setting acceptsKeyboardFocus: true
2026-04-28 11:46:43 -04:00
bbedward
85c2954958 osd(media): workaround for firefox reporting youtube thumbnails as
players
fixes #2298
2026-04-28 11:46:26 -04:00
bbedward
7c9e805cbe port audioservice fix 2026-04-28 10:25:12 -04:00
bbedward
6926470b04 v 1.4.6 2026-04-28 10:12:56 -04:00
bbedward
713ba1efbb idle/lock: add option to turn off monitors after lock explicitly 2026-04-28 10:12:35 -04:00
bbedward
1919ca7243 fix cherry pick issues 2026-04-28 10:01:09 -04:00
Archit Arora
bcf41ed5ca feat(system-tray): add icon tinting (#2266) 2026-04-25 15:32:14 -04:00
purian23
5033bdc630 (settings): Update monocrhrome & settings 2026-04-25 15:29:27 -04:00
purian23
b8bfaf9a26 (dbar): Settings reorg 2026-04-25 14:52:38 -04:00
Kangheng Liu
da45714c54 keybinds: add move workspace to monitor keybinds (#2268)
and distinguish with move columns
2026-04-25 14:45:23 -04:00
bbedward
0c2d00b79c fix transparency 2026-04-25 11:54:19 -04:00
purian23
c10b42f599 dms(blur): Dank all the things 2026-04-25 11:54:18 -04:00
bbedward
4c617cf022 fix popout transparency 2026-04-25 11:54:18 -04:00
bbedward
e75b95b854 blur: revise general blur styling and refine it
cherry-pick of 1abb2210 from master, translated to hotfix-1.4.5
(omits ElevationShadow gating since shadow overhaul isn't on this
branch; gates existing MultiEffect-based shadows by BlurService.enabled
instead). Drops DoNotDisturbDetail.qml hunk since the file does not
exist on this branch.
2026-04-25 11:54:18 -04:00
Lucas
6b15670918 nix: update quickshell version (#2263)
Updated the quickshell revision to 783c95, matching the "stable" package in other DMS distributions.
2026-04-24 17:17:36 -04:00
Walid Salah
c52b9e19a1 Fix focused app when switching to empty workspace (#2259)
* Fix multiple screens on niri, when switching to an empty wokspace the other screen focused app widget would get confused

* Blank workspace fix
2026-04-24 17:17:36 -04:00
purian23
7a3444bd30 dankinstall(distros): Enhance DMS minimal install logic
-Updated for Debian, Ubuntu, Fedora, and OpenSUSE
- New shared minimal installation logic to streamline package handling across distros
2026-04-23 18:41:02 -04:00
bbedward
a733d760e4 clipboard: decode metadata only 2026-04-23 09:29:12 -04:00
bbedward
1b33079e39 missing import 2026-04-22 11:28:20 -04:00
Kristijan Ribarić
1cf0dd1031 fix(quickshell): restore night mode and OSD surfaces after resume (#2254) 2026-04-22 11:13:18 -04:00
bbedward
8d49a5cbfc doctor: add Miracle WM to checks 2026-04-22 11:12:20 -04:00
Jos Dehaes
f5928b09d3 Labwc service (#2248)
* services: add LabwcService with quit

labwc has a minimal IPC surface (no socket, no queries) but it does
expose `labwc --exit` as a clean shutdown path. Wrap that in a small
Singleton service following the same shape as DwlService/NiriService
so the compositor-specific dispatch in callers can stay uniform.

* session: dispatch labwc logout via LabwcService

CompositorService.isLabwc was detected but never dispatched in
_logout(); labwc sessions therefore fell through to the Hyprland
exit call, which silently no-ops under labwc. Users had to set
customPowerActionLogout to 'labwc --exit' as a workaround.

Add a labwc branch alongside the existing niri/dwl/sway branches
so the power menu logout works out of the box.
2026-04-22 11:12:20 -04:00
Walid Salah
38373aa5f2 Fix: Expand tilde from config paths (#2242)
* Expand tilde to the home directory for paths from config

* Remove extra line
2026-04-22 11:12:20 -04:00
bbedward
665680e15e keybinds(niri): add preprocessors to KDL parsing
fixes #2230
2026-04-22 11:11:48 -04:00
bbedward
210607cfbc fix(ddc): prevent negative WaitGroup counter on rapid brightness changes 2026-04-22 11:11:48 -04:00
bbedward
69fca14611 audio: defensive checks on PwNode objects 2026-04-22 11:11:48 -04:00
DavutHaxor
10a235e686 Fix ddc brightness not applying because process exits before debounce timer runs (#2217)
* Fix ddc brightness not applying because process exits before debounce timer runs

* Added sync.WaitGroup to DDCBackend and use it instead of loop in wait logic, added timeout in case i2c hangs.

* go fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-04-22 11:10:34 -04:00
bbedward
253cc7f8a3 network: report negotiated link rate when connected
fixes #2214
2026-04-22 11:10:34 -04:00
bbedward
a63ad99684 doctor: fix quickshell regex
fixes #2204
2026-04-22 11:10:34 -04:00
Thomas Kroll
c44c032879 fix(privacy): detect screen casting on Niri via PipeWire (#2185)
Screen sharing was not detected by PrivacyService on Niri because:

1. Niri creates the screencast as a Stream/Output/Video node, but
   screensharingActive only checked PwNodeType.VideoSource nodes.

2. looksLikeScreencast() only inspected application.name and
   node.name, missing Niri's node which has an empty application.name
   but identifies itself via media.name (niri-screen-cast-src).

Add Stream/Output/Video to the checked media classes and include
media.name in the screencast heuristic. Also add a forward-compatible
check for NiriService.hasActiveCast for when Niri gains cast tracking
in its IPC.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 11:10:34 -04:00
Marcus Ramberg
dc1ce55971 core(doctor): show all detected terminals (#2163) 2026-04-22 11:10:34 -04:00
Al- Amin
9f65882a12 fix: Add match rule for new version of Gnome Calculator app ID (#2157) 2026-04-22 11:10:34 -04:00
Walid Salah
97bf83cce6 Make focused app widget only show focused app on the current screen (#2152) 2026-04-22 11:10:34 -04:00
Al- Amin
96bf0162d6 fix:update resizeactive binding to include height to make it work (#2126) 2026-04-22 11:10:22 -04:00
Al- Amin
73b833731a fix:add window rule for the new version of Gnome Calculator (#2125) 2026-04-22 11:10:22 -04:00
bbedward
84522aeaad screenshot: fix scaling of global coordinate space when using all
screens
2026-04-22 11:10:22 -04:00
bbedward
faf1a277d2 workspace: fix mouse area to edges
fixes #2108
2026-04-22 11:10:22 -04:00
Graeme Foster
60515736e6 fix(osd): coerce optional chain to bool in VolumeOSD enabled bindings (#2101)
Fixes #2100
2026-04-22 11:09:56 -04:00
bbedward
1715e2eab7 fix syncWallpaperForCurrentMode 2026-04-22 11:09:56 -04:00
bbedward
4e14cf5cce add notification card inset 2026-04-22 10:58:11 -04:00
bbedward
a644c93b1b fix blur port and warnings 2026-04-22 10:53:46 -04:00
Lucas
f9428a1009 doctor: add blur support (#2236) 2026-04-22 10:41:18 -04:00
purian23
b4b51785e5 feat:(Notepad): Add blur & update animation track 2026-04-22 10:41:05 -04:00
bbedward
0a97df6d49 launcher: some polishes for blur 2026-04-22 10:40:57 -04:00
bbedward
352ba77677 blur: stylize control center for blur mode 2026-04-22 10:39:56 -04:00
bbedward
d320035d97 blur: add probe to check compositor for ext-bg-effect 2026-04-22 10:38:13 -04:00
bbedward
8d262a9555 blur: add blur support with ext-bg-effect 2026-04-22 10:38:04 -04:00
bbedward
9bfa8310d2 dankinstall: workarounds for arch/extra change 2026-04-11 12:42:07 -04:00
bbedward
088ed806ae core: allow RO commands to run as root 2026-04-07 15:01:36 -04:00
bbedward
07d2c94676 clipboard: make CLI keep CL item in-memory again 2026-04-07 15:01:21 -04:00
bbedward
0bc1b7a3c2 clipboard: fix reliability of modal/popout 2026-04-07 15:01:16 -04:00
bbedward
c5987b28c0 fix(clipboard): wait for forked child to register Wayland source before returning 2026-04-07 15:01:10 -04:00
bbedward
18901c7cde core: migrate to dms-shell arch package 2026-04-06 10:10:36 -04:00
purian23
519a8357a1 fix(lock/greeter): sync auth files with master 2026-03-26 11:37:17 -04:00
purian23
799773c62b refactor: Remove faillock support and related properties from settings 2026-03-26 10:55:32 -04:00
purian23
247a674c79 fix(pam): Update config selection logic for PAM context 2026-03-26 10:55:05 -04:00
purian23
72b598057c fix(lock): Restore system PAM fallback, faillock support, and auth feedback
- Re-add loginConfigWatcher so installs can still fall through to
  /etc/pam.d instead of the bundled PAM assets
- Add login-faillock bundled PAM asset at runtime. Use it as the bundled fallback when dankshell config is absent
- Fix invalid bare property writes (u2fPending, u2fState, unlockInProgress,
  state) in Pam.qml
- Improve lockscreen auth feedback
2026-03-26 10:51:45 -04:00
Patrick Fischer
8180e30e8e fix: restore lock screen U2F/fingerprint auth to working state (#2052)
* fix: restore lock screen U2F/fingerprint auth to working state

* fix(pam): Keep SettingsData as single source of truth for auth availability
- Restores SettingsData for fingerprint/U2F, keeping lock screen and New Greeter Settings UI in sync

---------

Co-authored-by: purian23 <purian23@gmail.com>
2026-03-26 10:48:10 -04:00
purian23
dd9851b4f0 fix(Dock): Replace hardcoded max height mask in vertical mode 2026-03-26 10:41:02 -04:00
bbedward
056576fbe7 core: execute quickshell IPC with pid 2026-03-24 10:45:53 -04:00
bbedward
d28a5bdf7f widgets: convert DankButtonGroup to Row instead of Flow 2026-03-24 10:45:49 -04:00
bbedward
f62ea119f7 popout: avoid calling close on bad reference 2026-03-24 10:45:44 -04:00
bbedward
a1f9b98727 wallpaper: updatesEnabled set on screen changes 2026-03-24 10:45:37 -04:00
bbedward
b9c8914d46 cli/notify: append file:// prefix for --file arguments
fixes #1962
2026-03-24 10:45:28 -04:00
İlkecan Bozdoğan
8697840d46 nix: add package option for dms-shell (#1864)
... to make it configurable.
2026-03-24 10:44:51 -04:00
purian23
1f64bb8031 notifications(Settings): Update notifs popout settings overflow 2026-03-20 19:59:45 -04:00
purian23
eea7d12c0b dankinstall(Arch): improve AUR package installation logic 2026-03-20 17:50:24 -04:00
Linken Quy Dinh
85173126f4 fix: multi-monitor wallpaper cycling not working (#2042)
Fixed a QML property binding timing issue where dynamically created timers
and processes for per-monitor wallpaper cycling were being assigned to
properties and then immediately read back, which could return undefined
or stale values.

The fix stores the created object in a local variable before assigning
to the property map, ensuring a valid reference is always used.

Affected functions:
- startMonitorCycling() - timer creation
- cycleToNextWallpaper() - process creation
- cycleToPrevWallpaper() - process creation
2026-03-20 17:40:52 -04:00
bbedward
222187d8a6 niri: set com.danklinux.dms window rule for future compat 2026-03-20 10:05:29 -04:00
bbedward
bef3f65f63 popout: avoid calling functions on stale references 2026-03-20 09:36:38 -04:00
Dimariqe
bff83fe563 fix: redraw wallpaper after DMS lock screen is dismissed (#2037)
After unlocking the screen (startup lock or wake from sleep), the desktop
showed Hyprland's background color instead of the wallpaper.

WallpaperBackground disables QML updates via updatesEnabled after a 1-second
settle timer. While WlSessionLock is active, Hyprland does not composite the
background layer, so when the lock is released it needs a fresh Wayland buffer
— but none is committed because the render loop is already paused.

The previous attempt used SessionService.sessionUnlocked, which is unreliable
for the startup lock case: DMSService is not yet connected when lock() is
called at startup, so notifyLoginctl is a no-op and the loginctl state never
transitions, meaning sessionUnlocked never fires.

Fix by tracking the shell lock state directly from Lock.qml's shouldLock via
a new IdleService.isShellLocked property. WallpaperBackground watches this and
re-enables rendering for 1 second on unlock, ensuring a fresh buffer is
committed to Wayland before the compositor resumes displaying the layer.
2026-03-20 09:36:31 -04:00
bbedward
cbf00d133a wallpaper: tweak binding again for updatesEnabled 2026-03-20 09:25:04 -04:00
purian23
347f06b758 refactor(Notepad): Streamline hide behavior & auto-save function 2026-03-19 21:42:19 -04:00
bbedward
9070903512 cleanup settings tabs 2026-03-19 20:02:46 -04:00
purian23
e9d030f6d8 (greeter): Revise dir perms and add validations 2026-03-19 19:56:18 -04:00
bbedward
fbf9e6d1b9 greeter: remove variable assignments 2026-03-19 19:55:47 -04:00
purian23
e803812344 theme(greeter): fix auto theme accent variants & update selections 2026-03-19 19:55:43 -04:00
nick-linux8
9a64f2acf0 Fix(Greeter): Fixes #1992 Changed Greetd logic to include registryThemeVariants to pull in accent color (#2000) 2026-03-19 19:55:40 -04:00
zion
c647eafadc fix(nix/greeter): skip invalid customThemeFile in preStart (#1997)
* fix(nix/greeter): skip invalid customThemeFile in preStart

Avoid attempting to copy a null/empty/missing customThemeFile path by validating the jq result and file existence before cp.

Update distro/nix/greeter.nix

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>

* nix/greeter: update customTheme verification

---------

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>
Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2026-03-19 19:55:35 -04:00
purian23
720ec07d13 (greeter): Trial fix for 30s auth delay & wireplumber state dir 2026-03-19 19:55:31 -04:00
purian23
4b4334e611 dms(policy): Restore dms greeter sync in immutable distros 2026-03-19 19:55:27 -04:00
purian23
b69a96e80b fix(greeter): add wireplumber state directory & update U2F env variables 2026-03-19 19:55:23 -04:00
purian23
1e6a73fd60 greeter(auth): Enhance fingerprint/U2F auth support w/Quickshell PAM
- Split auth capability state by lock screen and greeter
- Share detection between settings UI and lock runtime
- Broaden greeter PAM include detection across supported distros
2026-03-19 19:55:18 -04:00
purian23
60b6280750 greeter(distros): Move comps to Suggests on Debian/OpenSUSE 2026-03-19 19:53:58 -04:00
purian23
9e079f8a4b fix(greeter): Dup crash handlers 2026-03-19 19:53:54 -04:00
purian23
62c2e858ef (settings): Enhance authentication checks in Greeter & LockScreen tabs 2026-03-19 19:53:49 -04:00
purian23
78357d45bb fix(greeter): Allow empty password submits to reach PAM 2026-03-19 19:53:12 -04:00
purian23
3ff9564c9b (greeter): PAM auth improvements and defaults update 2026-03-19 19:53:09 -04:00
purian23
b0989cecad fix(Greeter): Multi-distro reliability updates
- Merge duplicate niri input/output KDL nodes instead of appending. Allows more overrides
- Guard AppArmor install/uninstall behind IsAppArmorEnabled() check
2026-03-19 19:53:04 -04:00
purian23
47be6a1033 fix(Greeter): Don't stop greeter immediately upon uninstallation 2026-03-19 19:53:01 -04:00
purian23
31b415b086 feat(Greeter): Add install/uninstall/activate cli commands & new UI opts
- AppArmor profile management
- Introduced `dms greeter uninstall` command to remove DMS greeter configuration and restore previous display manager.
- Implemented AppArmor profile installation and uninstallation for enhanced security.
2026-03-19 19:52:57 -04:00
purian23
7156e1e299 feat: Implement immutable DMS command policy
- Added pre-run checks for greeter and setup commands to enforce policy restrictions
- Created cli-policy.default.json to define blocked commands and user messages for immutable environments.
2026-03-19 19:52:52 -04:00
purian23
c72c9bfb08 greeter: New Greeter Settings UI & Sync fixes
- Add PAM Auth via GUI
- Added new sync flags
- Refactored cache directory management & many others
- Fix for wireplumber permissions
- Fix for polkit auth w/icon
- Add pam_fprintd timeout=5 to prevent 30s auth blocks when using password
2026-03-19 19:52:48 -04:00
purian23
73c75fcc2c refactor(greeter): Update auth flows and add configurable opts
- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
2026-03-19 19:50:58 -04:00
bbedward
2ff42eba41 greeter: sync power menu options 2026-03-19 19:49:26 -04:00
purian23
9f13465cd7 feat: Add independent power action confirmation settings for dms greeter 2026-03-19 19:49:23 -04:00
purian23
366a98e0cc dms-greeter: Enhance DMS Greeter dankinstall & packaging across distros
- Added support for Debian, Ubuntu, Fedora, Arch, and OpenSUSE on dankinstall / dms greeter install
2026-03-19 19:48:20 -04:00
bbedward
31aeb8dc4b wallpaper: fixes for updatesEnable handling 2026-03-19 14:24:01 -04:00
bbedward
c4e7f3d62f workspaces: ignore X scroll events
fixes #2029
2026-03-19 13:22:07 -04:00
purian23
a1d13f276a dankinstall(debian): Minor update to ARM64 support 2026-03-18 09:27:52 -04:00
bbedward
dbf132d633 launcher v2: simplify screen change bindings 2026-03-18 09:27:47 -04:00
bbedward
59451890f1 popout: fix focusing of password prompts when popout is open
undesired effect of closing the popout but its probably the best solution
2026-03-16 11:37:21 -04:00
bbedward
e633c9e039 focused app: fallback to app name if no title in compact mode fixes #2005 2026-03-16 11:37:15 -04:00
bbedward
6c1fff2df1 cc: fix invalid number displays on percentages fixes #2010 2026-03-16 11:37:10 -04:00
bbedward
3891d125d1 dankbar: guard against nil screen names 2026-03-16 11:35:11 -04:00
bbedward
997011e008 fix: missing import in Hyprland service 2026-03-13 13:26:02 -04:00
dms-ci[bot]
2504396435 nix: update vendorHash for go.mod changes 2026-03-13 16:24:54 +00:00
bbedward
d206723b36 ci: fix hardcoded branch in vendor workflow 2026-03-13 12:22:46 -04:00
bbedward
a0ec3d59b8 nix: update flake 2026-03-13 12:18:38 -04:00
bbedward
17ef08aa58 nix: fix go regex matching 2026-03-13 12:18:38 -04:00
bbedward
57279d1c53 nix: dynamically resolve go version in flake 2026-03-13 12:18:38 -04:00
bbedward
8b003ac9cd ci: reveal errors in nix vendor hash update 2026-03-13 12:17:38 -04:00
Nek
0ea10b0ad2 fix(wallpaper): preserve per-monitor cycling when changing interval (#1981)
(#1816)
2026-03-13 11:46:14 -04:00
nick-linux8
2db4c9daa0 Added Better Handling In Event Dispatcher Function (#1980) 2026-03-13 11:45:02 -04:00
bbedward
363964e90b fix(udev): avoid event loop termination core: bump go to 1.26 2026-03-13 11:45:02 -04:00
Nek
a7b49eba70 fix(matugen): detect Zed Linux binary aliases (#1982) 2026-03-13 11:44:10 -04:00
bbedward
4ae334f60f settings: allow custom json to render all theme options 2026-03-13 11:44:05 -04:00
bbedward
86c0064ff9 fix(settings): fix animation speed binding in notifications tab fixes #1974 2026-03-12 11:45:36 -04:00
Adarsh219
5a6b52f07f fix(matugen): use single quotes for zed template paths (#1972) 2026-03-12 11:45:36 -04:00
Adarsh219
5aaa56853f feat: Add Zed editor theming support (#1954)
* feat: Add Zed editor theming support

* fix formatting and switch to CONFIG_DIR
2026-03-12 11:45:31 -04:00
bbedward
35913c22f5 fix(idle): ensure timeouts can never be 0 2026-03-11 18:55:44 -04:00
purian23
d7b560573c fix(settings): Improve error handling for plugin settings loading 2026-03-11 18:55:44 -04:00
bbedward
02a274ebe2 fix(launcher): select first file search result by default fixes #1967 2026-03-11 12:47:39 -04:00
nick-linux8
fc7b61c20b Issue:(Settings)Switched Neovim Mutagen Theme To Default False (#1964)
* Issue:(Settings)Switched Neovim Mutagen Theme To Default False

* also set to false in settingsData
- this is the case when file fails to parse

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-03-11 12:44:14 -04:00
bbedward
5880043f56 fix: dsearch references 2026-03-11 12:08:24 -04:00
Triệu Kha
fee3b7f2a7 fix(appdrawer): launcher launched via appdrawer doesnt respect size (#1960)
setting
2026-03-11 10:56:21 -04:00
bbedward
c0b0339fca plugins: fix list delegates 2026-03-11 09:58:24 -04:00
bbedward
26c1e62204 fix(dankbar): use ID as tie breaker 2026-03-10 11:48:33 -04:00
purian23
7b2d4dbe30 dankinstall: Update Arch/Quickshell installation 2026-03-10 11:05:25 -04:00
CaptainSpof
78c5d46c6b fix(wallpaper): follow symlinks when scanning wallpaper directory (#1947) 2026-03-10 11:05:25 -04:00
purian23
3fb85df504 fix(Clipboard) remove unused copyServe logic 2026-03-10 11:05:00 -04:00
micko
227dd24726 update deprecated syntax (#1928) 2026-03-10 11:05:00 -04:00
purian23
ae6a656899 fix(Clipboard): Epic RAM Growth - Closes #1920 2026-03-10 11:05:00 -04:00
Connor Welsh
a4055e0f01 fix(Calendar): add missing qs.Common import (#1926)
fixes calendar events getting dropped
2026-03-10 11:05:00 -04:00
Lucas
6d98c229ef flake: allow extra QT packages in dms-shell package (#1903) 2026-03-10 11:04:01 -04:00
Michael Erdely
71d93ad85e Not everyone uses paru or yay on Arch: Support pacman command (#1900)
* Not everyone uses paru or yay on Arch: Support pacman command
* Handle sudo properly when using pacman
* Move pacman to bottom per Purian23
* Remote duplicate which -- thanks Purian23!
2026-03-10 11:04:01 -04:00
Triệu Kha
4ec21fcd3d fix(dock): Dock flickering when having cursor floating by the side (#1897) 2026-03-10 11:04:01 -04:00
Lucas
0a2fe03fee ipc: update DankBar selection (#1894)
* ipc: update DankBar selection

* ipc: use getPreferredBar in dash open

* ipc: don't toggle dash on dash open
2026-03-10 11:04:01 -04:00
Triệu Kha
4f4745609b fix(osd): play/pause icon flipped in MediaPlaybackOSD (#1889) 2026-03-10 11:03:23 -04:00
purian23
a69cd515fb fix(dbar): Fixes autohide + click through edge case 2026-03-10 11:03:23 -04:00
purian23
06c4b97a6b fix(notifications): Allow duplicate history entry management w/unique IDs & source tracking 2026-03-10 11:03:23 -04:00
purian23
a6cf71a190 fix(notifications): Apply appIdSubs to iconFrImage fallback path - Consistent with the appIcon PR changes in #1880. 2026-03-10 11:01:34 -04:00
odt
21750156dc refactor(icons): centralize icon resolution into Paths.resolveIconPath/resolveIconUrl (#1880)
Supersedes #1878. Rather than duplicating the moddedAppId + file path
substitution pattern inline across 8 files, this introduces two
centralized functions in Paths.qml:

- resolveIconPath(iconName): for Quickshell.iconPath() callsites,
  with DesktopService.resolveIconPath() fallback
- resolveIconUrl(iconName): for image://icon/ URL callsites

All consumer files now use one-line calls. When no substitutions are
configured, moddedAppId() returns the original name unchanged (zero
cost), so this has no impact on users who don't use the feature.

Affected components:
- AppIconRenderer (8 lines → 1)
- NotificationCard, NotificationPopup, HistoryNotificationCard
- DockContextMenu, AppsDockContextMenu
- LauncherContent, LauncherTab (×3)

Co-authored-by: odtgit <odtgit@taliops.com>
2026-03-10 11:00:57 -04:00
supposede
f9b737f543 Update toolbar button styles with primary color (#1879) 2026-03-10 10:55:58 -04:00
odt
246b59f3b9 fix(icons): apply file path substitutions in launcher icon resolution (#1877)
Follow-up to #1867. The launcher's AppIconRenderer used its own
Quickshell.iconPath() call without going through appIdSubstitutions,
so PWA icons configured via regex file path rules were not resolved
in the app launcher.

Co-authored-by: odtgit <odtgit@taliops.com>
2026-03-10 10:55:53 -04:00
bbedward
dcda81ea64 wallpaper: bump render settle timer 2026-03-01 10:27:10 -05:00
bbedward
9909b665cd blurred wallpaper: defer update disabling much longer 2026-02-28 15:40:18 -05:00
bbedward
4bcd786be3 wallpaper: defer updatesEnabled binding 2026-02-28 01:10:26 -05:00
bbedward
64c9222000 loginctl: add fallbacks for session discovery 2026-02-27 10:12:25 -05:00
Iris
12acf2dd51 Change IsPluggedIn logic (#1859)
Co-authored-by: Iris <iris@raidev.eu>
2026-02-27 10:12:22 -05:00
Jan Greimann
fea97b4aad Adjust SystemUpdate process (#1845)
This fixes the problem that the system update terminal closes when the package manager encounters a problem (exit code != 0), allowing the user to understand the problem.

Signed-off-by: Jan Phillip Greimann <jan.greimann@ionos.com>
2026-02-27 10:12:19 -05:00
Kangheng Liu
c6d398eeac Systray: call context menu fallback for legacy protocol (#1839)
* systray: add call contextmenu fallback

directly call dbus contextmenu method. needs refactoring to be more
robust.

* add TODO

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-27 10:12:16 -05:00
bbedward
7a74be83d7 greeter: sync power menu options 2026-02-25 14:50:47 -05:00
bbedward
67a6427418 dankdash: fix menu overlays 2026-02-25 14:50:47 -05:00
purian23
18b20d3225 feat: Add independent power action confirmation settings for dms greeter 2026-02-25 14:50:47 -05:00
bbedward
8a76885fb6 desktop widgets: fix deactive loaders when widgets disabled fixes #1813 2026-02-25 12:34:47 -05:00
bbedward
69b1e61ab7 stage 1.4.3 2026-02-25 12:34:43 -05:00
bbedward
3f24cf37ca settings: make horizontal change more smart 2026-02-24 20:49:02 -05:00
bbedward
01218f34cb settings: restore notifyHorizontalBarChanged 2026-02-24 19:43:01 -05:00
purian23
9da58d8296 fix: Update HTML rendering injections 2026-02-24 19:43:01 -05:00
purian23
af0038e634 dbar: Refactor to memoize dbar & widget state via json 2026-02-24 19:20:30 -05:00
purian23
05c312b9eb cpu widget: Fix monitor binding 2026-02-24 19:20:30 -05:00
bbedward
89d5c958c4 settings: use Image in theme colors tab wp preview 2026-02-24 15:23:07 -05:00
bbedward
e4d86ad595 popout: fully unload popout layers on close 2026-02-24 15:20:00 -05:00
bbedward
532b54a028 wallpaper: handle initial load better, add dms randr command for quick physical scale retrieval 2026-02-24 15:20:00 -05:00
bbedward
504d027c3f privacy indicator: fix width when not active 2026-02-24 13:59:58 -05:00
bbedward
e8f95f4533 settings: use Image for per-mode previews 2026-02-24 13:37:34 -05:00
bbedward
b83256c83a matugen: skip theme refreshes if no colors changed 2026-02-24 13:37:34 -05:00
bbedward
8e2cd21be8 dock: fix tooltip positioning 2026-02-24 13:37:34 -05:00
bbedward
c5413608da dankbar: fix some defaults in reset 2026-02-24 13:37:34 -05:00
bbedward
586bcad442 widgets: set updatesEnabled false on background layers, if qs supports it 2026-02-24 13:37:34 -05:00
bbedward
3b3d10f730 widgets: fix moddedAppID consistency 2026-02-24 13:37:34 -05:00
purian23
4834891b36 settings: Re-adjust dbar layout 2026-02-24 13:37:34 -05:00
purian23
f60e65aecb settings: Dankbar layout updates 2026-02-24 13:37:34 -05:00
purian23
01387b0123 fix: Clipboard button widget alignment 2026-02-24 13:37:34 -05:00
bbedward
1476658c23 dankbar: fix syncing settings to new bars 2026-02-24 13:37:34 -05:00
bbedward
7861c6e316 i18n: term sync 2026-02-24 10:52:35 -05:00
bbedward
d2247d7b24 dankbar: restore horizontal change debounce 2026-02-24 10:52:35 -05:00
bbedward
2ff78d4a02 dpms: disable fade overlay in onRequestMonitorOn 2026-02-24 10:52:35 -05:00
bbedward
785243ce5f dankbar: optimize bindings in bar window 2026-02-24 10:52:35 -05:00
bbedward
0e1b868384 widgets: fix undefined icon warnings 2026-02-24 10:52:35 -05:00
null
2b08e800e8 feat: improve icon resolution and align switcher fallback styling (#1823)
- Implement deep search icon resolution in DesktopService with runtime caching.
- Update Paths.getAppIcon to utilize enhanced resolution for mismatched app IDs.
- Align Workspace Switcher fallback icons with AppsDock visual style.
- Synchronize fallback text logic between Switcher and Dock using app names.
2026-02-24 10:52:35 -05:00
purian23
74e4f8ea1e display: Fix output config on delete & popup height 2026-02-24 10:52:35 -05:00
purian23
9c58569b4c template: Refine bug report tracker 2026-02-24 10:52:35 -05:00
purian23
29de677e00 feat: Refactor DankBar w/New granular options - New background toggles - New maxIcon & maxText widget sizes (global) - Dedicated M3 padding slider - New independent icon scale options - Updated logic to improve performance on single & dual bar modes 2026-02-24 10:52:35 -05:00
purian23
fae4944845 fix: Animated Image warnings 2026-02-24 10:52:34 -05:00
Lucas
07a0ac4b7d doctor: fix imageformats detection (#1811) 2026-02-23 19:45:10 -05:00
bbedward
b2d8f4d73b keybinds: preserve scroll position of expanded item on list change fixes #1766 2026-02-23 19:33:29 -05:00
bbedward
fe58c45233 widgets: fallback when AnimatedImage probe fails to static Image 2026-02-23 19:03:48 -05:00
bbedward
3ea4e389eb thememode: connect to loginctl PrepareForSleep event 2026-02-23 19:03:48 -05:00
purian23
7276f295fc dms-greeter: Update dankinstall greeter automation w/distro packages 2026-02-23 18:53:29 -05:00
bbedward
93ed96a789 launcher: don't tie unload to visibility 2026-02-23 18:53:29 -05:00
purian23
bea325e94c audio: Sync audio hide opts w/dash Output devices 2026-02-23 18:53:29 -05:00
bbedward
2f8f1c30ad audio: fix cycle output, improve icon resolution for sink fixes #1808 2026-02-23 18:53:29 -05:00
Lucas
f859a14173 nix: update flake.lock (#1809) 2026-02-23 18:53:29 -05:00
bbedward
153f39da48 audio: disable effects when mpris player is playing 2026-02-23 18:53:29 -05:00
bbedward
e4accdd1c7 launcher: implement memory for selected tab fixes #1806 2026-02-23 10:20:48 -05:00
dms-ci[bot]
a2c89e0a8c nix: update vendorHash for go.mod changes 2026-02-23 10:20:48 -05:00
bbedward
e282831c2e widgets: make AnimatedImage conditional in DankCircularImage - Cut potential overhead of always using AnimatedImage 2026-02-23 10:20:48 -05:00
bbedward
5c5ff6195a osd: disable media playback OSD by default 2026-02-23 10:20:48 -05:00
Triệu Kha
c4bbf54679 clipboard: fix html elements get parsed in clipboard entry (#1798)
* clipboard: fix html elements get parsed in clipboard entry

* Revert "clipboard: fix html elements get parsed in clipboard entry"

This reverts commit 52b11eeb98.

* clipboard: fix html elements get parsed in clipboard entry
2026-02-23 10:20:48 -05:00
Jonas Bloch
98acafb4b8 fix(notepad): decode path URI when saving/creating a file (#1805) 2026-02-23 10:20:48 -05:00
Jonas Bloch
da20681fc0 feat: add support for animated gifs as profile pictures (#1804) 2026-02-23 10:20:48 -05:00
purian23
b38cb961b2 dms-greeter: Enable greetd via dms greeter install all-in-one cmd 2026-02-23 10:20:48 -05:00
bbedward
7a0bb07518 matugen: unconditionally run portal sync even if matugen errors 2026-02-22 23:09:18 -05:00
purian23
403e3e90a2 dms-greeter: Enhance DMS Greeter dankinstall & packaging across distros - Added support for Debian, Ubuntu, Fedora, Arch, and OpenSUSE on dankinstall / dms greeter install 2026-02-22 23:09:18 -05:00
bbedward
50b91f14b6 launcher: fix frecency ranking in search results fixes #1799 2026-02-22 23:09:18 -05:00
bbedward
b3df47fce0 scripts: fix shellcheck 2026-02-22 23:09:18 -05:00
bbedward
09bd65d746 bluetooth: expose trust/untrust on devices 2026-02-22 23:09:18 -05:00
长夜月玩Fedora
020d56ab7f Add support for 'evernight' distribution in Fedora (#1786) 2026-02-22 23:09:18 -05:00
Triệu Kha
f3bee65da9 Fix dock visible when theres no app (#1797)
* clipboard: improve image thumbnail
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask

* dock: fix dock still visible when there's no app
2026-02-22 23:09:18 -05:00
purian23
b14b0946e2 feat: DMS Greeter packaging for Debian/OpenSUSE on OBS 2026-02-22 23:09:18 -05:00
Lucas
ca44205f1c zen: add more commands to detection (#1792) 2026-02-22 23:09:18 -05:00
purian23
2d39e8fd2a ipc: Fix DankDash Wallpaper call 2026-02-22 23:09:18 -05:00
purian23
6d4df6e927 theme: Fix Light/Dark mode portal sync 2026-02-22 23:09:18 -05:00
Connor Welsh
b8ab86e6c0 distro: add cups-pk-helper as suggested dependency (#1670) 2026-02-22 23:09:18 -05:00
bbedward
837329a6d8 window rules: default to fixed for width/height part of #1774 2026-02-22 23:09:18 -05:00
purian23
8c6c2ffd23 ubuntu: Fix dms-git Go versioning to restore builds 2026-02-22 23:09:18 -05:00
bbedward
ad3c8b6755 v1.4.3 version file 2026-02-22 23:07:18 -05:00
bbedward
03a8e1e0d5 clipboard: fix memory leak from unbounded offer maps and unguarded file reads 2026-02-20 11:42:14 -05:00
bbedward
4d4d3c20a1 keybinds/niri: fix quote preservation 2026-02-20 11:42:14 -05:00
bbedward
cef16d6bc9 dankdash: fix widgets across different bar section fixes #1764s 2026-02-20 11:42:14 -05:00
bbedward
aafaad1791 core/screenshot: light cleanups 2026-02-20 11:42:14 -05:00
Patrick Fischer
7906fdc2b0 screensaver: emit ActiveChanged on lock/unlock (#1761) 2026-02-20 11:42:14 -05:00
Triệu Kha
397650ca52 clipboard: improve image thumbnail (#1759)
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask
2026-02-20 11:42:14 -05:00
purian23
826207006a template: Default install method 2026-02-20 11:42:14 -05:00
purian23
58c2fcd31c issues: Template fix 2026-02-20 11:42:14 -05:00
purian23
b2a2b425ec templates: Fix GitHub issue labels 2026-02-20 11:42:14 -05:00
shorinkiwata
942c9c9609 feat(distros): allow CatOS to run DMS installer (#1768)
- This PR adds support for **CatOS**
- CatOS is fully compatible with Arch Linux
2026-02-20 11:42:14 -05:00
purian23
46d6e1cff3 templates: Update DMS issue formats 2026-02-20 11:42:14 -05:00
bbedward
a4137c57c1 running apps: fix ordering on niri 2026-02-19 20:46:26 -05:00
bbedward
1ad8b627f1 launcher: fix premature exit of file search fixes #1749 2026-02-19 16:47:34 -05:00
Jonas Bloch
58a02ce290 Search keybinds fixes (#1748)
* fix: close keybind cheatsheet on escape press

* feat: match all space separated words in keybind cheatsheet search
2026-02-19 16:27:14 -05:00
bbedward
8e1ad1a2be audio: fix hide device not working 2026-02-19 16:24:48 -05:00
bbedward
68cd7ab32c i18n: term sync 2026-02-19 14:11:21 -05:00
Youseffo13
f649ce9a8e Added missing i18n strings and changed reset button (#1746)
* Update it.json

* Enhance SettingsSliderRow: add resetText property and update reset button styling

* added i18n strings

* adjust reset button width to be dynamic based on content size

* added i18n strings

* Update template.json

* reverted changes

* Update it.json

* Update template.json
2026-02-19 14:11:21 -05:00
bbedward
c4df242f07 dankbar: remove behaviors from monitoring widgets 2026-02-19 14:11:21 -05:00
bbedward
26846c8d55 dgop: round computed values to match display format 2026-02-19 14:11:21 -05:00
bbedward
31b44a667c flake: fix dev flake for go 1.25 and ashellchheck 2026-02-19 14:11:21 -05:00
bbedward
4f3b73ee21 hyprland: add serial to output model generator 2026-02-19 09:22:56 -05:00
bbedward
4cfae91f02 dock: fix context menu styling fixes #1742 2026-02-19 09:22:56 -05:00
bbedward
8d947a6e95 dock: fix transparency setting fixes #1739 2026-02-19 09:22:56 -05:00
bbedward
1e84d4252c launcher: improve perf of settings search 2026-02-19 09:22:56 -05:00
bbedward
76072e1d4c launcher: always heuristic lookup cached entries 2026-02-19 09:22:56 -05:00
bbedward
6408dce4a9 launcher v2: always heuristicLookup tab actions 2026-02-18 19:07:30 -05:00
bbedward
0b2e1cca38 i18n: term updates 2026-02-18 18:35:29 -05:00
bbedward
c1bfd8c0b7 system tray: fix to take up 0 space when empty 2026-02-18 18:35:29 -05:00
Youseffo13
90ffa5833b Added Missing i18n strings (#1729)
* inverted dock visibility and position option

* added missing I18n strings

* added missing i18n strings

* added i18n strings

* Added missing i18n strings

* updated translations

* Update it.json
2026-02-18 18:35:29 -05:00
bbedward
169c669286 widgets: add openWith/toggleWith modes for dankbar widgets 2026-02-18 16:24:07 -05:00
bbedward
f8350deafc keybinds: fix escape in keybinds modal 2026-02-18 14:57:53 -05:00
bbedward
0286a1b80b launcher v2: remove calc cc: enhancements for plugins to size details 2026-02-18 14:48:44 -05:00
beluch-dev
7c3e6c1f02 fix: correct parameter name in Hyprland windowrule (no_initial_focus) (#1726)
##Description
This PR corrects the parameter name to match new Hyprland standard.

## Changes
-Before: 'noinitialfocus'
-After: 'no_initial_focus'
2026-02-18 14:48:40 -05:00
bbedward
d2d72db3c9 plugins: fix settings focus loss 2026-02-18 13:36:51 -05:00
Evgeny Zemtsov
f81f861408 handle recycled server object IDs for workspace/group handles (#1725)
When switching tabs rapidly or closing multiple tabs, the taskbar shows
"ghost" workspaces — entries with no name, no coordinates, and no active
state. The ghosts appear at positions where workspaces were removed and
then recreated by the compositor.

When a compositor removes a workspace (sends `removed` event) and the
client calls Destroy(), the proxy is marked as zombie but stays in the
Context.objects map. For server-created objects (IDs >= 0xFF000000), the
server never sends `delete_id`, so the zombie proxy persists indefinitely.

When the compositor later creates a new workspace that gets a recycled
server object ID, GetProxy() returns the old zombie proxy. The dispatch
loop in GetDispatch() checks IsZombie() and silently drops ALL events
for zombie proxies — including property events (name, id, coordinates,
state, capabilities) intended for the new workspace. This causes the
ghost workspaces with empty properties in the UI.

Fix: check IsZombie() when handling `workspace` and `workspace_group`
events that carry a `new_id` argument. If the existing proxy is a
zombie, treat it as absent and create a fresh proxy via
registerServerProxy(), which replaces the zombie in the map. Subsequent
property events are then dispatched to the live proxy.
2026-02-18 13:36:51 -05:00
bbedward
af494543f5 1.4.2: staging ground 2026-02-18 13:36:43 -05:00
bbedward
db4de55338 popout: decouple shadow from content layer 2026-02-18 10:46:01 -05:00
bbedward
37ecbbbbde popout: disable layer after animation 2026-02-18 10:34:21 -05:00
purian23
d6a6d2a438 notifications: Maintain shadow during expansion 2026-02-18 10:34:21 -05:00
purian23
bf1c6eec74 notifications: Update initial popup height surfaces 2026-02-18 10:34:21 -05:00
bbedward
0ddae80584 running apps: fix scroll events being propagated fixes #1724 2026-02-18 10:34:21 -05:00
bbedward
5c96c03bfa matugen: make v4 detection more resilient 2026-02-18 09:57:35 -05:00
bbedward
dfe36e47d8 process list: fix scaling with fonts fixes #1721 2026-02-18 09:57:35 -05:00
purian23
63e1b75e57 dankinstall: Fix Debian ARM64 detection 2026-02-18 09:57:35 -05:00
bbedward
29efdd8598 matugen: detect emacs directory fixes #1720 2026-02-18 09:57:35 -05:00
bbedward
34d03cf11b osd: optimize bindings 2026-02-18 09:57:35 -05:00
bbedward
c339389d44 screenshot: adjust cursor CLI option to be more explicit 2026-02-17 22:28:46 -05:00
bbedward
af5f6eb656 settings: workaround crash 2026-02-17 22:20:19 -05:00
purian23
a6d28e2553 notifications: Tweak animation scale & settings 2026-02-17 22:07:36 -05:00
bbedward
6213267908 settings: guard internal writes from watcher 2026-02-17 22:03:57 -05:00
bbedward
d084114149 cc: fix plugin reloading in bar position changes 2026-02-17 17:25:19 -05:00
bbedward
f6d99eca0d popout: anchor height changing popout surfaces to top and bottom 2026-02-17 17:25:19 -05:00
bbedward
722eb3289e workspaces: fix named workspace icons 2026-02-17 17:25:19 -05:00
bbedward
b7f2bdcb2d dankinstall: no_anim on dms layers 2026-02-17 17:25:19 -05:00
bbedward
11c20db6e6 1.4.1 2026-02-17 14:08:15 -05:00
bbedward
8a4e3f8bb1 system updater: fix hide no update option 2026-02-17 14:08:04 -05:00
bbedward
bc8fe97c13 launcher: fix kb navigation not always showing last delegate in view 2026-02-17 14:08:04 -05:00
bbedward
47262155aa doctor: add qt6-imageformats check 2026-02-17 14:08:04 -05:00
bbedward
dd4c41a6b2 v1.4.0 2026-02-17 12:01:36 -05:00
bbedward
92a25fdb6a process list: add all/user/system filters 2026-02-17 11:25:05 -05:00
bbedward
d6650be008 dgop service: expose username 2026-02-17 11:04:55 -05:00
bbedward
2646e7b19a launcher v2: apply transparency to footer 2026-02-17 10:54:49 -05:00
bbedward
4133f11d82 changelog: remove text note 2026-02-17 10:50:01 -05:00
bbedward
22ed740394 ripple: small tweaks to shader 2026-02-17 10:39:32 -05:00
bbedward
063299a434 cc: network tab performance improvements 2026-02-17 10:25:19 -05:00
bbedward
44d836c975 ripple: use a shader for ripple effect 2026-02-17 09:27:18 -05:00
bbedward
da437e77fb keybinds: auto-focus cheatsheet search 2026-02-17 08:44:47 -05:00
Jonas Bloch
34a6bbfb32 feat: Keybinds cheatsheet search (#1706)
* feat(wip): add fuzzy finder in keybinds cheatsheet

* fix: replace GridLayout with RowLayout and don't use anchors in KeybindsModal

* fix: replace fuzzyfinder with simple inclusion criterion for keybind search

* fix: bring back categoryKeys (there was no reason to remove it)
2026-02-17 08:42:45 -05:00
Jonas Bloch
9ed53bac9e feat: Auto settings reload (#1707)
* feat: auto-reload settings json file

* fix: set settings file reload debounce to 50ms
2026-02-17 08:41:18 -05:00
purian23
3a6752c3d2 dock: Update indicator padding 2026-02-17 07:55:33 -05:00
purian23
ef19568dd7 audio: New ability to hide input/output devices
- Updated slider presets
- Disabled mouse wheel scrolling on list scroll
2026-02-17 00:54:32 -05:00
bbedward
f280cd9d3b keybinds: dont pass dirs 2026-02-16 23:55:11 -05:00
Divya Jain
cf4ce3c476 add support for globalprotect vpn using saml auth flow (#1689)
* add support for globalprotect vpn using saml auth flow

* go fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-16 23:25:35 -05:00
bbedward
ae59e53c4c settings: fix wallpaper cycle buttons 2026-02-16 23:14:56 -05:00
dms-ci[bot]
7e0d661f63 nix: update vendorHash for go.mod changes 2026-02-17 04:02:14 +00:00
bbedward
0b33d3f905 miraclewm: add support for Miracle WM 2026-02-16 23:00:25 -05:00
purian23
d62bdda56b theme: Add Cosmic light/dark & icon theming support 2026-02-16 21:25:30 -05:00
Lucas
5841b38cd9 Update nix packaging (#1703)
* nix: add kimageformats to DMS qml dependencies

* nix: enable polkit by default in NixOS module
2026-02-16 20:47:40 -05:00
purian23
83e2b5a7a6 notifications: Tweak toast button padding 2026-02-16 19:29:57 -05:00
bbedward
2f863f64ee core: set qt platform to wayland;xcb by default 2026-02-16 18:28:31 -05:00
bbedward
1a8b397cfd weather: keep tab height consistent 2026-02-16 18:14:30 -05:00
bbedward
196c421b75 Squashed commit of the following:
commit 051b7576f7
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 16:38:45 2026 -0500

    Height for realz

commit 7784488a61
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 16:34:09 2026 -0500

    Fix height and truncate text/URLs

commit 31b328d428
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 16:25:57 2026 -0500

    notifications: handle URL encoding in markdown2html

commit dbb04f74a2
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 16:10:20 2026 -0500

    notifications: more comprehensive decoder

commit b29c7192c2
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 15:51:37 2026 -0500

    notifications: html unescape

commit 8a48fa11ec
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 15:04:33 2026 -0500

    Add expressive curve on init toast

commit ee124f5e04
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 15:02:16 2026 -0500

    Expressive curves on swipe & btn height

commit 0fce904635
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 13:40:02 2026 -0500

    Provide bottom button clearance

commit 00d3829999
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 13:24:31 2026 -0500

    notifications: cleanup popup display logic

commit fd05768059
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 01:00:55 2026 -0500

    Add Privacy Mode
    - Smoother notification expansions
    - Shadow & Privacy Toggles

commit 0dba11d845
Author: purian23 <purian23@gmail.com>
Date:   Sat Feb 14 22:48:46 2026 -0500

    Further M3 enhancements

commit 949c216964
Author: purian23 <purian23@gmail.com>
Date:   Sat Feb 14 19:59:38 2026 -0500

    Right-Click to set Rules on Notifications directly

commit 62bc25782c
Author: bbedward <bbedward@gmail.com>
Date:   Fri Feb 13 21:44:27 2026 -0500

    notifications: fix compact spacing, reveal header bar, add bottom center
    position, pointing hand cursor fix

commit ed495d4396
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 20:25:40 2026 -0500

    Tighten init toast

commit ebe38322a0
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 20:09:59 2026 -0500

    Update more m3 baselines & spacing

commit b1735bb701
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 14:10:05 2026 -0500

    Expand rules on-Click

commit 9f13546b4d
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 12:59:29 2026 -0500

    Add Notification Rules
    - Additional right-click ops
    - Allow for 3rd boy line on init notification popup

commit be133b73c7
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 10:10:03 2026 -0500

    Truncate long title in groups

commit 4fc275bead
Author: bbedward <bbedward@gmail.com>
Date:   Thu Feb 12 23:27:34 2026 -0500

    notification: expand/collapse animation adjustment

commit 00e6172a68
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:50:11 2026 -0500

    Fix global warnings

commit 0772f6deb7
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:46:40 2026 -0500

    Tweak expansion duration

commit 0ffeed3ff0
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:16:16 2026 -0500

    notifications: Update Material 3 baselines
    - New right-click to mute option
    - New independent Notification Animation settings
2026-02-16 17:57:13 -05:00
bbedward
8399d64c2d settings: drop beta from confiugration 2026-02-16 17:51:20 -05:00
bbedward
c530eab303 settings: fix dropped disconnected displays on save 2026-02-16 17:47:28 -05:00
xdenotte
45b6362dd3 fix: correct preview centering with scaling (#1701) 2026-02-16 17:47:21 -05:00
bbedward
50b77dcfc3 i18n: term update 2026-02-16 17:42:10 -05:00
bbedward
be8f3adf01 core/screensaver: add methods to introspect XML 2026-02-16 17:36:49 -05:00
bbedward
75a8c171ea launcher: remove double loader 2026-02-16 12:17:46 -05:00
bbedward
466ff59573 launcher: keep loaded default 2026-02-16 12:06:31 -05:00
bbedward
053bb91927 process list: fix clipped graphs
fixes #1697
2026-02-16 11:37:19 -05:00
bbedward
2c9b22c016 changelog: add and enable 1.4 changelog 2026-02-16 10:33:59 -05:00
Jon Rogers
a9ee91586e fix: preserve _preScored from plugin items to allow ordering control (#1696)
The _preScored property allows plugins to control the ordering of
launcher results. Previously, this property was being overwritten
with undefined during item transformation, preventing plugins from
controlling which items appear first.

This change preserves the _preScored value from plugin items in:
- transformBuiltInLauncherItem()
- transformPluginItem()

Required for: devnullvoid/dms-web-search#7
2026-02-16 00:38:10 -05:00
Kristoffer Grönlund
81bce74612 greeter: Add support for Debian greetd user/group name (#1685)
* greeter: Detect user and group used by greetd

On most distros greetd runs as user and group "greeter",
but on Debian the user and group "_greetd" are used.

* greeter: Use correct group in sync command

* greeter: more generic group detection

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-16 00:36:58 -05:00
purian23
f2a6d2c7da core: Fix DMS Greeter group check & add Cosmic support 2026-02-15 22:48:22 -05:00
bbedward
0a9a34912e wallpapers: support more image formats + case insensitivity
fixes #1694
fixes #1660
2026-02-15 16:22:27 -05:00
Higor Prado
abff670814 fix(niri): restore lazy overview spotlight lifecycle to reduce idle VRAM (#1693) 2026-02-15 15:49:55 -05:00
bbedward
0d49acaaa8 launcher: try a more targeted unload approach 2026-02-15 15:48:49 -05:00
Higor Prado
ebe1785411 fix(launcher): release DankLauncherV2 resources after close (#1692)
* fix(launcher): release DankLauncherV2 resources after close

* launcher: make unload on close optional

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-15 15:26:03 -05:00
bbedward
f9f0192b22 i18n: update terms 2026-02-15 13:40:34 -05:00
bbedward
e5cdbf4cf5 clipboard: option to paste on enter
fixes #1686
2026-02-15 13:36:19 -05:00
Higor Prado
13ef1efa7b fix(qml): optimize VRAM usage in DankRipple (#1691)
Replace layer+MultiEffect mask with GPU-native clipping for rounded
corners. The previous approach created offscreen textures for every
clickable element with rounded corners (used in 34+ files across the
UI), adding 100-300MB VRAM on NVIDIA GPUs.

The new approach uses clip: true on a Rectangle with the corner
radius, which is handled natively by the GPU without creating
intermediate textures.

Visual impact: Minimal - the ripple effect works identically.
The only theoretical difference is slightly less smooth edges on
rounded corners during the ripple animation, which is not noticeable
at 10% opacity during the quick animation.

VRAM improvement: Tested on NVIDIA, ~100-300MB reduction.
2026-02-15 13:25:45 -05:00
Sunny
fbd9301a2d fixed emacs template to work for both light and dark themes (#1682) 2026-02-15 12:22:23 -05:00
Artem
24e3024b57 fix(brightness): refresh sysfs cache on hotplug (#1674)
* fix(brightness): refresh sysfs cache on hotplug

The SysfsBackend used a cache that was never refreshed on display hot plug, causing new backlight devices to not appear in IPC until restart.

This adds Rescan() to SysfsBackend and calls it in Manager.Rescan(), matching the behavior of DDCBackend.

Fixes: hotplugged external monitor brightness control via IPC

* make fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-14 14:00:01 -05:00
bbedward
52d5af11ba dankdash: fix triggering when clock widget isnt present on bar
fixes #1601
2026-02-14 13:54:51 -05:00
bbedward
44a45b00cf widgets: cleanup rectangles across popouts, modals, OSDs 2026-02-14 11:15:26 -05:00
bbedward
2b78fe5b9f popout: remove double rectangle artifact 2026-02-14 10:45:50 -05:00
bbedward
14f92669c6 doctor: add cups-pk-helper 2026-02-14 10:38:03 -05:00
bbedward
124106de87 scrollies: switch to frame animation for kinetic scroll 2026-02-13 22:26:36 -05:00
Connor Welsh
bb8e0d384f dock: resolve icons for pre-substituted app IDs (#1669) 2026-02-13 21:40:17 -05:00
bbedward
59d37847ec osd: allow overriding layer 2026-02-13 18:04:34 -05:00
chimera
acdc531dca MangoWC and Scroll Greeter Support for NixOS (#1647)
* add mangowc greeter to nix.

i am going to be suprised if this only needed this line

* point mangowc to mango

there is no way this works

* mango flake detection and maybe scroll support

* " "

* no mango flake dependency

* mango dependency remove too

i have got to add "parenthesis" to stuff more

* Final De-dependification of MangoWC

it works without the flake YES

* mangowc -> mango pt 1

* mangowc -> mango pt 2

necessary evil. will break inital greetd confs but works after change

* Preserve Compatibility
2026-02-13 17:38:21 -05:00
bbedward
ce75dac81b track art: use URLs directly 2026-02-13 17:31:51 -05:00
bbedward
b8d40761ff network: simplify connection handling 2026-02-13 17:24:58 -05:00
bbedward
3a7430f6da osd: reverse media playback icons and handle screen changes 2026-02-13 15:43:46 -05:00
bbedward
242660c51d theme: improve handling of custom themes with variants and accents in
light/dark mode (e.g. catpuccin will react to light/dark changes and
remember theme per-mode)
fixes #1094
2026-02-13 10:31:59 -05:00
bbedward
8a6c1e45ce themes: fix overflow of option button group
fixes #1399
2026-02-13 10:22:07 -05:00
bbedward
b8e5f9f3b1 matugen: support v4 2026-02-13 09:40:51 -05:00
bbedward
d60e70f9cc notifications: fix crash in modal 2026-02-12 23:15:22 -05:00
bbedward
cdb70fadb3 launcher v2: fix kb navigation to top of scroll 2026-02-12 22:41:40 -05:00
purian23
7867deef60 dock: Fix option to use custom logos 2026-02-12 21:02:06 -05:00
bbedward
a77c1adb32 matugen: dont signal terminals when disabled
fixes #1658
2026-02-12 16:57:21 -05:00
bbedward
da14d75a3b i18n: term update 2026-02-12 15:06:23 -05:00
Bernardo Gomes
7c66a34931 fix(i18n): capture missing strings and add pt-BR translations (#1654) 2026-02-12 15:05:44 -05:00
Bernardo Gomes
425715e0f0 feat(notifications): add configurable notification rules (#1655) 2026-02-12 15:04:02 -05:00
bbedward
a3baf8ce31 running apps: fix focusing of windows when grouped 2026-02-12 14:51:10 -05:00
bbedward
605e03b065 dankbar: fix spacing at scale of running apps, dock, and system tray 2026-02-12 14:38:31 -05:00
bbedward
0e9b21d359 plugins: add plugin state helpers 2026-02-12 14:04:56 -05:00
bbedward
ba5bf0cabc i18n: general RTL fixes 2026-02-12 11:58:32 -05:00
bbedward
96b9d7aab3 ci: update go version and golangci-lint version 2026-02-12 09:57:38 -05:00
dms-ci[bot]
750e4c4527 nix: update vendorHash for go.mod changes 2026-02-12 14:50:23 +00:00
bbedward
7417e26444 core: replace go-localereader directive 2026-02-12 09:48:10 -05:00
bbedward
00e1099912 weather: light redesign for dash card 2026-02-12 09:42:29 -05:00
bbedward
bd46d29ff0 settings: optimize sidebar bindings 2026-02-11 18:42:32 -05:00
bbedward
1a9d7684b9 wallpaper: fix per-monitor view modes
fixes #1582
2026-02-11 17:58:44 -05:00
bbedward
0133c19276 dock: fix auto-hide hit area
media osd: fix showing without album art
2026-02-11 17:51:29 -05:00
ArijanJ
46bb3b613b feat: add osd toggles to search index (#1652)
* feat: add osd toggles to search index
this commit also regenerates the search index

* add newline at end of translations file

* ran prek to fix the file :)
2026-02-11 13:28:04 -05:00
bbedward
5839a5de30 displays: add full screen only for hyprland and convert vrr to dropdown
fixes #1649
fixes #1548
2026-02-11 09:31:35 -05:00
bbedward
535d0bb0f0 lock/greeter: fix keyboard layout on Hyprland
fixes #1650
fixes #672
fixes #1600
2026-02-11 08:57:24 -05:00
bbedward
4d316007af lock: add lock at startup action, not sure how to handle it in crash
scenarios
launcher v2: fix state reset in section changes
fixes #1648
2026-02-10 23:25:54 -05:00
purian23
3c2d60d8e1 fix: QT notifs warning 2026-02-10 21:38:25 -05:00
purian23
9c4f4cbd0d notifications: Add Left/Right Keyboard Nav to Current/History tabs 2026-02-10 20:51:13 -05:00
bbedward
a337585b00 core/server/dbus: suppress unsubscribe warnings 2026-02-10 17:52:39 -05:00
bbedward
1cdec5d687 launcher v2: add visibility guards 2026-02-10 17:40:41 -05:00
bbedward
081b15e24c dock: fix intelligent auto hide on hyprland
fixes #1535
2026-02-10 17:29:39 -05:00
purian23
b04cb7b3cc guide: Include Fedora paths in the Contributing guide 2026-02-10 16:07:33 -05:00
ArijanJ
e2c3ff00fb feat(ipc): add player-specific mpris volume control (#1645)
* feat: add mpris volume control through ipc

* feat: add mpris volume action and default binds
2026-02-10 15:44:56 -05:00
bbedward
c783ff3dcf core: add DL helper, apply to TrackArt OSD, DankLocationSearch
- unrelated change to add gsettingsOrDconf helpers
2026-02-10 15:42:40 -05:00
bbedward
2c360dc3e8 mautgen: post-hook reload GTK4 and qt6ct
fixes #1643
2026-02-10 15:06:44 -05:00
bbedward
5342647bfb launcher v2: performance optimizations
- Use ListView in all tab
- use filesystem cache to speed up first launch
- apply highlights to visible models
2026-02-10 14:56:29 -05:00
bbedward
46a2f6f0d8 launcher v2: de-dupe cached entries by ID 2026-02-10 12:59:53 -05:00
bbedward
f8af8fc171 processlist: fix default popout focus 2026-02-10 12:52:11 -05:00
bbedward
3d0ee9d72b animations/ripple: clean up effect and apply more universally 2026-02-10 12:48:12 -05:00
bbedward
5a0bb260b4 popout: only scale texture size on DPR > 1 2026-02-10 09:56:20 -05:00
Erwin Boskma
9a7f1f5f2f feat: configurable volume amount on scroll for Media widget (#1641)
* Add audioWheelScrollAmount setting

* Add UI to configure volume change amount for Media Player widget

* Let Media Player widget use the configured volume change on wheel event
2026-02-10 09:12:03 -05:00
purian23
d88b04fc30 notifications: Update group expansion card animations 2026-02-09 22:48:07 -05:00
bbedward
6fe4cc98b9 popout: fix blurry text 2026-02-09 21:33:12 -05:00
ArijanJ
b9bcfd8d2c Making the new media playback OSD more beautiful (#1638)
* feat: decouple track art downloads into new TrackArtService

* feat: beautify media playback osd with track art

* fix: bug when switching from art to no art
2026-02-09 21:13:23 -05:00
bbedward
e3bd31bb52 clipboard: fix row layout overflow 2026-02-09 21:09:15 -05:00
purian23
0922e3e459 clipboard: Fix pinned entry logic
- Add keyboard nav to pinned entries
- Fix wrong copied selection upon Enter
2026-02-09 20:53:48 -05:00
purian23
a168b12bb2 dankbar: Fix widget context focus w/Autohide enabled 2026-02-09 19:42:27 -05:00
bbedward
8c01deba86 anims: revise ListView animations 2026-02-09 13:21:35 -05:00
bbedward
b645487e79 cc: expand mouse areas of scroll/click targets in bar 2026-02-09 13:15:43 -05:00
bbedward
91569affd7 displays: update mango display config syntax
fixes #1629
2026-02-09 09:49:31 -05:00
bbedward
1ed44ee6f3 audio: add per-device max volume limit setting 2026-02-09 09:26:34 -05:00
bbedward
fce120fa31 system monitor: disable anims until list is stable 2026-02-08 22:16:45 -05:00
bbedward
a02b0c0c3c animations: tweak list view transitions to not animate X 2026-02-08 22:10:51 -05:00
bbedward
c86999f389 popout: move layer to content wrapper 2026-02-08 22:06:35 -05:00
bbedward
2b546967d2 weather: fix anim on open 2026-02-08 21:43:56 -05:00
purian23
591d2ba4d4 settings: DankCollapsible 2026-02-08 20:26:06 -05:00
purian23
37cc4ab197 dms: Material Animation Refactor
- Thanks Google for Material 3 Expressive stuffs
- Thanks Caelestia shell for pushing qml limits to showcase the blueprint
2026-02-08 20:24:37 -05:00
Jhannes Reimann
d775974a90 i18n: wrap missing user-facing strings in I18n.tr() (#1624)
* fix: reverse rotation direction of sync icon in plugin browser

* i18n: wrap missing user-facing strings in I18n.tr()
2026-02-08 19:18:17 -05:00
Vladimir Kosteley
cc62aa4a9e fix: VpnPopout and Vpn widget tooltip positions (#1623)
* fix(vpn-widget): correct tooltip positioning for bottom bar alignment

- Calculate tooltip Y position based on bar edge (bottom vs top)
- Position tooltip above bar when edge is bottom to prevent overflow
- Account for tooltip height when positioning on bottom edge
- Use proper screen reference for consistent positioning across displays

* fix(VpnPopout): use correct screen height for popup sizing

- Fix popup height calculation to use the assigned screen property
  instead of the global Screen object
- Prevents incorrect positioning when multiple screens are present
- Fallback to Screen.height if screen property is not set

* fix(widgets): close DankPopout when screen is removed

- Add Connections handler to monitor Quickshell.onScreensChanged
- Automatically close popout if its assigned screen no longer exists
- Prevent orphaned popouts when displays are disconnected
2026-02-08 19:17:53 -05:00
bbedward
5b8b7b04be niri: replace github ref 2026-02-08 10:21:34 -05:00
Tulip Blossom
b4a8853591 fix(greeter): use 0755 permissions for greeter directories (#1619)
* fix(greeter): use 0755 permissions for greeter directories

Directories need to be executable to read/write to them (weirdly enough)

Fixes: https://github.com/AvengeMedia/DankMaterialShell/issues/1618

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>

* fix: also need 4 for world writeable perms on DMS greeter directory

---------

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>
2026-02-07 22:25:39 -05:00
Jhannes Reimann
4220dfe2a5 fix: reverse rotation direction of sync icon in plugin browser (#1617) 2026-02-07 18:53:00 -05:00
bbedward
4557426c28 notifications: fix kb navigation breaking on history tab close 2026-02-07 17:42:20 -05:00
purian23
8ee7fe8e66 notifications: Refactor Animations 2026-02-07 17:25:26 -05:00
bri
c4a41f994a feat(dms-greeter): add Niri override kdl includes (#1616)
This will optionally include one or two files into the Niri conf for
dms-greeter:
  - `/usr/share/greetd/niri_overrides.kdl` (for building into a system image)
  - `/etc/greetd/niri_overrides.kdl` (for local overrides)

This enables a distro or a system administrator to easily add their own
customizations to the dms-greeter Niri configuration.

Note: Niri next includes https://github.com/YaLTeR/niri/pull/3022 which
can make this a whole lot simpler: we're testing for the existence of
files and optionally adding `include` lines within the dms-greeter shell
script, but in the future it will be possible to simply use `include
optional=true` which we can just append unconditionally.

Lastly, I deduplicated the line that spawns the actual greeter by
appending that unconditionally at the end.
2026-02-07 12:45:13 -05:00
bbedward
fa639424f5 core/config: update default steam window rules
fixes #1615
2026-02-07 11:43:34 -05:00
bbedward
e618a8390c launcher v2: fix hover effect and view mode pref in all tab 2026-02-06 11:53:59 -05:00
bbedward
393e9ed2e4 greeter: try to fix random nixos issue 2026-02-06 10:56:44 -05:00
bbedward
e1ea441215 cava: use input source pipwire and auto 2026-02-06 10:12:16 -05:00
bbedward
654661fd66 cli/setup: add subcommands for individual includes 2026-02-06 09:53:41 -05:00
bbedward
c5a21f8da0 niri: ensure other configs too 2026-02-06 08:20:17 -05:00
bbedward
ca5b168117 niri: add ensure colors.kdl existence
fixes #1606
2026-02-06 08:18:37 -05:00
bbedward
aa88eb42ee cava: remove input config 2026-02-06 08:10:48 -05:00
purian23
ac84cadd77 fix: Truncate Media Playback OSD 2026-02-05 22:21:45 -05:00
ArijanJ
81d5235b9f feat: media playback OSD (#1602)
* feat: media playback OSD

* Update code style improvements
- Removes resetTimer to reuse event driven code

---------

Co-authored-by: purian23 <purian23@gmail.com>
2026-02-05 19:18:52 -05:00
bbedward
8944762c76 notifications: cap max anim speed in popout 2026-02-05 15:17:17 -05:00
bbedward
3d05c34673 i18n: sync terms 2026-02-05 14:38:17 -05:00
bbedward
c2ee41c844 running apps: make settings bar-specific 2026-02-05 14:37:05 -05:00
bbedward
6b537f30a5 matugen: sync adwaita accent color by visual similarity 2026-02-05 14:16:30 -05:00
bbedward
a3ae95df09 launcher v2: general performance improvements 2026-02-05 13:22:49 -05:00
bbedward
4349d68f87 greeter: block loading of memory file 2026-02-05 12:18:44 -05:00
bbedward
7d5c20125a animations: fine-grained anim settings for modals and popouts 2026-02-05 12:10:17 -05:00
bbedward
2583dbd3f2 niri/keybinds: expose when-locked, inhibitied, repeat through GUI editor
fixes #1437
2026-02-05 11:55:25 -05:00
bbedward
a103b93583 greeter: add connection on session load 2026-02-05 09:40:27 -05:00
claymorwan
fff018eafb fix(layers): change layers namespace for desktop widgets and plugin popouts (#1594) 2026-02-05 09:18:47 -05:00
bbedward
60b824e7a4 i18n: update settings search index 2026-02-05 09:12:08 -05:00
bbedward
e27e904157 doctor: add --copy option for github issue reporting 2026-02-04 19:33:22 -05:00
bbedward
fe15667986 clipboard: add watch -m for mime-types 2026-02-04 11:36:41 -05:00
bbedward
bd9029e533 niri: support any screenshot editor tool 2026-02-04 11:05:52 -05:00
bbedward
fa71d563ea vpn: uncheck "save password" by defaul 2026-02-04 10:55:06 -05:00
bbedward
143918bc5e plugins: fix reload IPC on failure 2026-02-04 09:46:10 -05:00
purian23
961680af8c feat: Alias for Audio Devices
- New custom audio UI to set custom names for input/output devices
2026-02-04 07:09:55 -05:00
purian23
6e3b3ce888 fix: Notepad Transparency override 2026-02-03 21:19:57 -05:00
bbedward
44292c3b55 theme: fix popup transparency setting 2026-02-03 21:06:15 -05:00
bbedward
c024c1b8e4 matugen: fix emacs template
fixes #1580
2026-02-03 16:15:13 -05:00
bbedward
13adfdec11 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2026-02-03 16:04:11 -05:00
bbedward
e9ec28aab7 core/greeter: embed base config, tie enable into sync, sync config
argument always, preserve existing args
2026-02-03 16:02:42 -05:00
purian23
0af4d1d6e3 greeter: Restore baseline configs 2026-02-03 15:52:07 -05:00
purian23
3ef0e63533 feat: DMS Greeter Sync w/niri include settings
- cursor, debug, input & options
- This lets the greeter inherit your niri display/output layout, input device behavior, cursor settings & debug flags overrides
2026-02-03 14:57:29 -05:00
purian23
f4dad69ccd General agent files 2026-02-03 14:56:56 -05:00
bbedward
b811316d0c DankPopout: make bg and content siblings 2026-02-03 14:31:31 -05:00
bbedward
f59aeb2782 dankbar: fix centering of numerous bar widgets 2026-02-03 13:44:57 -05:00
bbedward
24ce41935e bluetooth: improve performance of details 2026-02-03 11:30:38 -05:00
bbedward
3c4749ead0 widgets: add a button color setting 2026-02-03 11:03:33 -05:00
xxyangyoulin
22ab5b9660 feat(ipc): add tray icon control commands (#1576)
Add IPC commands to interact with system tray icons:
  - `dms ipc call tray list` - list all tray items
  - `dms ipc call tray activate <id>` - activate (left-click) a tray item
  - `dms ipc call tray status <id>` - show tray item details
2026-02-03 09:36:35 -05:00
bbedward
22f16f1da3 widgets: theme text field selection color 2026-02-03 09:35:35 -05:00
bbedward
a97409dfd7 window: freeze mask geometry in popout 2026-02-02 16:29:59 -05:00
grokXcopilot
eaa6a664c8 feat(niri): Add drag-and-drop workspace reordering (#1569)
* feat(niri): Add drag-and-drop workspace reordering

Add interactive drag-and-drop reordering for Niri workspace indicators
with smooth animations matching the system tray behavior.

- Add moveWorkspaceToIndex() to NiriService for workspace reordering
- Implement drag detection with 5px threshold
- Add shift animation for items between source and target
- Clamp drag offset to stay within workspace row bounds
- Reset drag state when workspace list changes during drag
- Visual feedback: opacity change, border highlight on drag/drop target

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(settings): Add workspace drag reorder toggle

Add workspaceDragReorder setting to enable/disable workspace
drag-and-drop reordering. Enabled by default, only visible on Niri.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:58:05 -05:00
bbedward
d934b3b3b4 launcher v2: improve search result responsiveness, highlight matches 2026-02-02 12:49:20 -05:00
Mmmattias
546cbfb3ca wallpaper: Only pause cycling when screen is locked or active window is fullscreen (#1553) 2026-02-01 20:47:13 -05:00
bbedward
39b70a53a0 cursor: more intelligent Xresources editing 2026-02-01 20:44:21 -05:00
bbedward
795f84adce notifications: handle material icons 2026-02-01 20:39:15 -05:00
purian23
3d80a9dd0f appsDock: Update Size & Color options 2026-02-01 00:22:08 -05:00
purian23
9669e9bc87 fix: Extend Blur Overview edge to edge 2026-01-31 22:16:13 -05:00
purian23
5f2a5a5d7d distro: Update DMS/OBS versioning 2026-01-31 20:20:19 -05:00
johngalt
ecfd721fc0 Zen Browser Theme: fixing background color in template (#1557) 2026-01-31 15:58:03 -05:00
purian23
07242a00b3 fix: Update DankDropdown & Clipboard Pins 2026-01-31 13:40:15 -05:00
Rin
4602442feb feat: add ipc handlers for color picker modal (#1554)
* dankcolorpickermodal: add ipc handlers

* add ipc docs for color picker modal
2026-01-30 22:35:02 -05:00
Higor Prado
a90717b20c Fix Process List popout crash from AppSearch (#1552) 2026-01-30 13:45:33 -05:00
bbedward
02edce2999 plugins: represent featured plugins in built-in browsers 2026-01-30 13:31:12 -05:00
bbedward
f2d9066f90 clipboard: add popout variant 2026-01-30 13:24:05 -05:00
bbedward
f6f7b1ed72 polkit: allow empty passwords 2026-01-30 09:19:10 -05:00
bbedward
803bc1cb7f system tray: allow re-ordering tray items 2026-01-30 09:17:01 -05:00
bbedward
67d3aa9da3 system tray: use id+title as identifier
fixes #1542
2026-01-29 22:01:33 -05:00
purian23
9fbff5e833 feat: Notepad widget quick context menu 2026-01-29 18:51:49 -05:00
purian23
c371140a97 feat: Clipboard widget context quick menu 2026-01-29 18:12:56 -05:00
bbedward
c755a3719d core/windowrules: disable hyprland from CLI 2026-01-29 13:12:17 -05:00
bbedward
4f153f3026 settings: remove bad text 2026-01-29 13:07:06 -05:00
bbedward
f2b1dbd256 greeter: pass --unsupported-gpu to sway 2026-01-29 12:39:06 -05:00
bbedward
be0ca993ff clipboard: add raw image mime-type to offers in CopyFile 2026-01-29 09:46:01 -05:00
bbedward
ed87e1b00b i18n: add Dutch 2026-01-29 09:35:23 -05:00
bbedward
ac509933d7 i18n: fix sound missing erorr message 2026-01-28 23:35:53 -05:00
bbedward
f49f98ff85 settings: undo mono font filtering 2026-01-28 22:07:08 -05:00
bbedward
10923346d7 clipboard: fix watch command 2026-01-28 21:36:16 -05:00
bbedward
f27bffc387 displays: add disable snap option in settings
fixes #1438
2026-01-28 21:08:16 -05:00
bbedward
36b43f93a3 displays: support for multiple output profiles
- add support for deleting unplugged configs
- Option to hide disconnected displays
fixes #1453
2026-01-28 20:51:29 -05:00
bbedward
2deeab9d08 clipboard: touch copied history entry
- makes it appear at the top of the history
2026-01-28 16:09:32 -05:00
bbedward
f00854879c workspaces: fix overflow with grouped apps + icons
fixes #1530
2026-01-28 16:00:44 -05:00
bbedward
75fd62865b core/dbus: fix arg types in calls 2026-01-28 15:25:16 -05:00
bbedward
757054e140 core/dbus: support Normalize for more dbus types 2026-01-28 13:28:13 -05:00
bbedward
eda59b348c clipboard: react to changes 2026-01-27 22:50:28 -05:00
bbedward
d19e81ffac clipboard: fix duplicate clear dialog 2026-01-27 22:41:01 -05:00
purian23
60c6872aec workflow: Update dms-git run times 2026-01-27 22:38:09 -05:00
bbedward
a9cb2fe912 clipboard: fix hash duplication check, set isOwner for CopyFile 2026-01-27 22:35:20 -05:00
purian23
a168a8160c feat: appsDock Widget Overflow & Config Options 2026-01-27 21:15:33 -05:00
bbedward
78662f9613 window-rules: fix checkbox alignment 2026-01-27 19:44:17 -05:00
bbedward
d9d7bb8dcc i18n: update settings search index 2026-01-27 19:39:29 -05:00
bbedward
3136f48b30 settings: make dock position match dankbar
fixes #1527
2026-01-27 19:33:32 -05:00
sin-1337
0c46711b01 Update Makefile (#1524)
Stop assuming the user's primary group matches their username.
2026-01-27 19:29:17 -05:00
bbedward
68159b5c41 niri: add window-rule management
- settings UI for creating, editing, deleting window ruels
- IPC to create a window rule for the currently focused toplevel

fixes #1292
2026-01-27 19:28:58 -05:00
purian23
6557d66f94 dms-git: It shall be beta 2026-01-27 17:56:08 -05:00
purian23
9553cb06d3 feat: Dock Overflow/Updated Settings Options 2026-01-27 00:52:15 -05:00
bbedward
122fb16dfb clipboard: simplify copyFile, fix copy image from history 2026-01-26 21:49:34 -05:00
niz
511502220f keyboard-layout: fixed hyprland keyboard compact mode (#1512) 2026-01-26 18:07:09 -05:00
bbedward
8bfe7439c0 ci: fix pre-commit go path 2026-01-26 18:01:21 -05:00
bbedward
8499033221 clipboard: fix file transfer & export functionality
- grants read to all installed flatpak apps
2026-01-26 17:58:06 -05:00
bbedward
705d5b04dd pre-commit: add go mod tidy 2026-01-26 16:46:48 -05:00
dms-ci[bot]
17eaa761f8 nix: update vendorHash for go.mod changes 2026-01-26 21:46:10 +00:00
bbedward
1cdbd01748 go mod tidy 2026-01-26 16:44:32 -05:00
bbedward
08cc076a4c clipboard: skip application/vnd.portal.filetransfer mime in history 2026-01-26 16:40:56 -05:00
bbedward
2a02d5594c clipboard: add cl copy --download option for images/videos
- offers application/vnd.portal.filetransfer and text/uri-list
2026-01-26 16:34:47 -05:00
bbedward
2263338878 dankbar: account for outlineThickness in margins
settings: dont clear caches or apply on startup
2026-01-26 14:19:26 -05:00
bbedward
26bc5425d3 displays: fix vrr=0 setting on hyprland 2026-01-26 11:00:37 -05:00
Karan Singh
38b4d1dc95 Disable VRR in hyprland configuration (#1509)
VRR disabled by default.
2026-01-26 10:57:34 -05:00
bbedward
3aaca7ff39 theme: allow overriding color center theme 2026-01-26 09:18:14 -05:00
bbedward
83d9808536 workspaces: add icon size offset 2026-01-25 22:49:46 -05:00
bbedward
ad458dfece clock: fix no shifting logic 2026-01-25 15:53:01 -05:00
bbedward
8f6fe7ed2b i18n: sync 2026-01-25 14:31:28 -05:00
bbedward
419a692593 update template for feature request 2026-01-25 14:23:05 -05:00
bbedward
03fdf795e0 launcher v2: general styling fixes
- scrollbar
- footer alignment
- radii
- hover colors
2026-01-25 14:21:03 -05:00
bbedward
832807a217 desktop clock: general scaling and stylng fixes for digital variant 2026-01-25 13:30:11 -05:00
Yamada.Kazuyoshi
f7df3b2a68 Fixed an issue where the UI width was shifted due to the clock widget when using non-monospaced fonts. (#1491) 2026-01-24 23:09:20 -05:00
bbedward
0d03e73595 fix vesktop theme name 2026-01-24 22:53:37 -05:00
bbedward
c5ae1a77d3 settings: sidebar scaling improvements 2026-01-24 22:51:59 -05:00
bbedward
5f16624000 misc: fix some various scaling issues with fonts
fixes #1268
2026-01-24 22:27:23 -05:00
purian23
80025804ab theme: Tweaks to Auto Color Mode 2026-01-24 20:43:45 -05:00
bbedward
028d3b4e61 workspaces: fix index numbers with show apps on vBar + animation 2026-01-24 20:31:45 -05:00
purian23
9cce5ccfe6 autoThemeMode: Add transition time & layout update 2026-01-24 19:33:37 -05:00
purian23
a260b8060e Merge branch 'master' into auto-theme 2026-01-24 18:19:13 -05:00
purian23
f945307232 cleanup: Auto theme switcher 2026-01-24 17:48:34 -05:00
bbedward
8f44d52cb2 launcher v2: allow categories in plugins 2026-01-24 16:58:55 -05:00
purian23
3413cb7b89 feat: Create new Auto theme mode based on region / time of day 2026-01-24 16:38:45 -05:00
bbedward
4e3b24ffbb settings: migrate vpnLastConnected to session
fixes #1488
2026-01-24 16:08:15 -05:00
bbedward
03cfa55e0b ipc: ass toast IPCs
fixes #964
2026-01-24 12:53:51 -05:00
bbedward
a887e60f40 keybinds: fix MangoWC config traversal in provider
fixes #1464
2026-01-24 12:23:59 -05:00
bbedward
816819bf9f dankinstall: fix xero color typo 2026-01-23 23:10:24 -05:00
bbedward
78f3bb3812 dankinstall: support XeroLinux
fixes #1474
2026-01-23 22:39:14 -05:00
bbedward
01d7ed5dd8 launcher v2: ability to toggle visibility in modal 2026-01-23 22:17:35 -05:00
Lucas
50311db280 nix: add qt-imageformats to DMS qml dependencies (#1479)
* nix: add qt-imageformats to DMS qml dependencies

* nix: update flake.lock
2026-01-23 21:53:35 -05:00
bbedward
01b1a276c5 launcher v2: support ScreenCopy in tiles 2026-01-23 21:29:48 -05:00
IChengHo
6d4c31492c fix: pass query string to launcher v2 during IPC toggle (#1477)
Ensure toggleQuery forwards the query parameter to the launcher v2
2026-01-23 19:43:42 -05:00
Jon Rogers
f8c5f07e9f Fix: Add view mode persistence for xdg-open picker modals (#1465)
* fix: Add browserPickerViewMode persistence to settings spec

The BrowserPickerModal (used by xdg-open feature) was not persisting
view mode selection between sessions. While the modal had code to save
the view mode preference, the browserPickerViewMode property was not
registered in SettingsSpec.js, preventing it from being saved to disk.

Added browserPickerViewMode and browserUsageHistory to SettingsSpec.js
to ensure user's view preference (list/grid) is properly persisted.

Fixes view mode reverting to grid after restarting DMS/QuickShell.

* fix: Add view mode persistence for both browser and file pickers

Extended the fix to include both picker modals used by xdg-open:

BrowserPickerModal (URLs):
- Added browserPickerViewMode and browserUsageHistory to SettingsSpec.js
- Already had save logic in BrowserPickerModal.qml

AppPickerModal/filePickerModal (files):
- Added appPickerViewMode and filePickerUsageHistory to SettingsSpec.js
- Added appPickerViewMode and filePickerUsageHistory properties to SettingsData.qml
- Added viewMode binding and onViewModeChanged handler to filePickerModal

Both modals now properly persist user's view preference (list/grid) and
usage history between sessions.

Fixes view mode reverting to default grid after restarting DMS/QuickShell
for both 'dms open https://...' and 'dms open file.pdf' workflows.
2026-01-23 19:39:13 -05:00
Ethan Todd
11e23feb0e lockscreen/greetd: add 0 in front of single digit hours for 12 hour format. greetd: add option to hide profile image (#1247)
* greetd: add lockScreenShowProfileImage option

* lockscreen/greetd: for non 24 hour formats, add 0 in front of single digit hours to ensure that everything is always centered properly - previously, it would only appear centered if on a double digit hour. also add getEffectiveTimeFormat function to GreetdSettings.

* clock: made pad 12 hour formats optional

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-23 14:47:59 -05:00
bbedward
b4ba2dac37 launcher v2: fix nvidia dgpu race condition 2026-01-23 14:15:46 -05:00
bbedward
d013c3b718 workspace: fix rename modal 2026-01-23 14:03:02 -05:00
Kamil Chmielewski
b3ea28c5c4 feat: add workspace rename dialog (#1429)
* feat: add workspace rename dialog

- Adds a modal dialog to rename the current workspace
- Supports both Niri (via IPC socket) and Hyprland (via hyprctl dispatch)
- Default keybinding: Ctrl+Shift+R to open the dialog
- Pre-fills with current workspace name
- Allows setting empty name to reset to default

* refactor: wrap WorkspaceRenameModal in LazyLoader

Reduces memory footprint when the modal is not in use.
2026-01-23 13:46:34 -05:00
bbedward
775b381987 lock: add disable media player option
fixes #1470
2026-01-23 13:34:25 -05:00
bbedward
3a41f2f1ed greeter+lock: remove random facts
fixes #1475
2026-01-23 13:25:42 -05:00
bbedward
972fc534a4 meta: support async launcher plugins, cached GIFs, paste on launcher v2
action
- Preparations for DankGifSearch plugin
2026-01-23 12:03:05 -05:00
purian23
808ee66e11 feat: AppsDock Widget on the Dankbar
- Pinnable apps independent from the main dock
- Drag & Drop support
2026-01-23 11:49:45 -05:00
bbedward
3936a516f8 lock: fix loginctl lock integration disabled setting
fixes #1471
2026-01-23 09:56:43 -05:00
purian23
15dc91f779 dock: Fix dock launcher button persistence 2026-01-22 18:15:00 -05:00
bbedward
dd3d2908a2 prek format 2026-01-22 17:57:12 -05:00
bbedward
0857023dba core: ipc fill in help, remove management tui 2026-01-22 17:51:38 -05:00
purian23
1edc8f468e feat: Pinnable DMS coreApps w/Color options 2026-01-22 17:45:38 -05:00
purian23
2681fe87bb feat: Implement Dank Launcher button on the Dock
- Configurable with custom icons/logos
- Respects light/dark theme
- Drag & Drop in place
2026-01-22 16:52:38 -05:00
bbedward
3f0d0f4d95 launcher v2: remove dupe launch on dGPU 2026-01-22 14:52:36 -05:00
bbedward
f24ecf1b99 weather: m/s wind units and feels like
fixes #1463
fixes #1456
2026-01-22 14:44:40 -05:00
bbedward
acdd1d2ec4 settings: fix theme flavor buttons 2026-01-22 13:58:44 -05:00
bbedward
d08496f237 launcher v2: add micro size 2026-01-22 10:10:19 -05:00
bbedward
27b4e0221b settings: fix emacs syntax err 2026-01-22 09:35:23 -05:00
Sunny
496ace0cd4 add dank emacs template (#1460)
* add dank emacs template

* prek

* prek ws

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-22 09:25:02 -05:00
bbedward
f61ed8b8a6 launcher v2: reduce debounce 2026-01-22 09:20:00 -05:00
bbedward
41ee88a3cf launcher v2: keep old namesapce 2026-01-22 09:06:04 -05:00
purian23
6bf1438ef1 fix: dms chroma hang on print 2026-01-21 22:47:53 -05:00
bbedward
b819306ab6 launcher v2: use Top layer by default 2026-01-21 21:59:38 -05:00
bbedward
b140afca8e launcher v2: retire spotlight launcher in favor of dank launcher 2026-01-21 21:34:31 -05:00
bbedward
6735989455 launcher v2: reset visibility on screen change 2026-01-21 19:29:03 -05:00
bbedward
db37ac24c7 launcher v2: support CachingImage in icon renderer 2026-01-21 17:54:36 -05:00
bbedward
0231270f9e launcher v2: use AppIconRenderer from legacy launcha 2026-01-21 17:51:24 -05:00
bbedward
b5194aa9e1 notifications: update dimensions and text expansion logic 2026-01-21 16:51:39 -05:00
bbedward
ea0ffaacb0 launcher v2: fix some plugin icon handling 2026-01-21 16:09:52 -05:00
bbedward
3b1f084a13 notepad: fix unsave changed dialog height 2026-01-21 16:01:59 -05:00
bbedward
39a9e3a89f add dms doctor to issue template 2026-01-21 14:25:41 -05:00
bbedward
7a7af775c2 launcher v2: some optims on meta performance
- limit plugin results to 10
- longer debounce
- search plugins when chars > 1
2026-01-21 14:20:12 -05:00
bbedward
6ac2a305f7 launcher v2: sort order preference for plugin results 2026-01-21 14:08:40 -05:00
bbedward
3507c6cec3 i18n: RTL fixes in about tab and dank bar settings 2026-01-21 11:57:46 -05:00
purian23
3ff00768ac core: dms chroma notepad updates 2026-01-21 11:48:08 -05:00
bbedward
556d253ea8 launcher v2: fix view mode persistence 2026-01-21 11:43:02 -05:00
bbedward
3922070488 launcher v2: meta improvements
- Allow disabling each plugin from "all" mode
- add IPCs for toggling specific modes
- niri: overview respect size & default to apps mode
- fix unicode icon handling
2026-01-21 11:38:48 -05:00
Eggrror404
eebb4827c4 feat(bar): enlarge bar icons if widget background is off (#1425)
* use iconSizeLarge if noBackground is on

* widgets: pass noBackground to barIconSize in param
2026-01-21 10:44:08 -05:00
Kamil Chmielewski
fd2c6a0784 Feat/niri workspace names (#1396)
* dankbar: show niri workspace names

Keep labels aligned with niri indices and live renames.

* dankbar: prefix named workspaces with index

Use workspace index toggle to show index: name labels.

* workspaces: change size conditions for workspace names

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-21 10:43:55 -05:00
bbedward
417bf37515 clipboard: fix header GUI and add tooltips 2026-01-21 10:19:52 -05:00
bbedward
132e799265 Revert "settings: fix modal not opening on latest quickshell (#1357)"
This reverts commit bdd01e335d.
2026-01-21 09:19:12 -05:00
dms-ci[bot]
bdc864781b nix: update vendorHash for go.mod changes 2026-01-21 14:18:47 +00:00
purian23
a343bc7562 feat: DMS Core Chroma Syntax Highlighter
- Thanks alecthomas for the project
2026-01-21 09:16:58 -05:00
bbedward
1f2e231386 launcher v2: fix context switch back on empty text field 2026-01-20 21:57:50 -05:00
bbedward
0e7f628c4a launcher v2: improve danksearch context switching behavior 2026-01-20 21:55:05 -05:00
bbedward
553f5257b3 launcher v2: general padding improvements, to more than just launcher v2
but yea
2026-01-20 21:46:02 -05:00
bbedward
80ce6aa19c launcher v2: spacing adjustments 2026-01-20 18:10:55 -05:00
bbedward
2b2977de4a launcher v2: smarter right/left arrow key handler 2026-01-20 18:02:23 -05:00
bbedward
1d5d876e16 launcher: Dank Launcher V2 (beta)
- Aggregate plugins/extensions in new "all" tab
- Quick tab actions
- New tile mode for results
- Plugins can enforce/require view mode, or set preferred default
- Danksearch under "files" category
2026-01-20 17:59:13 -05:00
Body
3c39162016 remove hardcoded width and padding fixing overlap (#1446) 2026-01-20 16:19:59 -05:00
bbedward
d38767fb5a settings: fix power&sleep tab button groups
fixes #1442
2026-01-20 11:41:39 -05:00
purian23
f2be6cfeb1 notepad: Update cursor color & activity 2026-01-19 00:34:10 -05:00
purian23
65486ed3cf notepad: QOL updates 2026-01-18 23:49:38 -05:00
bbedward
cc30e2a9e4 workspaces: fix occupied color overridworkspacs: fix occupied color
overridee
2026-01-18 22:44:54 -05:00
bbedward
ac68451cdf processlist: add full keyboard navigation 2026-01-18 21:03:34 -05:00
bbedward
0f6ae11c3d launcher: add name, icon, description overrides + hide/unhide options
- convenient helpers without needing to make .desktop overrides
fixes #1329
2026-01-18 20:30:50 -05:00
bbedward
7cb39f00ad i18n: add french 2026-01-18 14:24:20 -05:00
bbedward
f313d03348 dankbar: add click-through option 2026-01-18 14:22:50 -05:00
Eggrror404
1adbf3937b add option to change occupied workspace color (#1427) 2026-01-18 13:25:37 -05:00
Kamil Chmielewski
a685d9da52 feat: power off monitors when lock screen activates (#1402)
Add ability to immediately power off monitors when the lock screen
activates, controlled by a new setting "Power off monitors on lock".
Uses a 100ms polling timer to detect when the session lock actually
becomes active, then invokes compositor-specific DPMS commands.

For niri, uses the new power-off-monitors action via niri msg CLI
with socket fallback.

Wake on input: first input after lock arms wake, second input
actually powers monitors back on while keeping the lock screen visible.

Closes #1157
2026-01-18 13:08:58 -05:00
bbedward
13dededcc9 Makefile: don't overwrite VERSION file 2026-01-17 23:21:11 -05:00
bbedward
3bed2d9feb plugins: give popout customizable header actions 2026-01-17 22:43:10 -05:00
purian23
7241877995 feat: Intelligent Dock Auto-hide 2026-01-17 22:20:15 -05:00
dms-ci[bot]
340d79000c nix: update vendorHash for go.mod changes 2026-01-18 03:08:19 +00:00
bbedward
162ec909da core/server: add generic dbus service
- Add QML client with subscribe/introspect/getprop/setprop/call
- Add CLI helper `dms notify` that allows async calls with action
  handlers.
2026-01-17 22:04:58 -05:00
purian23
53f5240d41 notepad: Fix open/save modals 2026-01-17 15:23:57 -05:00
bbedward
27f0df07af widgets: refresh layout on plugin load
fixes #1414
2026-01-17 12:27:24 -05:00
Jon Rogers
ad940b5884 feat(plugins): Add toggle support with lazy daemon instantiation (#1407)
Add togglePlugin() function and IPC command to toggle plugin visibility,
particularly for slideout-capable daemon plugins like AI Assistant.

Implementation uses lazy instantiation for daemon plugins:
- Daemons remain uninstantiated on load (respecting lifecycle)
- First toggle() call instantiates the daemon on-demand
- Subsequent toggles use the existing instance
- Prevents duplicate instantiation while supporting toggle functionality

This approach preserves the fix from f9b9d986 (ensure daemon plugins
not instantiated twice) while enabling new toggle capabilities.

Changes:
- Add PluginService.togglePlugin() with lazy instantiation
- Add DMSShellIPC plugin.toggle() command
- Maintains compatibility with existing daemon plugins
2026-01-17 12:05:04 -05:00
purian23
ec8ab47462 distros: Deprecate Cliphist dependencies 2026-01-17 01:06:28 -05:00
purian23
35cbfeb008 feat: Save Pinned Clipboard entries 2026-01-17 00:52:47 -05:00
bbedward
7036362b9b dgop: fix default sort direction 2026-01-16 21:04:44 -05:00
bbedward
2bcb33e85c system monitor: update gauge sizes 2026-01-16 20:28:57 -05:00
bbedward
76ac036f85 system monitor: overhaul popout and app with new design 2026-01-16 20:20:03 -05:00
bbedward
581073394a dank16: update algorithm overall
- More similarities to primary, smoother gradient of
  cyan->purple->magenta->white, keep gray
- Make purple slot near-match for primaryContainer
2026-01-16 18:10:15 -05:00
Flux
d7b7086b21 labwc patch (#1391) 2026-01-16 09:50:01 -05:00
bbedward
59be179821 i18n: more RTL fixes across settings 2026-01-16 09:45:15 -05:00
bbedward
1cf2f6b946 popout: fix cross-monitor handling of widgets
fixes #1364
2026-01-16 09:45:15 -05:00
bbedward
a57a9c2121 doctor: add mango and labwc to compositors
fixes #1394
2026-01-16 09:45:15 -05:00
bbedward
67568c3746 greeter: remove WLR_DRM_DEVICES setting
fixes #1393
2026-01-16 09:45:15 -05:00
bbedward
afce792b80 dankbar: fix property preservation in widgets
fixes #1392
2026-01-16 09:45:15 -05:00
bbedward
f5c7493dbb weather: fix precipitationw weekly propability
fixes #1395
2026-01-16 09:45:15 -05:00
bbedward
f9b9d98638 plugins: ensure daemon plugins not instantiated twice 2026-01-16 09:45:15 -05:00
bbedward
2a97e03fa6 cc: fixed width column, remove anchoring from individual icons on vbar
maybe #1376
2026-01-16 09:45:15 -05:00
Lucas
d6dacc2975 nix: fix home module (#1387) 2026-01-16 08:46:26 +01:00
Bailey
aab4b6765d nix: Support specifying systemd target (#1385) 2026-01-16 02:01:51 -03:00
bbedward
3539aca1f7 cc: wrap icons in fixed size containers
maybe #1376
2026-01-15 23:05:21 -05:00
bbedward
81fbe9eaba controlcenter: fix visibility condition of no icons
fixes #1377
2026-01-15 23:00:43 -05:00
purian23
f9dc6de485 Fix fedora version format 2026-01-15 23:00:08 -05:00
bbedward
012022d370 plugins: fix plugin confirm third part repo window 2026-01-15 22:55:11 -05:00
purian23
993216e157 distro: Update Fedora dynamic versioning 2026-01-15 22:30:20 -05:00
purian23
c992f2b582 feat: Allow more pinned services in Control Center/Settings 2026-01-15 21:51:17 -05:00
purian23
3243adebca core: Update ghostty on dankinstall 2026-01-15 21:26:31 -05:00
Abhinav Chalise
baccef57d4 fix volume osd sliding ui update for vertical layout (#1382) 2026-01-15 21:10:43 -05:00
bbedward
a823095372 widgets: add fallback for steam apps 2026-01-15 21:07:57 -05:00
Lucas
172a743de4 doctor: use dbus for checking on services (#1384)
* doctor: use dbus for checking on services

* doctor: show docs URL for failed checks

* core: remove unused function
2026-01-15 20:59:47 -05:00
Ivan Molodetskikh
623eec3689 Add screencast indicator for niri (#1361)
* niri: Handle new Cast events

* bar: Add screen sharing indicator

Configurable like other icons; on by default.

* lockscreen: Add screen sharing indicator
2026-01-15 00:55:21 -05:00
bbedward
53a033fe35 dankdash: fix weather open IPC
fixes #1367
2026-01-14 22:29:29 -05:00
bbedward
c490ee24f4 matugen: fix nvim ID in skipTemplates 2026-01-14 22:27:07 -05:00
bbedward
cc1e49294e i18n: update terms 2026-01-14 22:22:27 -05:00
purian23
e6fa46ae26 dankdash: Center Media Art & Controls 2026-01-14 18:03:16 -05:00
purian23
35fe774a1b Update OBS Choice selection 2026-01-13 17:51:55 -05:00
purian23
1e6a0f9423 Update OBS DMS Stable workflow 2026-01-13 17:31:01 -05:00
bbedward
cc1877aadb modals: fix wifi passowrd, polkit, and VPN import 2026-01-13 17:21:16 -05:00
bbedward
f1eb1fa9ba settings: fix child windows on newer quickshell-git 2026-01-13 16:57:23 -05:00
Lucas
bdd01e335d settings: fix modal not opening on latest quickshell (#1357) 2026-01-13 16:41:54 -05:00
Lucas
4b7baf82cd nix: escape version string (#1353) 2026-01-13 11:24:51 -05:00
purian23
15c88ce1d2 quickshell: Despace Versioning 2026-01-13 11:07:12 -05:00
bbedward
8891c388d0 bump to v1.4-unstable 2026-01-13 08:40:56 -05:00
bbedward
cd9d92d884 update changelog link and VERSION 2026-01-13 08:31:50 -05:00
Lucas
1b69a5e62b nix: add wtype dependency (#1346) 2026-01-13 08:27:46 -05:00
bbedward
61d311b157 widgets: fix running apps positioning and popup manager 2026-01-13 08:26:29 -05:00
bbedward
6b76b86930 notifications: remove redundant trimStored and add null safety 2026-01-12 23:37:49 -05:00
bbedward
dcfb947c36 desktop widgets: sync position across screens option, clickthrough
option, grouping in settings, repositioning, new IPCs for control
fixes #1300
fixes #1301
2026-01-12 15:31:34 -05:00
bbedward
59893b7f44 notifications: use Theme.primary to represent do not distrub in bar 2026-01-12 11:57:42 -05:00
bbedward
d2c62f5533 matugen: add support for vscode-insiders 2026-01-12 11:46:29 -05:00
bbedward
2bbe9a0c45 core/wlcontext: use infinite poll timeout 2026-01-12 11:26:35 -05:00
bbedward
4e2ce82c0a notifications: swipe to dismiss on history 2026-01-12 11:08:22 -05:00
bbedward
104762186f widgets: respect radius for inactive DankButtonGroup i tems 2026-01-12 10:26:50 -05:00
bbedward
f1233ab1e3 matugen: add post_hook for mango 2026-01-12 10:05:19 -05:00
bbedward
d6b407ec37 settings: fix wallpaper preview cache update on per-mode change 2026-01-12 09:58:58 -05:00
bbedward
022b4b4bb3 enable changelog 2026-01-12 09:46:50 -05:00
bbedward
49b322582d keybinds: fix sh, fix screenshot-window options, empty args
part of #914
2026-01-12 09:35:30 -05:00
bbedward
1280bd047d settings: fix sidebar binding when clicked by emitting signal 2026-01-11 22:43:29 -05:00
bbedward
6f206d7523 dankdash: fix 24H format in weather tab
fixes #1283
2026-01-11 21:45:28 -05:00
bbedward
2e58283859 dgop: use used mem directly from API
- conditionally because it depends on newer dgop
2026-01-11 17:32:36 -05:00
Marcus Ramberg
99a5721fe8 settings: extract tab headings for search (#1333)
* settings: extract tab headings for search

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-11 17:14:45 -05:00
bbedward
5302ebd840 notifications: spacing improvements
fixes #1241
2026-01-11 14:35:34 -05:00
bbedward
fa427ea1ac settings: fix clipping of generic color selector
fixes #1242
2026-01-11 14:04:48 -05:00
bbedward
7027bd1646 systemtray: use Theme radius for menu options
fixes #1331
2026-01-11 14:03:23 -05:00
bbedward
3c38e17472 notifications: add compact mode, expansion in history, expansion in
popup
fixes #1282
2026-01-11 12:11:44 -05:00
shalevc1098
510ea5d2e4 feat: configurable app id substitutions (#1317)
* feat: add configurable app ID substitutions setting

* feat: add live icon updates when substitutions change

* fix: cursor not showing on headerActions in non-collapsible cards

* fix: address PR review feedback

- add tags for search index
- remove hardcoded height from text fields
2026-01-10 21:00:15 -05:00
bbedward
bb2234d328 cc: dont show preference flip if not on ethernet and wifi 2026-01-10 10:35:48 -05:00
bbedward
edbdeb0fb8 widgets: add artix and void NF mappings 2026-01-10 10:18:09 -05:00
Kostiantyn To
19541fc573 update-service: add Artix Linux to supported distributions list (#1318) 2026-01-10 10:18:00 -05:00
bbedward
7c936cacfb niri: fix effectiveScreenAssignment in modal 2026-01-10 10:13:41 -05:00
bbedward
c60cd3a341 modals/auth: add show password option
fixes #1311
2026-01-09 22:20:18 -05:00
shalevc1098
e37135f80d feat: map steam_app_ID to steam_icon_ID for actual game icons (#1312)
Steam Proton games use window class steam_app_XXXXX. Steam installs
icons as steam_icon_XXXXX. This maps between them so actual game
icons display instead of generic controller fallback.
2026-01-09 21:40:35 -05:00
bbedward
aac937cbcc settingns: fix missing help text on desktop widgets 2026-01-09 19:07:37 -05:00
bbedward
4b46d022af workspaces: add color options, add focus follows monitor, remove
per-monitor option (was misleading)
relevant to #1207
2026-01-09 14:10:57 -05:00
bbedward
7f0181b310 matugen/vscode: fix selection contrast 2026-01-09 10:16:03 -05:00
bbedward
6a109274f8 hyprland: always use single window 2026-01-09 09:57:31 -05:00
bbedward
0f09cc693a lock: handle case where session lock is rejected 2026-01-09 09:46:39 -05:00
bbedward
af0166a553 dankbar: add bar get/setPosition IPC 2026-01-09 00:09:49 -05:00
bbedward
a283017f26 audio: recreate media players on pipewire device change 2026-01-08 23:35:42 -05:00
bbedward
5ae2cd1dfb i18n: fix RTL in plugin settings 2026-01-08 19:16:55 -05:00
bbedward
eece811fb0 i18n: more RTL repairs 2026-01-08 18:45:38 -05:00
bbedward
1ff1f3a7f2 i18n: more RTL layout enhancements 2026-01-08 16:11:30 -05:00
bbedward
a21a846bf5 wallpaper: encode image URIs
fixes #1306
2026-01-08 14:32:12 -05:00
Anton Kesy
f5f21e738a fix typos (#1304) 2026-01-08 14:10:24 -05:00
bbedward
033e62418a hyprland: fix cursor setting 2026-01-08 09:30:52 -05:00
bbedward
3c69e8b1cc revert readme 2026-01-07 22:59:28 -05:00
bbedward
118be27796 update readme 2026-01-07 22:56:16 -05:00
bbedward
721d35d417 readme:update vid url 2026-01-07 22:54:38 -05:00
bbedward
7bc3d5910d settings: fade to lock and monitor off by default on 2026-01-07 21:31:12 -05:00
bbedward
ccc7047be0 welcome: make the first page stuff clickable
fixes #1295
2026-01-07 21:22:15 -05:00
bbedward
a5e107c89d changelog: capability to display new release message 2026-01-07 20:15:50 -05:00
bbedward
646d60dcbf displays: fix text-alignment in model mode 2026-01-07 16:54:31 -05:00
bbedward
5dc7c0d797 core: add resolve-include recursive
fixes #1294
2026-01-07 16:45:31 -05:00
bbedward
db1de9df38 keybinds: fix empty string args, more writable provider options 2026-01-07 15:38:44 -05:00
bbedward
3dd21382ba network: support hidden SSIDs 2026-01-07 14:13:03 -05:00
bbedward
ec2b3d0d4b vpn: aggregate all import errors
- we are dumb about importing by just trying to import everythting
- that caused errors to not be represented correctly
- just aggregate them all and present them in toast details
- Better would be to detect the type of file being imported, but this is
  better than nothing
2026-01-07 13:22:56 -05:00
bbedward
a205df1bd6 keybinds: initial support for writable hyprland and mangoWC
fixes #1204
2026-01-07 12:15:38 -05:00
bbedward
e822fa73da cursor: make min/max wider 2026-01-07 10:04:47 -05:00
bbedward
634e75b80c plugins: improve version check 2026-01-07 09:46:55 -05:00
bbedward
ec5b507efc greeter: change hypr startup to exec-once 2026-01-07 09:18:32 -05:00
bbedward
e6d289d48c workflow: update stable workflow to use GH app 2026-01-06 22:45:34 -05:00
bbedward
745d7f26ce cursor: create/update XResources for XWL apps 2026-01-06 22:06:01 -05:00
bbedward
ad43053b94 cursor: hypr, mango, and dankinstall support for configs 2026-01-06 20:35:22 -05:00
purian23
721700190b feat: DMS Cursor Control - Size & Theme in niri 2026-01-06 19:08:05 -05:00
bbedward
8c9c936d0e clipboard: add cliphist-migrate CLI 2026-01-06 16:49:18 -05:00
dms-ci[bot]
842bf6e3ff nix: update vendorHash for go.mod changes 2026-01-06 21:03:38 +00:00
bbedward
c1fbeb3f5e network: listen to NM Wired interface + use nmcli for route metrics
- Some other misc floating window change, too lazy to separate the
  commit
2026-01-06 16:01:28 -05:00
bbedward
c45eb2cccf plugins: ipc visibility conditions 2026-01-06 13:22:36 -05:00
bbedward
1b5abca83a launcher remove right key 2026-01-06 10:37:58 -05:00
bbedward
45818b202f launcher: support for plugins to define context menus
fixes #1279
2026-01-06 10:08:22 -05:00
Ethan Todd
1c8ce46f25 notifications: fix notifications being completely transient if history is disabled (#1284) 2026-01-06 09:39:33 -05:00
bbedward
f762f9ae49 theme: fix gtk apply button on empty file
fixes #1280
2026-01-05 21:56:50 -05:00
bbedward
4484f6bd61 launcher: built-in plugins, add settings search plugin with ? default
trigger
2026-01-05 21:46:12 -05:00
purian23
0076c45496 shell: dmsCoreApp updates 2026-01-05 20:31:06 -05:00
bbedward
ab071e12aa icons: fix transmission-gtk modded app ID again 2026-01-05 16:44:31 -05:00
bbedward
8386b40c50 launcher: F10 as alt for menu key 2026-01-05 14:58:50 -05:00
bbedward
03a985228d dankbar: add shadow option
fixes #916
2026-01-05 13:43:15 -05:00
bbedward
ef7d7ec13d desktop widgets: niri overview only option + grid on overlay when on
overview
2026-01-05 13:01:10 -05:00
bbedward
824792cca7 notifications: add support for none, count, app name, and full detail
for lock screen
fixes #557
2026-01-05 12:22:05 -05:00
bbedward
850e5b6572 session: handle hibernate error
fixes #308
2026-01-05 12:01:17 -05:00
bbedward
64310854a6 compositor+matugen: border override, hypr/mango layout overrides, new
templates, respect XDG paths
- Add Hyprland and MangoWC templates
- Add GUI gaps, window radius, and border thickness overrides for niri,
  Hyprland, and MangoWC
- Add replacement support in matugen templates for DATA_DIR, CACHE_DIR,
  CONFIG_DIR
fixes #1274
fixes #1273
2026-01-05 11:25:13 -05:00
bbedward
4005a55bf2 session: blockLoading true 2026-01-05 09:11:19 -05:00
bbedward
0236fe3276 session: fix persist on empty file 2026-01-05 09:07:00 -05:00
bbedward
c1d95a3086 launcher: fix invalid icon rendering wrong icon 2026-01-04 22:58:20 -05:00
bbedward
9b027df1d5 doctor: add links to dr command 2026-01-04 22:44:19 -05:00
purian23
5e03afe7f0 feat: Implement DMS Core Persistent Apps 2026-01-04 22:33:50 -05:00
bbedward
145a974b6d welcome: add IPC targets and button on about page 2026-01-04 21:45:02 -05:00
bbedward
d23fc9f2df welcome: add a first launch welcome page with doctor integration
fixes #760
2026-01-04 19:07:34 -05:00
bbedward
7ac5191e8d matugen: fix app checking
- double nil for flatpak + bin required to skip
2026-01-04 17:53:47 -05:00
bbedward
29d27ebd6d mautgen: update vscode package 2026-01-04 17:19:51 -05:00
bbedward
e45075dd84 launcher: fix binding loop 2026-01-04 17:19:35 -05:00
bbedward
80bc87e76b clock: fixed width chars in vertical mode 2026-01-04 13:20:20 -05:00
bbedward
76d88517ec matugen: publish vscode theme to marketplace/ovsix 2026-01-04 13:07:23 -05:00
bbedward
151d695212 launcher: optimize bindings and filters 2026-01-04 11:49:24 -05:00
Ethan Todd
2e1bed5fb5 nix: update home-manager module to remove default*, add clsettings (#1233)
* nix: update home-manager module, add clsettings

* nix: resolve message and rename clsettings->clipboardSettings.

* nix: fix home-manager plugin_settings management. add option for whether plugin settings should be managed by nix.
2026-01-04 11:18:28 -03:00
Lucas
f163b97c17 doctor: add json output (#1263)
* doctor: add json output

* doctor: fix systemd failed state as ok
2026-01-03 20:41:10 -05:00
bbedward
436c99927e settings: detect read-only on save attempts 2026-01-03 20:34:36 -05:00
bbedward
aa72eacae7 notifications: add image persistence 2026-01-03 19:56:08 -05:00
bbedward
913bb2ff67 niri: ensure outputs.kdl and binds.kdl exist 2026-01-03 18:31:36 -05:00
Lucas
3bb2696263 Add doctor command (#1259)
* feat: doctor command

* doctor: use console.warn for quickshell feature logs

* doctor: show compositor, quickshell and cli path in verbose

* doctor: show useful env variables

* doctor: indicate if config files are readonly

* doctor: add power-profiles-daemon and i2c

* doctor: cleanup code

* doctor: fix icon theme env variable

* doctor: use builtin config/cache dir functions

* doctor: refactor to use DoctorStatus struct and 'enum'

* doctor: use network backend detector
2026-01-03 18:28:23 -05:00
bbedward
166843ded4 niri: preserve remaining settings when turning off output 2026-01-03 16:17:16 -05:00
Ryan Bateman
02166a4ca5 feat: matugen detects flatpak installations of zenbrowser and vesktop (#1251)
* feat: matugen detects flatpak installations of zenbrowser and vesktop

* fix: add flatpak deps on precommit runner

* fix: address short circuit conditions
2026-01-03 15:28:39 -05:00
bbedward
f0f2e6ef72 i18n: update terms 2026-01-03 15:20:34 -05:00
bbedward
8d8d5de5fd matugen: update vscode template
- yaml/toml highlighting colors
- fix scrollbar contrast
- fix command-search marker
2026-01-03 15:10:38 -05:00
bbedward
6d76f0b476 power: add fade to monitor off option
fixes #558
2026-01-03 15:00:12 -05:00
bbedward
f3f720bb37 settings: fix network refresh button animation behavior
fixes #1258
2026-01-03 14:37:27 -05:00
bbedward
2bf85bc4dd motifications: add support for configurable persistent history
fixes #929
2026-01-03 13:08:48 -05:00
bbedward
faddc46185 core: respect QT_LOGGING_RULES var 2026-01-03 11:05:47 -05:00
bbedward
2991aac82e printers: fix input field height
fixes #1254
2026-01-03 10:54:53 -05:00
bbedward
e1817027b1 settings: add existence check in addition to RO check 2026-01-02 22:36:37 -05:00
bbedward
ba2d51bcbb core: initialize fd pipes in tests and increase queue size in test 2026-01-02 22:30:42 -05:00
Sparsh Mishra
7f10d6a9b8 Add media control bindings for audio playback (#1240)
* Add media control bindings for audio playback

* Update niri-binds.kdl for audio controls

Added play pause prev next controls for niri too
2026-01-02 22:25:21 -05:00
bbedward
405749aa98 theme: unconditionally load dms-colors.json 2026-01-02 22:01:04 -05:00
bbedward
77681fd387 launcher: allow terminal apps 2026-01-02 21:56:56 -05:00
bbedward
8253ec4496 theme: add dank16 to dms matugen template 2026-01-02 21:37:48 -05:00
bbedward
a1e001e640 i18n: update terms 2026-01-02 19:35:02 -05:00
bbedward
3a65ea21ba plugins: fix first plugin install reactivity 2026-01-02 19:22:04 -05:00
NikSne
7d761c4c9a feat(distro/nix/niri): add a hack for config includes with niri flake (#1239)
It works fine but needs all dms-generated config files to be present
2026-01-03 00:43:39 +01:00
Phil Jackson
4cb90c5367 Bar (mediaplayer): Mouse wheel options for media player widget (#1248)
* Add different options for scroll on media widget.

* Nicer lookup code.

* Remove some checks I didn't need.

* Update the search tags.

* EOF.
2026-01-02 17:08:42 -05:00
Ryan Bateman
1c7d15db0b util: add flatpak introspection utilities (#1234)
ci: run apt as sudo

ci: fix flatpak remote in runner

ci: flatpak install steps in runner

ci: specific version of freedesktop

ci: freedesktop install perms
2026-01-02 16:07:32 -05:00
vha
7268a3fe7f feat: Add group workspace apps toggle (#1238)
* Add group workspace apps toggle

* wording

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-02 15:55:51 -05:00
pcortellezzi
d2c4391514 feat: Persistent Plugins & Async Updates (#1231)
- PluginService: maintain persistent instances for Launcher plugins
- AppSearchService: reuse persistent instances for queries
- Added requestLauncherUpdate signal for async UI refreshes
2026-01-02 15:49:04 -05:00
sweenu
69b1d0c2da bar(ws): add option to show name (#1223) 2026-01-02 15:47:33 -05:00
sweenu
ba28767492 bar(clock): respect compact mode on vertical bar (#1222) 2026-01-02 15:46:33 -05:00
bbedward
6cff5f1146 settings: prevent overwrites if parse called with null object 2026-01-02 15:45:31 -05:00
bbedward
3e1c6534bd matugen: add GTKTheme method on type alias 2026-01-01 23:22:13 -05:00
bbedward
c1d57946d9 matugen: fix adw-gtk3 setting in light mode
- and add models.Get/GetOr helpers
2026-01-01 23:13:12 -05:00
bbedward
5e111d89a5 gamma: recreate controls on resume 2026-01-01 22:50:25 -05:00
Phil Jackson
1a98da22b2 Larger option for the media player widget. (#1236) 2026-01-01 22:12:37 -05:00
johngalt
618ccbcb2f zen-userchrome.css - fixing workspaces container color (#1194) 2026-01-01 22:03:59 -05:00
Body
d3a79a055e tweak background and popout colors to be brighter and more similar to adwaita (#1237) 2026-01-01 21:44:50 -05:00
bbedward
bae32e51ff core: skip display filtering in IPC 2026-01-01 15:24:55 -05:00
bbedward
edfda965e9 core: prevent stale path file 2026-01-01 14:04:58 -05:00
bbedward
a547966b23 vpn: wrap secrets in secrets key, cache pkcs11 pin input 2026-01-01 13:43:22 -05:00
bbedward
f6279b1b2e greeter: simplify start-hyprland check 2026-01-01 13:17:01 -05:00
bbedward
957c89a85d settings: refactor for read-only handling
- Remove default-* copying logic
- Allow in-memory changes of settings/session datas
- Convert SessionData to newer spec pattern
- Migrate weather coords to Session data
- Bricks home manager (temporarily)
2026-01-01 13:13:35 -05:00
bbedward
571a9dabcd dock: fix tooltip positioning with adjacent bars 2026-01-01 12:04:49 -05:00
bbedward
51ca9a7686 cachingimage: dont depend on sha256sum 2026-01-01 11:47:26 -05:00
bbedward
c141ad1e34 settings: guard saving before load completed 2026-01-01 11:30:09 -05:00
bbedward
37f972d075 vpn: update pksc11 handling 2025-12-31 15:42:41 -05:00
Oscar R.
7d8de6e6f0 Improving the logic for start-hyprland wrapper use (#1220)
* Adding a way to use the start-hyprland wrapper when it's needed from Hyprland 0.53 it's recommended because offers more security if happens a fail

* Deleting unnecessary things and doing verifications

* fix pre-commit

* Changing to not depend on hyprctl to obtain version and avoid posible problems

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-31 13:27:05 -05:00
bbedward
7ff751f8a2 vpn: attempt to support pkcs11 prompts 2025-12-31 10:03:49 -05:00
bbedward
651672afe2 gamma: allow steps of 100 with slider
fixes #1216
2025-12-31 09:31:16 -05:00
bbedward
2dbadfe1b5 clipboard: single disable + read-only history option 2025-12-31 09:14:35 -05:00
purian23
621710bd86 Update & Replace all issue templates 2025-12-30 23:03:50 -05:00
bbedward
1edecb05bb widgets: dynamic DankToggle height 2025-12-30 22:24:48 -05:00
bbedward
f1a876301b dankbar: fix reveal on overview/niri when auto-hide on 2025-12-30 22:19:25 -05:00
bbedward
97a07c399a greeter: use folderlistmodel for session iteration, add launch timeout 2025-12-30 11:49:00 -05:00
Oscar R.
18f095cb23 feat: implement smart compositor entry point (start-hyprland vs Hyprland) (#1211)
* Adding a way to use the start-hyprland wrapper when it's needed from Hyprland 0.53 it's recommended because offers more security if happens a fail

* Deleting unnecessary things and doing verifications

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-30 11:20:33 -05:00
bbedward
d95d516d64 settings: fix desktop widget accordion row height
fixes #1214
2025-12-30 10:56:20 -05:00
purian23
45ba64ab02 About versioning 2025-12-29 22:47:33 -05:00
bbedward
9501d66af6 matugen: fix skip 2025-12-29 17:46:32 -05:00
bbedward
2127fc339a core: update hypr config test 2025-12-29 14:59:02 -05:00
bbedward
7962fee0bd dankinstall: update hyprland reference config for 0.53
fixes #913
2025-12-29 14:55:12 -05:00
bbedward
d5c7b5c0cc workspace: update scroll accumulator logic 2025-12-29 12:11:37 -05:00
vha
5f77d69dd8 feat: accept numpad's enter key to finish screenshot selection (#1210)
* added reverse scrolling to settings and widget

* added support for dankbar scrolling

* Better settings description

* removed isNiri conditional from search index

* Added numpad enter key to finish screenshot selection
2025-12-29 11:13:57 -05:00
bbedward
60034be06a dankbar: copy high-dpi scrolling logic from DankListView 2025-12-29 10:52:33 -05:00
bbedward
518a5d38aa settings: show parse error message 2025-12-29 10:46:03 -05:00
Eduardo Ribeiro
2eeaf8ff62 feat: allow adjusting notification volume (#1199) 2025-12-29 10:41:12 -05:00
bbedward
cffee0fae6 matugen: make check codition an array 2025-12-29 10:36:24 -05:00
bbedward
f08e2ef5b8 hypr: add disable output option 2025-12-28 23:15:43 -05:00
Joaquim S.
2b0070c31a matugen/template: Soothing neovim theme (#1201) 2025-12-28 21:49:44 -05:00
Marcus Ramberg
ae82716afa core: apply gopls automatic modernizers (#1198) 2025-12-28 21:48:56 -05:00
johngalt
c281bf3b53 Adding Zen Browser matugen template (#1181)
* Adding Zen Browser matugen template

* Fixing indentation for matugen.go edits

* Trying to fix linting again..

* Tweaking contrasting surface colors in css, renamed file to match how firefox userchrome is named, also changed output directory to DMS config directory (like firefox)

* Modifing Zen userChrome again: removing unused css stuff, tweaking colors to better align with how pywalfox handles backgrounds/toolbars

* Last few tweaks on CSS - changing url bar highlight color, changing contrast on selected urls in dropdown

* matugen.go: fix check command for zen browser

* search_index: add zen browser setting
2025-12-28 12:52:41 -05:00
bbedward
45b8b2a895 clipboard: don't store sensitive mime types in history
fixes #1185
2025-12-28 11:13:34 -05:00
Tacticalsmooth
7b9ba840fb fixed lambda issue on nixos (#1188) 2025-12-28 12:50:07 +01:00
xxyangyoulin
84fb567ff5 Notification: Right-click to toggle Do Not Disturb (#1174) 2025-12-28 00:34:52 -05:00
Lucas
bbd57e0673 nix: remove dgop input; expose quickshell package (#1183)
* nix: remove dgop input

* nix: update quickshell

* nix: expose quickshell in outputs
2025-12-27 20:41:26 -05:00
vha
b1632a0a03 Fix #1179 normal scrolling direction (#1182)
* added reverse scrolling to settings and widget

* added support for dankbar scrolling

* Better settings description

* removed isNiri conditional from search index

* fix #1179 breaking normal scroll direction
2025-12-27 18:23:31 -05:00
bbedward
7aff1182c8 desktop widgets: easier copy/delete 2025-12-27 14:13:29 -05:00
vha
fbe362cd20 feat: Added reverse workspace scrolling (#1179)
* added reverse scrolling to settings and widget

* added support for dankbar scrolling

* Better settings description

* removed isNiri conditional from search index
2025-12-27 13:47:50 -05:00
bbedward
e7f94c94cc i18n: fix RTL alignment of settings sidebar 2025-12-27 12:51:10 -05:00
bbedward
7523190b16 i18n: add farsi 2025-12-27 12:46:40 -05:00
bbedward
da37e16b6e window: remove unused signal 2025-12-27 12:32:42 -05:00
xxyangyoulin
5c420ab50b AppSearch: Add ID search fallback for non-English / non-standard apps (#1173) 2025-12-27 12:26:43 -05:00
Joaquim S.
4493b7c231 matugen/template: Improve on generated theme + using dank16 instead (#1177) 2025-12-27 11:14:18 -05:00
bbedward
40a96c6eaf vpn: initialize slices so they dont serialize as null 2025-12-27 10:52:34 -05:00
bbedward
23a93082c6 fix trailing whitespace 2025-12-27 09:43:42 -05:00
Tacticalsmooth
f7650b5e1f Added mirroring option to display configuration (#1156)
* <quickshell/Modules/Settings/DisplayConfig>: Added mirroring option to the displayconfiguration

* removed niri option for mirroring

* Fix trailing whitespace

* removed emty rows

---------

Co-authored-by: Postboote1 <stoessel.matthias>
2025-12-27 09:27:19 -05:00
bbedward
3ebdd5631c dankdash: fix binding breaking when clicking overview card
fixes #1170
2025-12-27 00:58:47 -05:00
Linken Quy Dinh
6c4caf121a add seconds to wallpaper cycling (#1169) 2025-12-27 00:53:48 -05:00
bbedward
89788e9ca7 workspace: chagne pill hover color 2025-12-27 00:53:01 -05:00
bbedward
0787c63fed bar: change widget base hover blend logic 2025-12-27 00:49:11 -05:00
bbedward
9fc0d5efff settings: add index extractor script for search 2025-12-26 22:21:20 -05:00
bbedward
6611dfbe05 settings: fix search height 2025-12-26 20:44:35 -05:00
bbedward
8a71ead51d themes: remove catpuccin, support accent colors 2025-12-26 20:28:55 -05:00
bbedward
d9d6ab5776 settings: add search
- because tabs arent loaded at runtime, we have to have a separate index
- Less ideal, but functional enough for now
2025-12-26 19:19:47 -05:00
bbedward
d6fe7bea27 vpn: remove redundant property definitions 2025-12-26 19:02:16 -05:00
xxyangyoulin
1194f3ffb8 media: add scroll wheel behavior configuration (#1160) 2025-12-26 14:43:47 -05:00
bbedward
5ac81e6dd6 dankbar: dont apply exclusive zone to popup positioning 2025-12-26 14:38:34 -05:00
Lucas
987856a1de nix: update flake inputs (#1161) 2025-12-26 14:15:22 -05:00
bbedward
ef52ce0990 themes: support for variants 2025-12-26 14:00:14 -05:00
bbedward
06b14a5869 dankinstall: fix plasma session collision 2025-12-26 13:06:21 -05:00
bbedward
fd839059c0 popout: use mapToItem instead of mapToGlobal for popout positioning
fixes #1152
2025-12-25 12:50:26 -05:00
bbedward
ec6db7962a i18n: sync terms 2025-12-25 12:09:40 -05:00
Aaron Tulino
adf92cbc46 Add battery charge limit (#1151) 2025-12-25 12:09:09 -05:00
Aaron Tulino
6b6f51cd1f Add volume and brightness percentages (#1148) 2025-12-25 12:07:29 -05:00
Aaron Tulino
df6c60213f Use volume_mute icon for volume==0 (#1150) 2025-12-25 11:31:05 -05:00
Aaron Tulino
6303304a10 Allow toggling mute with right-click on bar (#1147) 2025-12-24 16:12:47 -05:00
Aaron Tulino
8e76789119 Fix touchpad scrolling behavior (#1146)
* Fix touchpad scrolling behavior

* Make touchpad scroll smoothly
For a normal mouse wheel, adjusting by 5% per scroll makes sense. For a touchpad, however, it should adjust by the smallest increment possible for a smooth experience.
2025-12-24 16:12:34 -05:00
Aaron Tulino
10e81cfdd3 Clear lock screen textbox on Escape key press (#1139) 2025-12-24 09:47:55 -05:00
Aaron Tulino
03fd3a4f16 Add Do Not Disturb to IPC (#1140) 2025-12-24 09:47:44 -05:00
bbedward
8fdc748ed2 weather: fix icons 2025-12-24 00:33:54 -05:00
bbedward
6c56d23b93 themes: fix terminals always dark with custom themes 2025-12-23 22:33:33 -05:00
bbedward
45d34dcb5b themes: consistent usage of primaryPressed 2025-12-23 21:45:06 -05:00
bbedward
d7ac0d50fa launcher: use primaryPressed for hover 2025-12-23 21:36:59 -05:00
bbedward
1d4d145187 desktop plugins: enable by default 2025-12-23 14:10:55 -05:00
bbedward
a5b9ff98c0 displays: explicitly write scale 1 for niri
fixes #1116
2025-12-23 11:53:23 -05:00
bbedward
6feaecd92e niri: add gaps and radius override 2025-12-23 11:00:20 -05:00
bbedward
b066a25308 dankdash: use CachingImage in wallpaper tab
fixes #1130
2025-12-23 10:47:50 -05:00
bbedward
777a552b57 spotlight: restore darken background option
fixes #1126
2025-12-23 10:45:11 -05:00
bbedward
7dbe608c28 settings: fix theme application of default-settings json 2025-12-23 10:33:14 -05:00
bbedward
61630e447b desktop-widgets: add overlay IPC and overview option 2025-12-22 20:29:01 -05:00
bbedward
91385e7c83 dankbar: option to show when bar is hidden and no windows 2025-12-22 19:54:53 -05:00
bbedward
04648fcca7 spotlight: remove darken bg opt, improve performance 2025-12-22 16:07:40 -05:00
bbedward
080fc7e44e i18n: term update 2025-12-22 14:37:47 -05:00
bbedward
0b60da3d6d dock: add isolate runninig apps by display option 2025-12-22 14:35:04 -05:00
claymorwan
a4492b90e7 matugen: fix equibop theme not working (#1122)
* matugen: equibop theme

* style: withespace apparently

* matugen: fix equibop theme not working
2025-12-22 14:19:57 -05:00
bbedward
c9331b7338 dropdown: improve perf + add fuzzy search to printers 2025-12-22 14:18:24 -05:00
bbedward
4982ea53dd window: add support for startSystemMove, resize, maximize to floating
windows
2025-12-22 13:18:37 -05:00
claymorwan
c703cb6504 matugen: equibop theme (#1119)
* matugen: equibop theme

* style: withespace apparently
2025-12-22 12:03:20 -05:00
bbedward
a7494971fd desktop widgets: centralize config in desktop widgets tab, variants
always available
2025-12-22 10:39:19 -05:00
purian23
c548255bfc ubuntu: DMS-Greeter 2025-12-21 23:56:17 -05:00
purian23
9656c7afd7 ubuntu: Update hardcoded arcs 2025-12-21 23:37:08 -05:00
purian23
414b8c8272 Ubuntu: DMS - add ARM64 support 2025-12-21 22:31:59 -05:00
bbedward
b4f83d09d4 themes: incorporate theme registry, browser, dms URI scheme handling 2025-12-21 22:03:48 -05:00
purian23
67ee74ac20 core: Fix Debian Architecture logic 2025-12-21 21:59:20 -05:00
purian23
93539d2b6b core: Debian Sid/OpenSuse Leap, Slowroll support 2025-12-21 21:36:33 -05:00
bbedward
524d967745 matugen: remove bad kitty tab option
fixes #1109
2025-12-21 17:42:20 -05:00
bbedward
0effbebbb6 matugen: fix GTK4 light mode
fixes #1110
fixes #1056
2025-12-21 12:52:01 -05:00
bbedward
dca07a70f8 desktop widgets: put grid on bottom layer 2025-12-20 09:58:01 -05:00
bbedward
02936c97fd desktop widget: handle key events in widget 2025-12-20 09:34:52 -05:00
Ethan Todd
8f7e732827 notifications: add modal IPC command for dismissing all popups. rename clearAllPopups() to dismissAllPopups(), since clear is otherwise used to mean eliminated entirely rather than just sent to the notification center. (#1100) 2025-12-20 08:13:26 -05:00
johngalt
5ffe563b7d adding gruvbox material custom theme varieties (#1098) 2025-12-19 22:38:22 -05:00
Joaquim S.
6ef08c3d54 matugen/template: Added neovim to matugen pipeline (#1097) 2025-12-19 14:16:45 -05:00
bbedward
908b4b58cd desktop widgets: add grid/grid size hints 2025-12-19 14:05:04 -05:00
purian23
f2611e0de0 fedora: Remove cliphist on dms-git 2025-12-18 23:55:09 -05:00
purian23
ea75a9d351 distro: Convert DMS Greeter to Stable on Fedora Copr 2025-12-18 23:00:13 -05:00
bbedward
3a744d7d68 core: new line on version 2025-12-18 22:12:50 -05:00
purian23
195d312ae2 distro: Decople Fedora DMS Stable spec 2025-12-18 20:03:23 -05:00
Omar Abragh
76006a7377 matugen: Set cursor color for theme (#1088) 2025-12-18 17:23:42 -05:00
bbedward
11536da512 fix missing import 2025-12-18 17:22:01 -05:00
bbedward
2a91bc41f7 i18n: general term cleanup, add missing terms, interpolate some 2025-12-18 16:19:27 -05:00
bbedward
baf23157fc i18n: sync translations 2025-12-18 14:51:57 -05:00
bbedward
83b81be825 keybinds: add log if ShortcutInhibitor is missing 2025-12-18 14:15:54 -05:00
bbedward
4aefa0f1f7 core: skip replacing niri/dms configs
fixes #1072
2025-12-18 11:58:45 -05:00
bbedward
e53a7cee97 matugen: wrap pywalfox in sh 2025-12-18 11:51:37 -05:00
bbedward
8437e1aa7b desktop widgets: use preview window instead of margin shift for non-niri 2025-12-18 10:05:38 -05:00
bbedward
632f40cc0a desktop plugins: use mapToGlobal on moving widgets 2025-12-18 08:57:58 -05:00
Ethan Todd
7d81445341 notifications: add modal function for clearing all (#1082) 2025-12-18 08:28:58 -05:00
bbedward
78a5f401d7 core: remove ascii art from version 2025-12-18 00:14:02 -05:00
bbedward
8745f98c95 matugen: fix vscode editor color reload 2025-12-17 23:40:09 -05:00
bbedward
f0f5bcc630 matugen: add color reload capability to vscode theme 2025-12-17 23:30:06 -05:00
purian23
8a3c513605 distro: Relocate Ubuntu dgop/dsearch to danklinux 2025-12-17 23:08:37 -05:00
bbedward
145d2636dd clock: make desktop clock not use precision seconds always 2025-12-17 21:00:04 -05:00
bbedward
f2b9dc8988 displays: add adaptiveSyncSupported to wlroutput API 2025-12-17 20:36:54 -05:00
bbedward
2e4d56728b niri: track open modals in modal manager for focus transfers 2025-12-17 20:21:34 -05:00
bbedward
18231ed324 niri: don't rely on text field length for launching 2025-12-17 16:40:54 -05:00
bbedward
d0b61d8ed1 niri: release focus for popouts on overview 2025-12-17 16:17:44 -05:00
bbedward
d385a44949 notifications: attempt to minimize rapid window creation/destruction 2025-12-17 16:10:25 -05:00
bbedward
d97392d46e clipboard: remove ownership option 2025-12-17 15:34:40 -05:00
bbedward
6abb2c73fd desktop: fix widget display toggle 2025-12-17 14:24:13 -05:00
bbedward
7e141c6b36 dankbar/vpn: right click to quick connect 2025-12-17 14:16:11 -05:00
bbedward
53553c1f62 clock: add analog seconds option for desktop widget 2025-12-17 14:04:14 -05:00
bbedward
523ccc6bf8 i18n: WIP initial RTL support
- notifications
- color picker
- process list
- settings
- control center, dash
- launcher

part of #1059
2025-12-17 13:50:06 -05:00
bbedward
811e89fcfa matugen: change pywalfox post hook 2025-12-17 12:43:05 -05:00
bbedward
5d5be4d9d7 lock: different pam fallback 2025-12-17 12:40:28 -05:00
bbedward
88457ab139 lock: add pam login fallback locally 2025-12-17 12:31:45 -05:00
bbedward
0034926df7 plugins/desktop-widgets: create a new "desktop" widget plugin type
- Draggable per-monitor background layer widgets
- Add basic dms version checks on plugins
- Clock: built-in clock desktop plugin
- dgop: built-in system monitor desktop plugin
2025-12-17 12:08:03 -05:00
musjj
d082d41ab9 nix: refactor module structure and flake output (#1014)
- The program module is now called dank-material-shell
- The homeModules structure is flattened
2025-12-17 08:12:30 -03:00
purian23
b7911475b6 distros: Prefer stable quickshell 2025-12-16 23:52:37 -05:00
bbedward
672754b0b5 dankdash: fix weather tooltips
fixes #1065
2025-12-16 15:27:44 -05:00
bbedward
0d1553123b binds: accidentally deleted import 2025-12-16 15:16:44 -05:00
bbedward
ba6c51c102 core: exit non-zero when SIGUSR1 is received (for systemd r estart) 2025-12-16 14:47:46 -05:00
bbedward
d64206a9ff core: detect quickshell crash on SIGTERM 2025-12-16 14:44:22 -05:00
bbedward
d9a1089039 displays: add hyprland HDR options 2025-12-16 14:12:51 -05:00
bbedward
55fe463405 displays: break monolith config down and allow floats/fix integer
writing (niri)
2025-12-16 13:36:00 -05:00
bbedward
e84210e962 displays: fix niri hot corner config 2025-12-16 12:54:26 -05:00
bbedward
ff506548d3 displays: add niri-specific layout options to configurator 2025-12-16 12:23:34 -05:00
arfan
f6b09751e9 fix: update getWorkspaceIndex function to include index parameter also fix workspace padding number (#1062) 2025-12-16 11:32:21 -05:00
bbedward
3d863979c4 core: preserve quickshell exit code 2025-12-16 09:01:13 -05:00
purian23
2947ff4131 distro: Revise server side file handling 2025-12-16 01:08:12 -05:00
purian23
b8fca10896 Remove auto run on tags 2025-12-16 00:17:13 -05:00
purian23
33e45794d2 No run on push 2025-12-15 23:29:36 -05:00
purian23
42cc88ca65 Workflow update 2025-12-15 23:24:16 -05:00
purian23
0b7f2416ca distro: Bring up Stable 2025-12-15 23:10:24 -05:00
purian23
5d5c745ee5 Push the logs 2025-12-15 22:18:35 -05:00
purian23
e0429e4c60 distro: Re-add suffix 2025-12-15 21:31:13 -05:00
bbedward
0bece5287e dock: improve pinned app re-ordering feedback, fix vertical dock
ordering
fixes #1046
fixes #938
2025-12-15 20:46:36 -05:00
purian23
60b5e47836 update gitignore env 2025-12-15 19:06:43 -05:00
purian23
aa75b44790 distro: OBS version matching 2025-12-15 18:03:58 -05:00
bbedward
769f58caa9 displays: fix reverted state for position 2025-12-15 17:43:52 -05:00
bbedward
e7facf740d update CHANGELOG 2025-12-15 17:18:59 -05:00
Austin Farmer
04921eef62 Move Ghostty Application Theming (#1047)
* Moved ghostty config

First test. Seems to work but probably broke something.

* Updated test
2025-12-15 17:16:46 -05:00
Oliver Portee
8863c42879 fix light mode/dark mode switch for stock themes (#1057) 2025-12-15 17:16:23 -05:00
bbedward
2745116ac5 displays: add configurator for niri, Hyprland, and MangoWC
- Configure position, VRR, orientation, resolution, refresh rate
- Split Display section into Configuration, Gamma, and Widgets
- MangoWC omits VRR because it doesnt have per-display VRR
- HDR configuration not present for Hyprland
2025-12-15 16:36:14 -05:00
bbedward
bafe1c5fee niri: handle window urgency event
fixes #1033
2025-12-15 12:16:43 -05:00
bbedward
306d7b2ce0 gamma: guard against application
- QML will sync its desired state with GO, when IE settings are changed
  or opened. Go was applying gamma even if unchanged
- Track last applied gamma to avoid sends
2025-12-15 11:43:16 -05:00
bbedward
e9f6583c60 workspaces: add scroll handler to widget itself 2025-12-15 11:12:27 -05:00
redybcs
42a2835929 Update flake.nix to fix Hash Mismatch (#1035)
Looks like there hasn't been any go.mod updates since the workflow to fix the hash was repaired.
2025-12-15 14:14:29 +01:00
purian23
c2c90c680e distro: OBS edgecase 2025-12-15 01:28:00 -05:00
purian23
cd01f6378c Revise OBS / PPA Workflows 2025-12-15 00:37:42 -05:00
purian23
6033075de6 distro: Revise builds to use API variants 2025-12-15 00:32:40 -05:00
tsukasa
79794d3441 dankmodal: removed backgroundWindow to fix clicking twice (#1030)
* dankmodal: removed backgroundWindow

removed 'backgroundWindow' but combined it with 'contentWindow'

* made single window behavior specific to hyprland

this should keep other compositor behavior the same and fix double
clicking to exit out of Spotlight/ClipboardHist/Powermenu
2025-12-14 19:52:06 -05:00
bbedward
031f86b417 Revert "Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)"
This reverts commit ca5fe6f7db.
2025-12-14 19:09:04 -05:00
bbedward
891f53cf6f battery: fix button group sclaing 2025-12-14 17:22:54 -05:00
bbedward
848991cf5b idle: implement screensaver interface
- Mainly used to create the idle inhibitor when an app requests
  screensaver inhibit
2025-12-14 16:49:59 -05:00
bbedward
d37ddd1d41 vpn: optim cc and dankbar widget 2025-12-14 16:12:46 -05:00
Pi Home Server
00d12acd5e Add hide option for updater widget (#1028) 2025-12-14 15:55:47 -05:00
bbedward
3bbc78a44f dankbar: make control center widget per-instance not global
fixes #1017
2025-12-14 15:52:46 -05:00
bbedward
b0a6652cc6 ci: simplify changelog handling 2025-12-14 14:23:27 -05:00
bbedward
cb710b2e5f notifications: fix redundant height animation 2025-12-14 13:40:21 -05:00
tsukasa
ca5fe6f7db Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)
There's possibly more but this fix the need of having to click the
background twice to close those modals.
2025-12-14 11:16:25 -05:00
bbedward
fb75f4c68b lock/greeter: fix font alignment
fixes #1018
2025-12-14 11:13:48 -05:00
bbedward
5e2a418485 binds: fix to scale with arbitrary font sizes 2025-12-14 10:56:03 -05:00
bbedward
24fe215067 ci: pull changelogs from obs/launchpad APIs
- Get changelog from OBS/Launchpad API endpoints, instead of storing in
  git
2025-12-14 10:42:00 -05:00
bbedward
ab2e8875ac runningapps: round icon margin to integer 2025-12-14 10:25:36 -05:00
dms-ci[bot]
dec5740c74 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 15:22:12 +00:00
bbedward
208266dfa3 dwl: fix layout popout 2025-12-14 10:17:58 -05:00
dms-ci[bot]
32f218d58c ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 04:07:07 +00:00
dms-ci[bot]
6fdaab2ccd ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 03:58:50 +00:00
purian23
d336866f44 distro: Let the workflow run 2025-12-13 22:54:58 -05:00
purian23
b40df5f1c4 distro: Unify options across repos 2025-12-13 22:38:25 -05:00
dms-ci[bot]
3c9886ad1b ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 01:55:34 +00:00
bbedward
ea205ebd12 wallpaper: pause cycling when locked, clean state when changing modes 2025-12-13 20:29:02 -05:00
bbedward
30dad46c94 dankbar: add scroll wheel behavior configuration 2025-12-13 20:12:21 -05:00
dms-ci[bot]
fbf79e62e9 ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 21:23:58 +00:00
dms-ci[bot]
efcf72bc08 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 21:19:59 +00:00
bbedward
3b511e2f55 i18n: add hungarian 2025-12-13 14:03:49 -05:00
dms-ci[bot]
e4e20fb43a ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 15:26:22 +00:00
dms-ci[bot]
48ccff67a6 ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 15:25:01 +00:00
Souyama
a783d6507b Change DPMS off to DPMS toggle in hyprland.conf (#1011) 2025-12-13 10:07:11 -05:00
bbedward
fd94e60797 cava: dont set method/source 2025-12-13 10:04:20 -05:00
bbedward
a1bcb7ea30 vpn: just try and import all types on errors 2025-12-13 10:02:57 -05:00
bbedward
31b67164c7 clipboard: re-add ownership option 2025-12-13 09:45:04 -05:00
bbedward
786c13f892 clipboard: fix mime type selection 2025-12-13 09:35:55 -05:00
bbedward
c652659d54 wallpaper: scale texture to physical pixels
- reverts a regression
2025-12-13 08:43:46 -05:00
dms-ci[bot]
ca39196f13 ci: Auto-update OBS packages [dms,dms-git]
🤖 Automated by GitHub Actions
2025-12-13 07:00:01 +00:00
dms-ci[bot]
f02dd8fd4b ci: Auto-update PPA packages [dms,dms-git,dms-greeter]
🤖 Automated by GitHub Actions
2025-12-13 06:51:47 +00:00
purian23
0f89886ce7 distro: Break the loop 2025-12-13 01:44:20 -05:00
dms-ci[bot]
1118404192 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:34:59 +00:00
dms-ci[bot]
f011ea6cce ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:30:45 +00:00
dms-ci[bot]
b2ac9c6c1a ci: Auto-update OBS packages [dms,dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:06:44 +00:00
dms-ci[bot]
fbab41abd6 ci: Auto-update PPA packages [dms,dms-git,dms-greeter]
🤖 Automated by GitHub Actions
2025-12-13 05:58:14 +00:00
bbedward
82f881af5b matugen: scrub the never implemented dynamic contrast palette 2025-12-13 00:51:51 -05:00
purian23
68de9b437d distro: Switch to dms-ci 2025-12-13 00:50:42 -05:00
bbedward
830a715b6d wlcontext: use poll with wake pipe instead of read deadlines 2025-12-13 00:46:30 -05:00
bbedward
ce4aca9a72 fix shellcheck 2025-12-13 00:29:20 -05:00
bbedward
7641171a01 clipboard: move cl receive to main wlcontext goroutine 2025-12-13 00:16:56 -05:00
purian23
119e084e52 distro: Remove PR tests 2025-12-13 00:15:37 -05:00
bbedward
7c6d52913e niri: fix test 2025-12-12 23:57:50 -05:00
bbedward
f63ab5cf7c ci: add workflow for pushing stable tag 2025-12-12 23:57:50 -05:00
purian23
50f1bc5017 distros: Remove false path dir 2025-12-12 23:52:31 -05:00
bbedward
c3ab409b6a clipboard: scrap persist, optimize mime-type handling 2025-12-12 23:48:07 -05:00
purian23
44f6ab4878 distro: Reformat workflow newlines 2025-12-12 23:35:37 -05:00
bbedward
5fda6e0f12 clipboard: allow configuration even when disabled 2025-12-12 23:17:55 -05:00
purian23
38068e78c9 distros: PR writeback 2025-12-12 23:02:52 -05:00
purian23
66d22727e9 distros: Enhance build automation 2025-12-12 22:41:51 -05:00
Lucas
db2f68e35d nix: fix qt-plugins path (#1005) 2025-12-13 01:34:25 +01:00
Marcus Ramberg
352277ec15 notifications: add ipc call for toggleDoNotDisturb (#1002) 2025-12-12 18:21:00 -05:00
bbedward
d6043e64f2 osd: increase shadow buffer
accounts for percentage view
2025-12-12 18:11:31 -05:00
bbedward
d3f5b8f32e niri: fix gap reactivity 2025-12-12 16:58:07 -05:00
bbedward
6c3c722674 niri: add warnings on auto-generated files 2025-12-12 16:53:52 -05:00
purian23
5b8edb13d8 distro: OBS updates 2025-12-12 15:42:21 -05:00
bbedward
c595727b94 osd: optimize surface damage
fixes #994
2025-12-12 15:37:39 -05:00
bbedward
d46302588a clipboard: add shift+enter to paste from clipboard history modal
fixes #358
2025-12-12 15:29:10 -05:00
bbedward
0ff9fdb365 notifications: add swipe to dismiss functionality
fixes #927
2025-12-12 14:39:51 -05:00
purian23
e95f7ce367 Update Copr specs 2025-12-12 12:30:18 -05:00
Pi Home Server
df1a8f4066 Add lock screen layout settings (#981)
* Add lock screen layout settings

* Update translation keys
2025-12-12 11:45:00 -05:00
bbedward
32e6c1660f wallpaper: clamp max texture size 2025-12-12 11:17:28 -05:00
bbedward
d6b9b72e9b ci: disable pkg builds from main release wf 2025-12-12 10:16:24 -05:00
bbedward
179ad03fa4 ci: switch to dispatch-based release flow 2025-12-12 10:01:44 -05:00
bbedward
c3cb82c84e dankinstall: call add-wants for niri/hyprland with dms service 2025-12-12 09:58:12 -05:00
bbedward
4b52e2ed9e niri: fix keybind handling of cooldown-ms parameter 2025-12-12 09:52:35 -05:00
bbedward
77fd61f81e workspaces: make icons scale with bar size, fixi valign of numbers
fixes #990
2025-12-12 00:23:40 -05:00
Lucas
c3ffb7f83b nix: remove wl-clipboard and cliphist dependencies (#991) 2025-12-11 21:44:36 -05:00
Lucas
89dcd72d70 nix: let paths be used instead of only packages in plugins (#988) 2025-12-11 23:57:22 +01:00
bbedward
5c3346aa9d core: fix test 2025-12-11 16:33:31 -05:00
bbedward
7c4b383477 clipboard: persistence off by default
- It's a little risky and messy of a default
2025-12-11 16:28:56 -05:00
bbedward
bdc0e8e0fc clipboard: dont take ownership on nil offers 2025-12-11 15:55:42 -05:00
bbedward
6d66f93565 core: mock wayland context for tests & add i18n guidance to CONTRIBUTING 2025-12-11 14:50:02 -05:00
Lucas
9cac93b724 nix: fix pre-commit hook in dev-shell (#987) 2025-12-11 14:40:19 -05:00
bbedward
0709f263af core: add test coverage for some of the wayland stack
- mostly targeting any race issue detection
2025-12-11 13:47:18 -05:00
Lucas
4e4effd8b1 nix: fix home-manager module plugins (#984) 2025-12-11 19:36:32 +01:00
bbedward
f9632cba61 core: remove hyprpicker remnant 2025-12-11 13:05:07 -05:00
bbedward
38db6a41d5 gamma: fix initial night mode enablement 2025-12-11 12:27:58 -05:00
bbedward
7c6f0432c8 clipboard: add copyEntry (by id) handler 2025-12-11 12:00:47 -05:00
bbedward
56ff9368be matugen: add option to disable DMS templates
fixes #983
2025-12-11 11:48:59 -05:00
bbedward
597e21d44d clipboard: remove wl-copy references 2025-12-11 11:10:27 -05:00
bbedward
5bf54632be media: add option to disable visualizer in bar widget
fixes #978
2025-12-11 10:55:32 -05:00
bbedward
3a8d3ee515 core: use stdlib for xdg dirs 2025-12-11 10:15:23 -05:00
bbedward
1c1cf866e2 settings: make default height screen-aware 2025-12-11 09:51:44 -05:00
bbedward
ccc1df75f1 nix: update vendorHash 2025-12-11 09:50:50 -05:00
bbedward
d2c3f87656 ci: fix nix vendor-hash workflow 2025-12-11 09:46:57 -05:00
bbedward
6d62229b5f clipboard: introduce native clipboard, clip-persist, clip-storage functionality 2025-12-11 09:41:07 -05:00
Marcus Ramberg
7c88865d67 Refactor pre-commit hooks to use prek (#976)
* ci: change to prek for pre-commit

* refactor: fix shellcheck warnings for the scripts

* chore: unify whitespace formatting

* nix: add prek to dev shell
2025-12-11 09:11:12 -05:00
bbedward
c8cfe0cb5a dwl: fix layout popout not opening
fixes #980
2025-12-11 09:05:53 -05:00
Lucas
e573bdba92 nix: add QML dependencies to dms-shell package (#967) 2025-12-11 09:19:43 +01:00
Lucas
d8cd15d361 nix: add plugins in NixOS module (#970)
* nix: remove unnecessary /etc/xdg/quickshell/dms and .config/quickshell/dms

* nix: add plugins in NixOS module
2025-12-11 09:03:22 +01:00
Lucas
1db3907838 nix: fix greeter per-monitor and per-mode wallpapers (#974) 2025-12-11 09:01:14 +01:00
Lucas
72cfd37ab7 nix: fix niri module (#969) 2025-12-10 23:21:52 -05:00
bbedward
1e67ee995e plugins: hide uninstall and update buttons for system plugins 2025-12-10 19:30:58 -05:00
bbedward
6c26b4080c core: fix socket reported CLI version 2025-12-10 16:48:44 -05:00
purian23
0dbd59b223 Manual Changelog versioning 2025-12-10 13:52:29 -05:00
Lucas
b2066c60d1 nix: drop unnecessary dependencies and enable power and accounts daemons (#963) 2025-12-10 19:35:58 +01:00
bbedward
8d7ae324ff Revert "distro: update ppa-build script to ref right version"
This reverts commit c0d3c4f875.
2025-12-10 13:31:15 -05:00
bbedward
c0d3c4f875 distro: update ppa-build script to ref right version 2025-12-10 13:28:34 -05:00
purian23
27a771648a Ubuntu workflow tweak 2025-12-10 12:37:56 -05:00
purian23
86affc7304 Add WorkDIR to build steps 2025-12-10 12:33:41 -05:00
purian23
d939b99628 Workflow build increment logic 2025-12-10 12:27:10 -05:00
purian23
1fcf777f3d Bump OBS spec 2025-12-10 12:13:43 -05:00
purian23
7a8e23faa9 Update build scripts 2025-12-10 12:06:28 -05:00
bbedward
73a4dd3321 change codename 2025-12-10 11:18:08 -05:00
purian23
13ce873a69 Update dms stable systemd & desktop path 2025-12-10 11:16:03 -05:00
886 changed files with 196883 additions and 34979 deletions

View File

@@ -0,0 +1,57 @@
{
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(git -C /home/purian23/dms diff --stat .github/workflows/)",
"Bash(git -C /home/purian23/projects/danklinux diff --stat .github/workflows/)",
"Bash(git -C /home/purian23/dms diff .github/workflows/)",
"Bash(git -C /home/purian23/dms diff .github/workflows/run-ppa.yml)",
"Bash(osc cat:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(git show-ref:*)",
"Bash(git tag:*)",
"Bash(bash -c 'ALL_PATHS=$(grep -A 5 \"\"<service name=\\\"\"download_url\\\"\">\"\" distro/debian/dms/_service | grep \"\"<param name=\\\"\"path\\\"\">\"\" | sed \"\"s/.*<param name=\\\"\"path\\\"\">\\(.*\\)<\\/param>.*/\\1/\"\"); SOURCE_PATH=\"\"\"\"; for path in $ALL_PATHS; do if echo \"\"$path\"\" | grep -qE \"\"(source|archive|\\.tar\\.(gz|xz|bz2))\"\" && ! echo \"\"$path\"\" | grep -qE \"\"(distropkg|binary)\"\"; then SOURCE_PATH=\"\"$path\"\"; break; fi; done; echo \"\"Selected path: $SOURCE_PATH\"\"')",
"Bash(curl:*)",
"Bash(tar:*)",
"Bash(git -C /home/purian23/dms log:*)",
"Bash(osc status:*)",
"Bash(osc commit:*)",
"Bash(osc up:*)",
"Bash(osc results:*)",
"Bash(osc api:*)",
"Bash(systemctl:*)",
"Bash(dms version:*)",
"Bash(git describe:*)",
"Bash(qmlsc:*)",
"Bash(qmllint-qt6:*)",
"Bash(make fmt:*)",
"Bash(make test:*)",
"Bash(dms chroma list-styles:*)",
"Bash(python3:*)",
"Bash(time dms chroma:*)",
"Bash(dms chroma:*)",
"Bash(make build:*)",
"Bash(pgrep:*)",
"Bash(go build:*)",
"Bash(/tmp/dms-test chroma:*)",
"Bash(1)",
"Bash(go install:*)",
"Bash(grep:*)",
"Bash(journalctl:*)",
"Bash(qdbus:*)",
"Bash(TZ='Asia/Tokyo' date:*)",
"Bash(dms --help:*)",
"Bash(dms run:*)",
"Bash(dms status:*)",
"Bash(dms kill:*)",
"Bash(tee:*)",
"Bash(qmlscene:*)",
"Bash(quickshell --version:*)",
"WebFetch(domain:forum.qt.io)",
"Bash(gh api:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)"
]
}
}

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*.sh]
# like -i=4
indent_style = space
indent_size = 4
[*.nix]
# like -i=4
indent_style = space
indent_size = 4

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
cd "$REPO_ROOT"
# =============================================================================
# Go CI checks (when core/ files are staged)
# =============================================================================
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
if [[ -n "$STAGED_CORE_FILES" ]]; then
echo "Go files staged in core/, running CI checks..."
cd "$REPO_ROOT/core"
# Format check
echo " Checking gofmt..."
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
if [[ -n "$UNFORMATTED" ]]; then
echo "The following files are not formatted:"
echo "$UNFORMATTED"
echo ""
echo "Run: cd core && gofmt -s -w ."
exit 1
fi
# golangci-lint
if command -v golangci-lint &>/dev/null; then
echo " Running golangci-lint..."
golangci-lint run ./...
else
echo " Warning: golangci-lint not installed, skipping lint"
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
# Tests
echo " Running tests..."
if ! go test ./... >/dev/null 2>&1; then
echo "Tests failed! Run 'go test ./...' for details."
exit 1
fi
# Build checks
echo " Building..."
mkdir -p bin
go build -buildvcs=false -o bin/dms ./cmd/dms
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
echo "All Go CI checks passed!"
cd "$REPO_ROOT"
fi
# =============================================================================
# i18n sync check (DISABLED for now)
# =============================================================================
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
# if command -v python3 &>/dev/null; then
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
# echo "Translations out of sync"
# echo "Run: python3 scripts/i18nsync.py sync"
# exit 1
# fi
# fi
# fi
exit 0

View File

@@ -1,65 +0,0 @@
---
name: Bug Report
about: Crashes or unexpected behaviors
title: ""
labels: "bug"
assignees: ""
---
<!-- If your issue is related to ICONS
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] Other (specify)
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the issue -->
## Expected Behavior
<!-- Describe what you expected to happen -->
## Steps to Reproduce
<!-- Please provide detailed steps to reproduce the issue -->
1.
2.
3.
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
<!-- you can get a log file with the following steps:
dms kill
mkdir ~/dms_logs
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
-->
```
Paste error messages or logs here
```
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->

129
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,129 @@
name: Bug Report
description: Crashes or unexpected behaviors
labels:
- bug
body:
- type: markdown
attributes:
value: |
## DankMaterialShell Bug Report
Limit your report to one issue per submission unless similarly related
- type: dropdown
id: compositor
attributes:
label: Compositor
options:
- Niri
- Hyprland
- MangoWC (dwl)
- Sway
validations:
required: true
- type: dropdown
id: distribution
attributes:
label: Distribution
options:
- Arch Linux
- CachyOS
- Fedora
- NixOS
- Debian
- Ubuntu
- Gentoo
- OpenSUSE
- Other (specify below)
validations:
required: true
- type: input
id: distribution_other
attributes:
label: If Other, please specify
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: dropdown
id: installation_method
attributes:
label: Select your Installation Method
options:
- DankInstaller
- Distro Packaging
- Source
validations:
required: true
- type: dropdown
id: original_installation_method
attributes:
label: Was this your original Installation method?
options:
- "Yes"
- No (specify below)
default: 0
validations:
required: false
- type: input
id: original_installation_method_specify
attributes:
label: If no, specify
placeholder: e.g., Distro Packaging, then Source
validations:
required: false
- type: textarea
id: dms_doctor
attributes:
label: dms doctor -vC
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
placeholder: Paste the output of `dms doctor -vC` here
value: |
<details>
<summary>Click to expand</summary>
</details>
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Brief description of the issue
placeholder: What happened?
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: Describe the expected behavior
validations:
required: false
- type: textarea
id: steps_to_reproduce
attributes:
label: Steps to Reproduce
description: Please provide detailed steps to reproduce the issue
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error Messages/Logs
description: Please include any error messages, stack traces, or relevant logs
placeholder: |
Paste error messages or logs here
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots/Recordings
description: If applicable, add screenshots or screen recordings
placeholder: Attach images or videos here
validations:
required: false

View File

@@ -1,33 +0,0 @@
---
name: Request a Feature
about: New widgets, new widget behavior, etc.
title: ""
labels: "enhancement"
assignees: ""
---
## Feature Description
<!-- Brief description of the feature requested -->
## Use Case
<!-- Explain the purpose of this feature/why it'd be useful to you -->
## Compositor
Is this feature specific to one compositor?
- [ ] All compositors
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
## Proposed Solution
<!-- If you have any ideas for how to implement this, please share! -->
## Alternatives/Existing Solutions
<!-- Include any similar/pre-existing products that solve this problem -->

View File

@@ -0,0 +1,62 @@
name: Feature Request
description: Suggest a new feature or improvement for DMS. Keep features focused on a single topic with clear benefits, examples, etc. Avoid vague or broad requests, they will be closed.
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
## DankMaterialShell Feature Request
- type: textarea
id: feature_description
attributes:
label: Feature Description
description: Brief description of the feature requested
placeholder: What feature would you like to see?
validations:
required: true
- type: textarea
id: use_case
attributes:
label: Use Case
description: Explain the purpose of this feature/why it'd be useful to you
placeholder: Why is this feature important?
validations:
required: false
- type: dropdown
id: compositor
attributes:
label: Compositor(s)
description: Is this feature specific to one or more compositors?
options:
- All compositors
- Niri
- Hyprland
- MangoWC (dwl)
- Sway
- Other (specify below)
validations:
required: true
- type: input
id: compositor_other
attributes:
label: If Other, please specify
placeholder: e.g., Wayfire, Mutter, etc.
validations:
required: false
- type: textarea
id: proposed_solution
attributes:
label: Proposed Solution
description: If you have any ideas for how to implement this, please share!
placeholder: Suggest a solution or approach
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives/Existing Solutions
description: Include any similar/pre-existing products that solve this problem
placeholder: List alternatives or existing solutions
validations:
required: false

View File

@@ -1,40 +0,0 @@
---
name: Request Assistance or Support
about: Help with installation, usage, or general questions.
title: ""
labels: "support"
assignees: ""
---
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] other
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the support needed -->
## Solutions Tried
<!-- Describe what you've tried so far -->
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
## Configuration Details
<!-- Include any configuration if relevant -->
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->

View File

@@ -0,0 +1,124 @@
name: Support Request
description: Help with installation, usage, or general questions about DankMaterialShell
labels:
- support
body:
- type: markdown
attributes:
value: |
## DankMaterialShell Support Request
- type: dropdown
id: compositor
attributes:
label: Compositor
options:
- Niri
- Hyprland
- MangoWC (dwl)
- Sway
- Other (specify below)
validations:
required: true
- type: input
id: compositor_other
attributes:
label: If Other, please specify
placeholder: e.g., Wayfire, Mutter, etc.
validations:
required: false
- type: dropdown
id: distribution
attributes:
label: Distribution
options:
- Arch Linux
- CachyOS
- Fedora
- NixOS
- Debian
- Ubuntu
- Gentoo
- OpenSUSE
- Other (specify below)
validations:
required: true
- type: input
id: distribution_other
attributes:
label: If Other, please specify
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: dropdown
id: installation_method
attributes:
label: Select your Installation Method
options:
- DankInstaller
- Distro Packaging
- Source
validations:
required: true
- type: dropdown
id: original_installation_method_different
attributes:
label: Was your original Installation method different?
options:
- "Yes"
- No (specify below)
default: 0
validations:
required: false
- type: input
id: original_installation_method_specify
attributes:
label: If no, specify
placeholder: e.g., Distro Packaging, then Source
validations:
required: false
- type: textarea
id: dms_doctor
attributes:
label: dms doctor -vC
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
placeholder: Paste the output of `dms doctor -vC` here
value: |
<details>
<summary>Click to expand</summary>
</details>
validations:
required: false
- type: textarea
id: description
attributes:
label: Description
description: Brief description of the support needed
placeholder: What do you need help with?
validations:
required: true
- type: textarea
id: solutions_tried
attributes:
label: Solutions Tried
description: Describe what you've tried so far (commands, documentation, etc.)
placeholder: List steps or resources you've already tried
validations:
required: false
- type: textarea
id: configuration
attributes:
label: Configuration Details
description: Include any relevant configuration if relevant
placeholder: Add configuration or environment info
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots/Recordings
description: If applicable, add screenshots or screen recordings
placeholder: Attach images or videos here
validations:
required: false

31
.github/workflows/dms-stable.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Update stable branch
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
update-stable:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Push to stable branch
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force

View File

@@ -28,25 +28,20 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
- name: Add flathub
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Add a flatpak that mutagen could support
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Format check
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.6
working-directory: core
- name: Test
run: go test -v ./...

29
.github/workflows/prek.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Pre-commit Checks
on:
push:
pull_request:
branches: [master, main]
jobs:
pre-commit-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
- name: Add flathub
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Add a flatpak that mutagen could support
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: core/go.mod
- name: run pre-commit hooks
uses: j178/prek-action@v1

View File

@@ -1,16 +1,19 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g., v1.0.1)"
required: true
type: string
permissions:
contents: write
actions: write
concurrency:
group: release-${{ github.ref_name }}
group: release-${{ inputs.tag }}
cancel-in-progress: true
jobs:
@@ -24,10 +27,14 @@ jobs:
run:
working-directory: core
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Set up Go
@@ -54,7 +61,7 @@ jobs:
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }}
cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }}
@@ -68,7 +75,7 @@ jobs:
run: |
set -eux
cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-${{ matrix.arch }}
@@ -91,7 +98,7 @@ jobs:
run: |
set -eux
cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }}
@@ -171,17 +178,23 @@ jobs:
runs-on: ubuntu-24.04
needs: [build-core] #, update-versions]
env:
TAG: ${{ github.ref_name }}
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${{ github.ref_name }}
git checkout ${{ github.ref_name }}
git fetch origin --force tag ${TAG}
git checkout ${TAG}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Download core artifacts
uses: actions/download-artifact@v4
@@ -221,6 +234,7 @@ jobs:
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only
### Checksums
@@ -273,6 +287,9 @@ jobs:
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
# Copy root assets directory to quickshell for systemd service and desktop file
cp -r assets quickshell/
# Tar the CONTENTS of quickshell/, not the directory itself
(cd quickshell && tar --exclude='.git' \
--exclude='.github' \
@@ -376,6 +393,19 @@ jobs:
rm -rf _temp_full
done
- name: Generate vendored source tarball
run: |
set -euxo pipefail
VERSION_NUM=${TAG#v}
cd core
go mod vendor
cd ..
tar czf "_release_assets/dms-cli-${VERSION_NUM}.tar.gz" \
--transform "s,^core/,dms-cli-${VERSION_NUM}/," \
--exclude='core/.git' \
core/
(cd _release_assets && sha256sum "dms-cli-${VERSION_NUM}.tar.gz" > "dms-cli-${VERSION_NUM}.tar.gz.sha256")
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
@@ -387,289 +417,3 @@ jobs:
prerelease: ${{ contains(env.TAG, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trigger-obs-update:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Update OBS packages
run: |
VERSION="${{ github.ref_name }}"
cd distro
bash scripts/obs-upload.sh dms "Update to $VERSION"
trigger-ppa-update:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Upload to PPA
run: |
VERSION="${{ github.ref_name }}"
cd distro/ubuntu/ppa
bash create-and-upload.sh ../dms dms questing
copr-build:
runs-on: ubuntu-latest
needs: release
env:
TAG: ${{ github.ref_name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building DMS stable version: $VERSION"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: 1%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "SRPM built: $SRPM_NAME"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "Uploading SRPM to avengemedia/dms..."
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
fi

View File

@@ -3,8 +3,17 @@ name: DMS Copr Stable Release
on:
workflow_dispatch:
inputs:
package:
description: 'Package to build (dms, dms-greeter, or both)'
required: false
default: 'dms'
type: choice
options:
- dms
- dms-greeter
- both
version:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false
default: ''
release:
@@ -13,13 +22,32 @@ on:
default: '1'
jobs:
build-and-upload:
determine-packages:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.set-packages.outputs.packages }}
steps:
- name: Set package list
id: set-packages
run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
if [ "$PACKAGE_INPUT" = "both" ]; then
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
else
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
fi
build-and-upload:
needs: determine-packages
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
@@ -39,210 +67,84 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release=$RELEASE" >> $GITHUB_OUTPUT
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
echo "✅ RPM build environment ready"
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
echo "📦 Downloading DMS QML source for v${VERSION}..."
# Download DMS QML source
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
echo "✅ Source downloaded"
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
PACKAGE="${{ matrix.package }}"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
# Copy spec file from repository
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
# Replace placeholders with actual values
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
Name: dms
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
# This ensures the correct architecture is used for each build target
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
# Download dms-cli for target architecture
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
# Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
%posttrans
# Clean up old installation path from previous versions (only if empty)
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
# Remove directories only if empty (preserves any user-added files)
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Signal running DMS instances to reload (harmless if none running)
pkill -USR1 -x dms >/dev/null 2>&1 || :
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%files -n dms-cli
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
echo ""
echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
echo "🔨 Building SRPM..."
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
PACKAGE="${{ matrix.package }}"
echo "🔨 Building SRPM for ${PACKAGE}..."
rpmbuild -bs ${PACKAGE}.spec
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "✅ SRPM built: $SRPM_NAME"
echo ""
echo "=== SRPM Info ==="
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
@@ -252,37 +154,57 @@ jobs:
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
echo "✅ Copr CLI configured"
- name: Determine Copr project
id: copr_project
run: |
PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else
echo "❌ Unknown package: $PACKAGE"
exit 1
fi
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
echo "✅ Copr project: $COPR_PROJECT"
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "🚀 Uploading SRPM to avengemedia/dms..."
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
PACKAGE="${{ matrix.package }}"
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
echo " SRPM: $(basename $SRPM)"
echo " Version: $VERSION"
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "✅ Build submitted successfully!"
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
echo "🔗 https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
else
echo "⚠️ Could not extract build ID, but upload may have succeeded"
fi
- name: Build summary
if: always()
run: |
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
PACKAGE="${{ matrix.package }}"
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY

View File

@@ -4,107 +4,186 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Package to update (dms, dms-git, or all)'
required: false
default: 'all'
description: "Package to update"
required: true
type: choice
options:
- dms
- dms-git
- dms-greeter
- all
default: "dms"
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false
default: ''
push:
tags:
- 'v*'
default: ""
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates
id: check
env:
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check dms stable tag
# Sets LATEST_TAG variable in parent scope if update needed
check_dms_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update both stable packages
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_dms_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
cd "${{ github.workspace }}"
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
echo "Manual trigger: $PKG"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
# Fallback - proceed
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
@@ -113,119 +192,162 @@ jobs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
if: needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine packages to update
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
# Use check-updates outputs when available
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Use version from check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - dms-git only
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
# Manual workflow dispatch
# Determine version for dms stable and dms-greeter using the API
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Using latest release from API: $LATEST_TAG"
else
echo "ERROR: Could not fetch latest release from API"
exit 1
fi
fi
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
else
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
fi
fi
- name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry
# Single changelog entry (git snapshots don't need history)
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
} > distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
# Single changelog entry (git snapshots don't need history)
CHANGELOG_DATE=$(date -R)
{
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
echo ""
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version
- name: Update stable version (dms + dms-greeter)
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update Debian _service files
PACKAGES="${{ steps.packages.outputs.packages }}"
echo "==> Updating packaging files to version: $VERSION_NO_V"
# Update dms spec and changelog when dms is in the upload list
if [[ "$PACKAGES" == *"dms"* ]]; then
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
DATE_STR=$(date "+%a %b %d %Y")
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo "- Update to stable $VERSION release"
} > distro/opensuse/dms.spec
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog"
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
fi
fi
# Update dms-greeter changelog when dms-greeter is in the upload list
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms, dms-greeter stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version-file: ./core/go.mod
- name: Install OSC
run: |
@@ -244,33 +366,148 @@ jobs:
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
id: upload
env:
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
TAG_VERSION: ${{ steps.packages.outputs.version }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
echo "uploaded_packages=" >> $GITHUB_OUTPUT
echo "skipped_packages=" >> $GITHUB_OUTPUT
exit 0
fi
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi
if [[ "$PACKAGES" == "all" ]]; then
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PKG to OBS..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
LOG_FILE=$(mktemp)
set +e
if [[ "$PKG" == "dms-git" ]]; then
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
else
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
fi
STATUS=$?
set -e
cat "$LOG_FILE"
if [[ $STATUS -ne 0 ]]; then
rm -f "$LOG_FILE"
echo "❌ Upload failed for $PKG"
exit $STATUS
fi
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
echo " $PKG is already up to date. Skipped."
SKIPPED_PACKAGES+=("$PKG")
else
UPLOADED_PACKAGES+=("$PKG")
fi
rm -f "$LOG_FILE"
done
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
- name: Summary
if: always()
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
else
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
UPLOADED_COUNT=0
SKIPPED_COUNT=0
if [[ -n "$UPLOADED_PACKAGES" ]]; then
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
fi
if [[ -n "$SKIPPED_PACKAGES" ]]; then
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
fi
in_list() {
local item="$1"
local list="$2"
[[ " $list " == *" $item "* ]]
}
if [[ "${{ job.status }}" == "success" ]]; then
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
else
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
STATUS_ICON="✅"
STATUS_TEXT="uploaded"
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
STATUS_ICON=""
STATUS_TEXT="up to date (skipped)"
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
STATUS_ICON="❌"
STATUS_TEXT="failed"
fi
case "$PKG" in
dms)
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY

View File

@@ -4,15 +4,15 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: 'dms-git'
default: "dms-git"
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
default: ''
default: ""
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
jobs:
check-updates:
@@ -32,41 +32,113 @@ jobs:
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
# Get current commit hash (8 chars to match changelog format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check stable package tag
check_stable_package() {
local PKG="$1"
local PPA_NAME="$2"
# Use git ls-remote to find the latest tag, sorted by version (descending)
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT
if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
# Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_stable_package "dms" "dms"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_stable_package "dms-greeter" "danklinux"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: $PKG"
fi
else
# Fallback
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
@@ -75,21 +147,19 @@ jobs:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
if: needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: false
go-version-file: ./core/go.mod
cache: false
- name: Install build dependencies
run: |
@@ -102,7 +172,7 @@ jobs:
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@@ -110,79 +180,101 @@ jobs:
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
id: packages
run: |
# Use packages determined by check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
fi
- name: Upload to PPA
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
exit 0
fi
# Export REBUILD_RELEASE so ppa-build.sh can use it
if [[ -n "$REBUILD_RELEASE" ]]; then
export REBUILD_RELEASE
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
# Map package to PPA name
case "$PKG" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
echo "⚠️ Unknown package: $PKG, skipping"
continue
;;
esac
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "Uploading $PKG to PPA $PPA_NAME..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-greeter to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
else
PPA_NAME="$PACKAGES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
fi
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
done
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
- name: Summary
if: always()
run: |
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-git" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
if [[ -z "$PACKAGES" ]]; then
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY

View File

@@ -36,17 +36,17 @@ jobs:
run: |
set -euo pipefail
echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dmsCli 2>&1); then
if output=$(nix build .#dms-shell 2>&1); then
echo "Build succeeded, no hash update needed"
exit 0
fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
echo "Verifying build with new vendorHash..."
nix build .#dmsCli
nix build .#dms-shell
echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
else
echo "No changes to flake.nix"
fi

13
.gitignore vendored
View File

@@ -56,6 +56,8 @@ UNUSED
CLAUDE-activeContext.md
CLAUDE-temp.md
AGENTS-activeContext.md
AGENTS-temp.md
# Auto-generated theme files
*.generated.*
@@ -96,20 +98,17 @@ go.work
go.work.sum
# env file
.env
.env*
# Editor/IDE
# .idea/
# .vscode/
vim/
bin/
# Extracted source trees in Ubuntu package directories
distro/ubuntu/*/dms-git-repo/
distro/ubuntu/*/DankMaterialShell-*/
distro/ubuntu/danklinux/*/dsearch-*/
distro/ubuntu/danklinux/*/dgop-*/
# direnv
.envrc
.direnv/
quickshell/dms-plugins
__pycache__

22
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,22 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: end-of-file-fixer
- repo: local
hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
language: system
types: [shell]
- repo: local
hooks:
- id: go-mod-tidy
name: go mod tidy
entry: bash -c 'cd core && go mod tidy'
language: system
files: ^core/.*\.(go|mod|sum)$
pass_filenames: false

33
CHANGELOG.MD Normal file
View File

@@ -0,0 +1,33 @@
This file is more of a quick reference so I know what to account for before next releases.
# 1.4.0
- Overhauled system monitor, graphs, styling
- dbus API for plugins, KDEConnect
- new dank16 algorithm
- launcher actions, customize env, args, name, icon
- launcher v2 - omega stuff, GIF search, supa powerful
- dock on bar
- window rule manager, with IPC - #TODO verify RTL layout (niri only)
# 1.2.0
- Added clipboard and clipboard history integration
- Added swipe to dismiss notification popups and from center
- Added paste from clipboard history view - requires wtype
- Optimize surface damage of OSD & Toast
- Add monitor configurator (niri, Hyprland, MangoWC)
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
- requires intervention and doc update
- Added desktop widget plugins
- dev guidance available
- builtin clock & dgop widgets
- new IPC targets
- Initial RTL support/i18n
- Theme registry
- Notification persistence & history
- **BREAKING** vscode theme needs re-installed
- dms doctor cmd
- niri/hypr/mango gaps/window/border overrides
- settings search
- notification display ops on lock screen

View File

@@ -6,10 +6,10 @@ To contribute fork this repository, make your changes, and open a pull request.
## Setup
Enable pre-commit hooks to catch CI failures before pushing:
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash
git config core.hooksPath .githooks
prek install
```
### Nix Development Shell
@@ -21,7 +21,8 @@ nix develop
```
This will provide:
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make
- Quickshell and required QML packages
- Properly configured QML2_IMPORT_PATH
@@ -36,10 +37,43 @@ This is a monorepo, the easiest thing to do is to open an editor in either `quic
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
**Note:** Paths may vary by distribution. Below are examples for Arch Linux and Fedora.
**Arch Linux:**
```json
{
"[qml]": {
"editor.defaultFormatter": "qt-project.qmlls",
"editor.formatOnSave": true
},
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls",
"qt-core.additionalQtPaths": [
{
"name": "Qt-6.x-linux-g++",
"path": "/usr/bin/qmake"
}
]
}
```
**Fedora:**
```json
{
"[qml]": {
"editor.defaultFormatter": "qt-project.qmlls",
"editor.formatOnSave": true
},
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/bin/qmlls",
"qt-core.additionalQtPaths": [
{
"name": "Qt-6.x-Fedora-linux-g++",
"path": "/usr/bin/qmake6"
}
]
}
```
@@ -54,6 +88,20 @@ touch .qmlls.ini
5. Make your changes, test, and open a pull request.
### I18n/Localization
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
```qml
import qs.Common
Text {
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
}
```
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)

View File

@@ -43,7 +43,6 @@ install-shell:
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
@echo "Shell files installed"
install-completions:
@@ -59,10 +58,10 @@ install-completions:
install-systemd:
@echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
install-icon:

View File

@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
</div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
@@ -163,7 +163,7 @@ quickshell -p quickshell/
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
# Use in home-manager or NixOS configuration
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
imports = [ inputs.dms.homeModules.dank-material-shell ];
}
```

View File

@@ -1,10 +1,10 @@
[Desktop Entry]
Type=Application
Name=DMS Application Picker
Name=DMS
Comment=Select an application to open links and files
Exec=dms open %u
Icon=danklogo
Terminal=false
NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
Categories=Utility;

View File

@@ -9,9 +9,9 @@ Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always
Restart=on-failure
RestartSec=1.23
TimeoutStopSec=10
[Install]
WantedBy=graphical-session.target
WantedBy=graphical-session.target

View File

@@ -102,7 +102,11 @@ linters:
- linters:
- ineffassign
path: internal/proto/
# binary.Write to bytes.Buffer can't fail
# binary.Write/Read to bytes.Buffer can't fail
- linters:
- errcheck
text: "Error return value of `binary\\.Write` is not checked"
text: "Error return value of `binary\\.(Write|Read)` is not checked"
# bytes.Reader.Read can't fail (reads from memory)
- linters:
- errcheck
text: "Error return value of `buf\\.Read` is not checked"

View File

@@ -56,3 +56,21 @@ packages:
outpkg: mocks_version
interfaces:
VersionFetcher:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
config:
dir: "internal/mocks/wlcontext"
outpkg: mocks_wlcontext
interfaces:
WaylandContext:
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
config:
dir: "internal/mocks/wlclient"
outpkg: mocks_wlclient
interfaces:
WaylandDisplay:
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
config:
dir: "internal/mocks/utils"
outpkg: mocks_utils
interfaces:
AppChecker:

View File

@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.9.0
hooks:
- id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...
language: system
pass_filenames: false
types: [go]

View File

@@ -14,34 +14,63 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
## System Integration
**Wayland Protocols**
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
- `wp-viewporter` - Fractional scaling support
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
- `ext-workspace-v1` - Workspace protocol support
- `wlr-output-management-unstable-v1` - Display configuration
### Wayland Protocols (Client)
**DBus Interfaces**
- NetworkManager/iwd - Network management
- logind - Session control and inhibit locks
- accountsservice - User account information
- CUPS - Printer management
- Custom IPC via unix socket (JSON API)
All Wayland protocols are consumed as a client - connecting to the compositor.
**Hardware Control**
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
- Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
| Protocol | Purpose |
| ----------------------------------------- | ----------------------------------------------------------- |
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
| `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
| `wlr-output-management-unstable-v1` | Display configuration |
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
| `ext-data-control-v1` | Clipboard history and persistence |
| `ext-workspace-v1` | Workspace integration |
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
### DBus Interfaces
**Client (consuming external services):**
| Interface | Purpose |
| -------------------------------- | --------------------------------------------- |
| `org.bluez` | Bluetooth management with pairing agent |
| `org.freedesktop.NetworkManager` | Network management |
| `net.connman.iwd` | iwd Wi-Fi backend |
| `org.freedesktop.network1` | systemd-networkd integration |
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
| `org.freedesktop.Accounts` | User account information |
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
| CUPS via IPP + D-Bus | Printer management with job notifications |
**Server (implementing interfaces):**
| Interface | Purpose |
| ----------------------------- | -------------------------------------- |
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
Custom IPC via unix socket (JSON API) for shell communication.
### Hardware Control
| Subsystem | Method | Purpose |
| --------- | ------------------- | ---------------------------------- |
| DDC/CI | I2C direct | External monitor brightness |
| Backlight | logind or sysfs | Internal display brightness |
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
| udev | netlink monitor | Backlight device updates (for OSD) |
### Plugin System
**Plugin System**
- Plugin registry integration
- Plugin lifecycle management
- Settings persistence
## CLI Commands
- `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
@@ -67,9 +96,10 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
## Building
Requires Go 1.24+
Requires Go 1.25+
**Development build:**
```bash
make # Build dms CLI
make dankinstall # Build installer
@@ -77,6 +107,7 @@ make test # Run tests
```
**Distribution build:**
```bash
make dist # Build without update/greeter features
```
@@ -84,6 +115,7 @@ make dist # Build without update/greeter features
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
**Installation:**
```bash
sudo make install # Install to /usr/local/bin/dms
```
@@ -91,6 +123,7 @@ sudo make install # Install to /usr/local/bin/dms
## Development
**Setup pre-commit hooks:**
```bash
git config core.hooksPath .githooks
```
@@ -98,6 +131,7 @@ git config core.hooksPath .githooks
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
**Regenerating Wayland Protocol Bindings:**
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
@@ -105,6 +139,7 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
```
**Module Structure:**
- `cmd/` - Binary entrypoints (dms, dankinstall)
- `internal/distros/` - Distribution-specific installation logic
- `internal/proto/` - Wayland protocol bindings
@@ -139,4 +174,4 @@ Most packages available in standard repos. Minimal building required.
**Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
See installer output for distribution-specific details during installation.
See installer output for distribution-specific details during installation.

View File

@@ -8,7 +8,7 @@
<rect x="0" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="20" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="0" y="37" width="24" height="8" fill="#CCBEFF"/>
<!-- A -->
<rect x="36" y="5" width="20" height="8" fill="#CCBEFF"/>
<rect x="32" y="13" width="8" height="8" fill="#CCBEFF"/>
@@ -18,7 +18,7 @@
<rect x="52" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="32" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="52" y="37" width="8" height="8" fill="#CCBEFF"/>
<!-- N -->
<rect x="64" y="5" width="12" height="8" fill="#CCBEFF"/>
<rect x="92" y="5" width="8" height="8" fill="#CCBEFF"/>
@@ -32,7 +32,7 @@
<rect x="92" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="64" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="84" y="37" width="16" height="8" fill="#CCBEFF"/>
<!-- K -->
<rect x="104" y="5" width="8" height="8" fill="#CCBEFF"/>
<rect x="124" y="5" width="8" height="8" fill="#CCBEFF"/>
@@ -43,4 +43,4 @@
<rect x="120" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="104" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="124" y="37" width="8" height="8" fill="#CCBEFF"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,10 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}

View File

@@ -0,0 +1,40 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
"github.com/spf13/cobra"
)
var blurCmd = &cobra.Command{
Use: "blur",
Short: "Background blur utilities",
}
var blurCheckCmd = &cobra.Command{
Use: "check",
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
Args: cobra.NoArgs,
Run: runBlurCheck,
}
func init() {
blurCmd.AddCommand(blurCheckCmd)
}
func runBlurCheck(cmd *cobra.Command, args []string) {
supported, err := blur.ProbeSupport()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
switch supported {
case true:
fmt.Println("supported")
default:
fmt.Println("unsupported")
}
}

View File

@@ -179,7 +179,7 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
for i := 0; i < sepLen; i++ {
for range sepLen {
fmt.Print("─")
}
fmt.Println()
@@ -236,6 +236,7 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
defer ddc.Close()
time.Sleep(100 * time.Millisecond)
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
ddc.WaitPending()
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return
}

View File

@@ -0,0 +1,300 @@
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/spf13/cobra"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
ghtml "github.com/yuin/goldmark/renderer/html"
)
var (
chromaLanguage string
chromaStyle string
chromaInline bool
chromaMarkdown bool
chromaLineNumbers bool
// Caching layer for performance
lexerCache = make(map[string]chroma.Lexer)
styleCache = make(map[string]*chroma.Style)
formatterCache = make(map[string]*html.Formatter)
cacheMutex sync.RWMutex
maxFileSize = int64(5 * 1024 * 1024) // 5MB default
)
var chromaCmd = &cobra.Command{
Use: "chroma [file]",
Short: "Syntax highlight source code",
Long: `Generate syntax-highlighted HTML from source code.
Reads from file or stdin, outputs HTML with syntax highlighting.
Language is auto-detected from filename or can be specified with --language.
Examples:
dms chroma main.go
dms chroma --language python script.py
echo "def foo(): pass" | dms chroma -l python
cat code.rs | dms chroma -l rust --style dracula
dms chroma --markdown README.md
dms chroma --markdown --style github-dark notes.md
dms chroma list-languages
dms chroma list-styles`,
Args: cobra.MaximumNArgs(1),
Run: runChroma,
}
var chromaListLanguagesCmd = &cobra.Command{
Use: "list-languages",
Short: "List all supported languages",
Run: func(cmd *cobra.Command, args []string) {
for _, name := range lexers.Names(true) {
fmt.Println(name)
}
},
}
var chromaListStylesCmd = &cobra.Command{
Use: "list-styles",
Short: "List all available color styles",
Run: func(cmd *cobra.Command, args []string) {
for _, name := range styles.Names() {
fmt.Println(name)
}
},
}
func init() {
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
chromaCmd.Flags().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output")
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)")
chromaCmd.AddCommand(chromaListLanguagesCmd)
chromaCmd.AddCommand(chromaListStylesCmd)
}
func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer {
cacheMutex.RLock()
if lexer, ok := lexerCache[key]; ok {
cacheMutex.RUnlock()
return lexer
}
cacheMutex.RUnlock()
lexer := fallbackFunc()
if lexer != nil {
cacheMutex.Lock()
lexerCache[key] = lexer
cacheMutex.Unlock()
}
return lexer
}
func getCachedStyle(name string) *chroma.Style {
cacheMutex.RLock()
if style, ok := styleCache[name]; ok {
cacheMutex.RUnlock()
return style
}
cacheMutex.RUnlock()
style := styles.Get(name)
if style == nil {
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
style = styles.Fallback
}
cacheMutex.Lock()
styleCache[name] = style
cacheMutex.Unlock()
return style
}
func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter {
key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers)
cacheMutex.RLock()
if formatter, ok := formatterCache[key]; ok {
cacheMutex.RUnlock()
return formatter
}
cacheMutex.RUnlock()
var opts []html.Option
if inline {
opts = append(opts, html.WithClasses(false))
} else {
opts = append(opts, html.WithClasses(true))
}
opts = append(opts, html.TabWidth(4))
if lineNumbers {
opts = append(opts, html.WithLineNumbers(true))
opts = append(opts, html.LineNumbersInTable(false))
opts = append(opts, html.WithLinkableLineNumbers(false, ""))
}
formatter := html.New(opts...)
cacheMutex.Lock()
formatterCache[key] = formatter
cacheMutex.Unlock()
return formatter
}
func runChroma(cmd *cobra.Command, args []string) {
var source string
var filename string
// Read from file or stdin
if len(args) > 0 {
filename = args[0]
// Check file size before reading
fileInfo, err := os.Stat(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err)
os.Exit(1)
}
if fileInfo.Size() > maxFileSize {
fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n",
fileInfo.Size(), maxFileSize)
fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n")
}
content, err := os.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
os.Exit(1)
}
source = string(content)
} else {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) != 0 {
_ = cmd.Help()
os.Exit(0)
}
content, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
os.Exit(1)
}
source = string(content)
}
// Handle empty input
if strings.TrimSpace(source) == "" {
return
}
// Handle Markdown rendering
if chromaMarkdown {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle(chromaStyle),
highlighting.WithFormatOptions(
html.WithClasses(!chromaInline),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
ghtml.WithHardWraps(),
ghtml.WithXHTML(),
),
)
var buf bytes.Buffer
if err := md.Convert([]byte(source), &buf); err != nil {
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
os.Exit(1)
}
fmt.Print(buf.String())
return
}
// Detect or use specified lexer
var lexer chroma.Lexer
if chromaLanguage != "" {
lexer = getCachedLexer(chromaLanguage, func() chroma.Lexer {
l := lexers.Get(chromaLanguage)
if l == nil {
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
os.Exit(1)
}
return l
})
} else if filename != "" {
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
return lexers.Match(filename)
})
}
// Try content analysis if no lexer found (limit to first 1KB for performance)
if lexer == nil {
analyzeContent := source
if len(source) > 1024 {
analyzeContent = source[:1024]
}
lexer = lexers.Analyse(analyzeContent)
}
// Fallback to plaintext
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer)
// Get cached style
style := getCachedStyle(chromaStyle)
// Get cached formatter
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
// Tokenize
iterator, err := lexer.Tokenise(nil, source)
if err != nil {
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
os.Exit(1)
}
// Format and output
if chromaLineNumbers {
var buf bytes.Buffer
if err := formatter.Format(&buf, style, iterator); err != nil {
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
os.Exit(1)
}
// Add spacing between line numbers
output := buf.String()
output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>")
fmt.Print(output)
} else {
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
os.Exit(1)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ package main
import (
"fmt"
"os"
"os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
"github.com/spf13/cobra"
)
@@ -121,13 +121,7 @@ func runColorPick(cmd *cobra.Command, args []string) {
}
func copyToClipboard(text string) {
var cmd *exec.Cmd
if _, err := exec.LookPath("wl-copy"); err == nil {
cmd = exec.Command("wl-copy", text)
} else {
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
return
if err := clipboard.CopyText(text); err != nil {
fmt.Fprintln(os.Stderr, "clipboard copy failed:", err)
}
_ = cmd.Run()
}

View File

@@ -64,10 +64,8 @@ var killCmd = &cobra.Command{
}
var ipcCmd = &cobra.Command{
Use: "ipc",
Short: "Send IPC commands to running DMS shell",
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
PreRunE: findConfig,
Use: "ipc [target] [function] [args...]",
Short: "Send IPC commands to running DMS shell",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
@@ -77,6 +75,13 @@ var ipcCmd = &cobra.Command{
},
}
func init() {
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = findConfig(cmd, args)
printIPCHelp()
})
}
var debugSrvCmd = &cobra.Command{
Use: "debug-srv",
Short: "Start the debug server",
@@ -171,7 +176,6 @@ var pluginsUpdateCmd = &cobra.Command{
}
func runVersion(cmd *cobra.Command, args []string) {
printASCII()
fmt.Printf("%s\n", formatVersion(Version))
}
@@ -220,7 +224,7 @@ func getBaseVersion() string {
}
// Fallback
return "0.6.2"
return "1.0.2"
}
func startDebugServer() error {
@@ -512,6 +516,15 @@ func getCommonCommands() []*cobra.Command {
colorCmd,
screenshotCmd,
notifyActionCmd,
notifyCmd,
genericNotifyActionCmd,
matugenCmd,
clipboardCmd,
chromaCmd,
doctorCmd,
configCmd,
dlCmd,
randrCmd,
blurCmd,
}
}

View File

@@ -0,0 +1,318 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Configuration utilities",
}
var resolveIncludeCmd = &cobra.Command{
Use: "resolve-include <compositor> <filename>",
Short: "Check if a file is included in compositor config",
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runResolveInclude,
}
func init() {
configCmd.AddCommand(resolveIncludeCmd)
}
type IncludeResult struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
}
func runResolveInclude(cmd *cobra.Command, args []string) {
compositor := strings.ToLower(args[0])
filename := args[1]
var result IncludeResult
var err error
switch compositor {
case "hyprland":
result, err = checkHyprlandInclude(filename)
case "niri":
result, err = checkNiriInclude(filename)
case "mangowc", "dwl", "mango":
result, err = checkMangoWCInclude(filename)
default:
log.Fatalf("Unknown compositor: %s", compositor)
}
if err != nil {
log.Fatalf("Error checking include: %v", err)
}
output, _ := json.Marshal(result)
fmt.Fprintln(os.Stdout, string(output))
}
func checkHyprlandInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
return IncludeResult{}, err
}
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
if _, err := os.Stat(targetPath); err == nil {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
}
processed := make(map[string]bool)
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
if processed[absPath] {
return false
}
processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return false
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "source") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) < 2 {
continue
}
sourcePath := strings.TrimSpace(parts[1])
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
continue
}
if hyprlandFindInclude(expanded, target, processed) {
return true
}
}
return false
}
func checkNiriInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
return IncludeResult{}, err
}
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
if _, err := os.Stat(targetPath); err == nil {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "config.kdl")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
}
processed := make(map[string]bool)
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
if processed[absPath] {
return false
}
processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return false
}
baseDir := filepath.Dir(absPath)
content := string(data)
for _, line := range strings.Split(content, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "include") {
continue
}
startQuote := strings.Index(trimmed, "\"")
if startQuote == -1 {
continue
}
endQuote := strings.LastIndex(trimmed, "\"")
if endQuote <= startQuote {
continue
}
includePath := trimmed[startQuote+1 : endQuote]
if matchesTarget(includePath, target) {
return true
}
fullPath := includePath
if !filepath.IsAbs(includePath) {
fullPath = filepath.Join(baseDir, includePath)
}
if niriFindInclude(fullPath, target, processed) {
return true
}
}
return false
}
func checkMangoWCInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/mango")
if err != nil {
return IncludeResult{}, err
}
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
if _, err := os.Stat(targetPath); err == nil {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "config.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
mainConfig = filepath.Join(configDir, "mango.conf")
}
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
}
processed := make(map[string]bool)
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
if processed[absPath] {
return false
}
processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return false
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "source") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) < 2 {
continue
}
sourcePath := strings.TrimSpace(parts[1])
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
continue
}
if mangowcFindInclude(expanded, target, processed) {
return true
}
}
return false
}
func matchesTarget(path, target string) bool {
path = strings.TrimPrefix(path, "./")
target = strings.TrimPrefix(target, "./")
return path == target || strings.HasSuffix(path, "/"+target)
}

View File

@@ -22,6 +22,7 @@ func init() {
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
dank16Cmd.Flags().Bool("neovim", false, "Output in Neovim plugin format")
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
@@ -40,6 +41,7 @@ func runDank16(cmd *cobra.Command, args []string) {
isJson, _ := cmd.Flags().GetBool("json")
isKitty, _ := cmd.Flags().GetBool("kitty")
isFoot, _ := cmd.Flags().GetBool("foot")
isNeovim, _ := cmd.Flags().GetBool("neovim")
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
isGhostty, _ := cmd.Flags().GetBool("ghostty")
isWezterm, _ := cmd.Flags().GetBool("wezterm")
@@ -116,6 +118,8 @@ func runDank16(cmd *cobra.Command, args []string) {
fmt.Print(dank16.GenerateGhosttyTheme(colors))
} else if isWezterm {
fmt.Print(dank16.GenerateWeztermTheme(colors))
} else if isNeovim {
fmt.Print(dank16.GenerateNeovimTheme(colors))
} else {
fmt.Print(dank16.GenerateGhosttyTheme(colors))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
)
var dlOutput string
var dlUserAgent string
var dlTimeout int
var dlIPv4Only bool
var dlCmd = &cobra.Command{
Use: "dl <url>",
Short: "Download a URL to stdout or file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := runDownload(args[0]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
func init() {
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
}
func runDownload(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("invalid request: %w", err)
}
switch {
case dlUserAgent != "":
req.Header.Set("User-Agent", dlUserAgent)
default:
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
}
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{DialContext: dialer.DialContext}
if dlIPv4Only {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp4", addr)
}
}
client := &http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
if dlOutput == "" {
_, err = io.Copy(os.Stdout, resp.Body)
return err
}
if dir := filepath.Dir(dlOutput); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("mkdir failed: %w", err)
}
}
f, err := os.Create(dlOutput)
if err != nil {
return fmt.Errorf("create failed: %w", err)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
os.Remove(dlOutput)
return fmt.Errorf("write failed: %w", err)
}
fmt.Println(dlOutput)
return nil
}

View File

@@ -4,6 +4,7 @@ package main
import (
"bufio"
"context"
"errors"
"fmt"
"os"
@@ -15,6 +16,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra"
@@ -109,16 +111,37 @@ func updateArchLinux() error {
}
var packageName string
if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
var isAUR bool
if isArchPackageInstalled("dms-shell") {
packageName = "dms-shell"
} else if isArchPackageInstalled("dms-shell-git") {
packageName = "dms-shell-git"
isAUR = true
} else if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
isAUR = true
} else {
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
fmt.Println("Info: No dms-shell package found.")
fmt.Println("Info: Falling back to git-based update method...")
return updateOtherDistros()
}
if !isAUR {
fmt.Printf("This will update %s using pacman.\n", packageName)
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
return err
}
fmt.Println("dms successfully updated")
return nil
}
var helper string
var updateCmd *exec.Cmd
@@ -377,7 +400,7 @@ func updateDMSBinary() error {
}
version := ""
for _, line := range strings.Split(string(output), "\n") {
for line := range strings.SplitSeq(string(output), "\n") {
if strings.Contains(line, "\"tag_name\"") {
parts := strings.Split(line, "\"")
if len(parts) >= 4 {
@@ -443,7 +466,7 @@ func updateDMSBinary() error {
decompressedPath := filepath.Join(tempDir, "dms")
if err := os.Chmod(decompressedPath, 0755); err != nil {
if err := os.Chmod(decompressedPath, 0o755); err != nil {
return fmt.Errorf("failed to make binary executable: %w", err)
}
@@ -454,11 +477,7 @@ func updateDMSBinary() error {
fmt.Printf("Installing to %s...\n", currentPath)
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
replaceCmd.Stdin = os.Stdin
replaceCmd.Stdout = os.Stdout
replaceCmd.Stderr = os.Stderr
if err := replaceCmd.Run(); err != nil {
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
return fmt.Errorf("failed to replace binary: %w", err)
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
@@ -57,12 +58,15 @@ var keybindsRemoveCmd = &cobra.Command{
}
func init() {
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
keybindsSetCmd.Flags().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)")
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd)
@@ -79,24 +83,35 @@ func init() {
func initializeProviders() {
registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
hyprlandProvider := providers.NewHyprlandProvider("")
if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err)
}
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
mangowcProvider := providers.NewMangoWCProvider("")
if err := registry.Register(mangowcProvider); err != nil {
log.Warnf("Failed to register MangoWC provider: %v", err)
}
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
configDir, _ := os.UserConfigDir()
if configDir != "" {
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
}
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
miracleProvider := providers.NewMiracleProvider("")
if err := registry.Register(miracleProvider); err != nil {
log.Warnf("Failed to register Miracle WM provider: %v", err)
}
if configDir != "" {
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
}
}
niriProvider := providers.NewNiriProvider("")
@@ -110,12 +125,21 @@ func initializeProviders() {
}
}
func runKeybindsList(_ *cobra.Command, _ []string) {
func runKeybindsList(cmd *cobra.Command, _ []string) {
providerList := keybinds.GetDefaultRegistry().List()
asJSON, _ := cmd.Flags().GetBool("json")
if asJSON {
output, _ := json.Marshal(providerList)
fmt.Fprintln(os.Stdout, string(output))
return
}
if len(providerList) == 0 {
fmt.Fprintln(os.Stdout, "No providers available")
return
}
fmt.Fprintln(os.Stdout, "Available providers:")
for _, name := range providerList {
fmt.Fprintf(os.Stdout, " - %s\n", name)
@@ -132,6 +156,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewSwayProvider(path)
case "scroll":
return providers.NewSwayProvider(path)
case "miracle":
return providers.NewMiracleProvider(path)
case "niri":
return providers.NewNiriProvider(path)
default:
@@ -201,6 +227,12 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
options["allow-inhibiting"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
desc, _ := cmd.Flags().GetString("desc")
if err := writable.SetBind(key, action, desc, options); err != nil {

View File

@@ -3,14 +3,14 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
@@ -32,9 +32,16 @@ var matugenQueueCmd = &cobra.Command{
Run: runMatugenQueue,
}
var matugenCheckCmd = &cobra.Command{
Use: "check",
Short: "Check which template apps are detected",
Run: runMatugenCheck,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
@@ -49,6 +56,7 @@ func init() {
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
@@ -68,6 +76,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
stockColors, _ := cmd.Flags().GetString("stock-colors")
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
return matugen.Options{
StateDir: stateDir,
@@ -75,19 +84,24 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
ConfigDir: configDir,
Kind: kind,
Value: value,
Mode: mode,
Mode: matugen.ColorMode(mode),
IconTheme: iconTheme,
MatugenType: matugenType,
RunUserTemplates: runUserTemplates,
StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark,
SkipTemplates: skipTemplates,
}
}
func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
if err := matugen.Run(opts); err != nil {
err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err)
}
}
@@ -97,33 +111,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
wait, _ := cmd.Flags().GetBool("wait")
timeout, _ := cmd.Flags().GetDuration("timeout")
socketPath := os.Getenv("DMS_SOCKET")
if socketPath == "" {
var err error
socketPath, err = server.FindSocket()
if err != nil {
log.Info("No socket available, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
}
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Info("Socket connection failed, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
defer conn.Close()
request := map[string]any{
"id": 1,
"method": "matugen.queue",
"params": map[string]any{
request := models.Request{
ID: 1,
Method: "matugen.queue",
Params: map[string]any{
"stateDir": opts.StateDir,
"shellDir": opts.ShellDir,
"configDir": opts.ConfigDir,
@@ -136,15 +127,23 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"stockColors": opts.StockColors,
"syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"skipTemplates": opts.SkipTemplates,
"wait": wait,
},
}
if err := json.NewEncoder(conn).Encode(request); err != nil {
log.Fatalf("Failed to send request: %v", err)
}
if !wait {
if err := sendServerRequestFireAndForget(request); err != nil {
log.Info("Server unavailable, running synchronously")
err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err)
}
return
}
fmt.Println("Theme generation queued")
return
}
@@ -154,17 +153,22 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
resultCh := make(chan error, 1)
go func() {
var response struct {
ID int `json:"id"`
Result any `json:"result"`
Error string `json:"error"`
}
if err := json.NewDecoder(conn).Decode(&response); err != nil {
resultCh <- fmt.Errorf("failed to read response: %w", err)
resp, ok := tryServerRequest(request)
if !ok {
log.Info("Server unavailable, running synchronously")
err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
resultCh <- matugen.ErrNoChanges
case err != nil:
resultCh <- err
default:
resultCh <- nil
}
return
}
if response.Error != "" {
resultCh <- fmt.Errorf("server error: %s", response.Error)
if resp.Error != "" {
resultCh <- fmt.Errorf("server error: %s", resp.Error)
return
}
resultCh <- nil
@@ -172,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
select {
case err := <-resultCh:
if err != nil {
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err)
}
fmt.Println("Theme generation completed")
@@ -180,3 +187,12 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
log.Fatalf("Timeout waiting for theme generation")
}
}
func runMatugenCheck(cmd *cobra.Command, args []string) {
checks := matugen.CheckTemplates(nil)
data, err := json.Marshal(checks)
if err != nil {
log.Fatalf("Failed to marshal check results: %v", err)
}
fmt.Println(string(data))
}

View File

@@ -0,0 +1,68 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
"github.com/spf13/cobra"
)
var (
notifyAppName string
notifyIcon string
notifyFile string
notifyTimeout int
)
var notifyCmd = &cobra.Command{
Use: "notify <summary> [body]",
Short: "Send a desktop notification",
Long: `Send a desktop notification with optional actions.
If --file is provided, the notification will have "Open" and "Open Folder" actions.
Examples:
dms notify "Hello" "World"
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`,
Args: cobra.MinimumNArgs(1),
Run: runNotify,
}
var genericNotifyActionCmd = &cobra.Command{
Use: "notify-action-generic",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
notify.RunActionListener(args)
},
}
func init() {
notifyCmd.Flags().StringVar(&notifyAppName, "app", "DMS", "Application name")
notifyCmd.Flags().StringVar(&notifyIcon, "icon", "", "Icon name or path")
notifyCmd.Flags().StringVar(&notifyFile, "file", "", "File path (enables Open/Open Folder actions)")
notifyCmd.Flags().IntVar(&notifyTimeout, "timeout", 5000, "Timeout in milliseconds")
}
func runNotify(cmd *cobra.Command, args []string) {
summary := args[0]
body := ""
if len(args) > 1 {
body = args[1]
}
n := notify.Notification{
AppName: notifyAppName,
Icon: notifyIcon,
Summary: summary,
Body: body,
FilePath: notifyFile,
Timeout: int32(notifyTimeout),
}
if err := notify.Send(n); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@@ -1,17 +1,14 @@
package main
import (
"encoding/json"
"fmt"
"mime"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
@@ -93,32 +90,6 @@ func mimeTypeToCategories(mimeType string) []string {
}
func runOpen(target string) {
socketPath, err := server.FindSocket()
if err != nil {
log.Warnf("DMS socket not found: %v", err)
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Warnf("DMS socket connection failed: %v", err)
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
defer conn.Close()
buf := make([]byte, 1)
for {
_, err := conn.Read(buf)
if err != nil {
return
}
if buf[0] == '\n' {
break
}
}
// Parse file:// URIs to extract the actual file path
actualTarget := target
detectedMimeType := openMimeType
@@ -160,6 +131,12 @@ func runOpen(target string) {
detectedRequestType = "url"
}
log.Infof("Detected HTTP(S) URL")
} else if strings.HasPrefix(target, "dms://") {
// Handle DMS internal URLs (theme/plugin install, etc.)
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected DMS internal URL")
} else if _, err := os.Stat(target); err == nil {
// Handle local file paths directly (not file:// URIs)
// Convert to absolute path
@@ -206,7 +183,7 @@ func runOpen(target string) {
}
method := "apppicker.open"
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
method = "browser.open"
params["url"] = target
}
@@ -219,8 +196,9 @@ func runOpen(target string) {
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
if err := json.NewEncoder(conn).Encode(req); err != nil {
log.Fatalf("Failed to send request: %v", err)
if err := sendServerRequestFireAndForget(req); err != nil {
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
log.Infof("Request sent successfully")

View File

@@ -0,0 +1,58 @@
package main
import (
"encoding/json"
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var randrCmd = &cobra.Command{
Use: "randr",
Short: "Query output display information",
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
Run: runRandr,
}
func init() {
randrCmd.Flags().Bool("json", false, "Output in JSON format")
}
type randrJSON struct {
Outputs []randrOutput `json:"outputs"`
}
func runRandr(cmd *cobra.Command, args []string) {
outputs, err := queryRandr()
if err != nil {
log.Fatalf("%v", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
data, err := json.Marshal(randrJSON{Outputs: outputs})
if err != nil {
log.Fatalf("failed to marshal JSON: %v", err)
}
fmt.Println(string(data))
return
}
for i, out := range outputs {
if i > 0 {
fmt.Println()
}
status := "enabled"
if !out.Enabled {
status = "disabled"
}
fmt.Printf("%s (%s)\n", out.Name, status)
fmt.Printf(" Scale: %.4g\n", out.Scale)
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
if out.Refresh > 0 {
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
}
}
}

View File

@@ -7,9 +7,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
@@ -20,11 +18,9 @@ var rootCmd = &cobra.Command{
Use: "dms",
Short: "dms CLI",
Long: "dms is the DankMaterialShell management CLI and backend server.",
Run: runInteractiveMode,
}
func init() {
// Add the -c flag
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
}
@@ -38,7 +34,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
if statErr == nil && !info.IsDir() {
configPath = customConfigPath
log.Debug("Using config from: %s", configPath)
return nil // <-- Guard statement
return nil
}
if statErr != nil {
@@ -50,15 +46,18 @@ func findConfig(cmd *cobra.Command, args []string) error {
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil // <-- Guard statement
if len(getAllDMSPIDs()) == 0 {
os.Remove(configStateFile)
} else {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil
}
os.Remove(configStateFile)
}
}
@@ -73,18 +72,3 @@ func findConfig(cmd *cobra.Command, args []string) error {
log.Debug("Using config from: %s", configPath)
return nil
}
func runInteractiveMode(cmd *cobra.Command, args []string) {
detector, _ := dms.NewDetector()
if !detector.IsDMSInstalled() {
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
log.Info("Please install DMS using dankinstall before using this management interface.")
os.Exit(1)
}
model := dms.NewModel(Version)
p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatalf("Error running program: %v", err)
}
}

View File

@@ -4,25 +4,25 @@ import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
var (
ssOutputName string
ssIncludeCursor bool
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
ssOutputName string
ssCursor string
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
)
var screenshotCmd = &cobra.Command{
@@ -52,7 +52,7 @@ Examples:
dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor
dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
}
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor
if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn
}
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify
@@ -257,9 +259,7 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
}
}
cmd := exec.Command("wl-copy", "--type", mimeType)
cmd.Stdin = &data
return cmd.Run()
return clipboard.Copy(data.Bytes(), mimeType)
}
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {

View File

@@ -4,19 +4,24 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
)
var setupCmd = &cobra.Command{
Use: "setup",
Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts",
Use: "setup",
Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err)
@@ -24,9 +29,248 @@ var setupCmd = &cobra.Command{
},
}
var setupBindsCmd = &cobra.Command{
Use: "binds",
Short: "Deploy default keybinds config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("binds"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupLayoutCmd = &cobra.Command{
Use: "layout",
Short: "Deploy default layout config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("layout"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupColorsCmd = &cobra.Command{
Use: "colors",
Short: "Deploy default colors config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("colors"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupAlttabCmd = &cobra.Command{
Use: "alttab",
Short: "Deploy default alt-tab config (niri only)",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("alttab"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupOutputsCmd = &cobra.Command{
Use: "outputs",
Short: "Deploy default outputs config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("outputs"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupCursorCmd = &cobra.Command{
Use: "cursor",
Short: "Deploy default cursor config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("cursor"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupWindowrulesCmd = &cobra.Command{
Use: "windowrules",
Short: "Deploy default window rules config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("windowrules"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
type dmsConfigSpec struct {
niriFile string
hyprFile string
niriContent func(terminal string) string
hyprContent func(terminal string) string
}
var dmsConfigSpecs = map[string]dmsConfigSpec{
"binds": {
niriFile: "binds.kdl",
hyprFile: "binds.conf",
niriContent: func(t string) string {
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
},
hyprContent: func(t string) string {
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
},
},
"layout": {
niriFile: "layout.kdl",
hyprFile: "layout.conf",
niriContent: func(_ string) string { return config.NiriLayoutConfig },
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
},
"colors": {
niriFile: "colors.kdl",
hyprFile: "colors.conf",
niriContent: func(_ string) string { return config.NiriColorsConfig },
hyprContent: func(_ string) string { return config.HyprColorsConfig },
},
"alttab": {
niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig },
},
"outputs": {
niriFile: "outputs.kdl",
hyprFile: "outputs.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
},
"cursor": {
niriFile: "cursor.kdl",
hyprFile: "cursor.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
},
"windowrules": {
niriFile: "windowrules.kdl",
hyprFile: "windowrules.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
},
}
func detectTerminal() (string, error) {
terminals := []string{"ghostty", "foot", "kitty", "alacritty"}
var found []string
for _, t := range terminals {
if utils.CommandExists(t) {
found = append(found, t)
}
}
switch len(found) {
case 0:
return "ghostty", nil
case 1:
return found[0], nil
}
fmt.Println("Multiple terminals detected:")
for i, t := range found {
fmt.Printf("%d) %s\n", i+1, t)
}
fmt.Printf("\nChoice (1-%d): ", len(found))
var response string
fmt.Scanln(&response)
response = strings.TrimSpace(response)
choice := 0
fmt.Sscanf(response, "%d", &choice)
if choice < 1 || choice > len(found) {
return "", fmt.Errorf("invalid choice")
}
return found[choice-1], nil
}
func detectCompositorForSetup() (string, error) {
compositors := greeter.DetectCompositors()
switch len(compositors) {
case 0:
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
case 1:
return strings.ToLower(compositors[0]), nil
}
selected, err := greeter.PromptCompositorChoice(compositors)
if err != nil {
return "", err
}
return strings.ToLower(selected), nil
}
func runSetupDmsConfig(name string) error {
spec, ok := dmsConfigSpecs[name]
if !ok {
return fmt.Errorf("unknown config: %s", name)
}
compositor, err := detectCompositorForSetup()
if err != nil {
return err
}
var filename string
var contentFn func(string) string
switch compositor {
case "niri":
filename = spec.niriFile
contentFn = spec.niriContent
case "hyprland":
filename = spec.hyprFile
contentFn = spec.hyprContent
default:
return fmt.Errorf("unsupported compositor: %s", compositor)
}
if filename == "" {
return fmt.Errorf("%s is not supported for %s", name, compositor)
}
var dmsDir string
switch compositor {
case "niri":
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
case "hyprland":
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
}
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
path := filepath.Join(dmsDir, filename)
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
return fmt.Errorf("%s already exists and is not empty: %s", name, path)
}
terminal := "ghostty"
if contentFn != nil && name == "binds" {
terminal, err = detectTerminal()
if err != nil {
return err
}
}
content := contentFn(terminal)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err)
}
fmt.Printf("Deployed %s to %s\n", name, path)
return nil
}
func runSetup() error {
fmt.Println("=== DMS Configuration Setup ===")
ensureInputGroup()
wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
@@ -100,6 +344,37 @@ func runSetup() error {
return nil
}
// Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() {
if !utils.HasGroup("input") {
return
}
currentUser := os.Getenv("USER")
if currentUser == "" {
currentUser = os.Getenv("LOGNAME")
}
if currentUser == "" {
return
}
out, err := execGroups(currentUser)
if err == nil && strings.Contains(out, "input") {
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
return
}
fmt.Println("Adding user to input group for Caps Lock OSD support...")
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
} else {
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
}
}
func execGroups(user string) (string, error) {
out, err := exec.Command("groups", user).Output()
return string(out), err
}
func promptCompositor() (deps.WindowManager, bool) {
fmt.Println("Select compositor:")
fmt.Println("1) Niri")

View File

@@ -0,0 +1,338 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers"
"github.com/spf13/cobra"
)
var windowrulesCmd = &cobra.Command{
Use: "windowrules",
Short: "Manage window rules",
}
var windowrulesListCmd = &cobra.Command{
Use: "list [compositor]",
Short: "List all window rules",
Long: "List all window rules from compositor config file. Returns JSON with rules and DMS status.",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runWindowrulesList,
}
var windowrulesAddCmd = &cobra.Command{
Use: "add <compositor> '<json>'",
Short: "Add a window rule to DMS file",
Long: "Add a new window rule to the DMS-managed rules file.",
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runWindowrulesAdd,
}
var windowrulesUpdateCmd = &cobra.Command{
Use: "update <compositor> <id> '<json>'",
Short: "Update a window rule in DMS file",
Long: "Update an existing window rule in the DMS-managed rules file.",
Args: cobra.ExactArgs(3),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runWindowrulesUpdate,
}
var windowrulesRemoveCmd = &cobra.Command{
Use: "remove <compositor> <id>",
Short: "Remove a window rule from DMS file",
Long: "Remove a window rule from the DMS-managed rules file.",
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runWindowrulesRemove,
}
var windowrulesReorderCmd = &cobra.Command{
Use: "reorder <compositor> '<json-array-of-ids>'",
Short: "Reorder window rules in DMS file",
Long: "Reorder window rules by providing a JSON array of rule IDs in the desired order.",
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runWindowrulesReorder,
}
func init() {
configCmd.AddCommand(windowrulesCmd)
windowrulesCmd.AddCommand(windowrulesListCmd)
windowrulesCmd.AddCommand(windowrulesAddCmd)
windowrulesCmd.AddCommand(windowrulesUpdateCmd)
windowrulesCmd.AddCommand(windowrulesRemoveCmd)
windowrulesCmd.AddCommand(windowrulesReorderCmd)
}
type WindowRulesListResult struct {
Rules []windowrules.WindowRule `json:"rules"`
DMSStatus *windowrules.DMSRulesStatus `json:"dmsStatus,omitempty"`
}
type WindowRuleWriteResult struct {
Success bool `json:"success"`
ID string `json:"id,omitempty"`
Path string `json:"path,omitempty"`
Error string `json:"error,omitempty"`
}
func getCompositor(args []string) string {
if len(args) > 0 {
return strings.ToLower(args[0])
}
if os.Getenv("NIRI_SOCKET") != "" {
return "niri"
}
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
// return "hyprland"
// }
return ""
}
func writeRuleError(errMsg string) {
result := WindowRuleWriteResult{Success: false, Error: errMsg}
output, _ := json.Marshal(result)
fmt.Fprintln(os.Stdout, string(output))
os.Exit(1)
}
func writeRuleSuccess(id, path string) {
result := WindowRuleWriteResult{Success: true, ID: id, Path: path}
output, _ := json.Marshal(result)
fmt.Fprintln(os.Stdout, string(output))
}
func runWindowrulesList(cmd *cobra.Command, args []string) {
compositor := getCompositor(args)
if compositor == "" {
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
}
var result WindowRulesListResult
switch compositor {
case "niri":
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
log.Fatalf("Failed to expand niri config path: %v", err)
}
parseResult, err := providers.ParseNiriWindowRules(configDir)
if err != nil {
log.Fatalf("Failed to parse niri window rules: %v", err)
}
allRules := providers.ConvertNiriRulesToWindowRules(parseResult.Rules)
provider := providers.NewNiriWritableProvider(configDir)
dmsRulesPath := provider.GetOverridePath()
dmsRules, _ := provider.LoadDMSRules()
dmsRuleMap := make(map[int]windowrules.WindowRule)
for i, dr := range dmsRules {
dmsRuleMap[i] = dr
}
dmsIdx := 0
for i, r := range allRules {
if r.Source == dmsRulesPath {
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
allRules[i].ID = dmr.ID
allRules[i].Name = dmr.Name
}
dmsIdx++
}
}
result.Rules = allRules
result.DMSStatus = parseResult.DMSStatus
case "hyprland":
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
log.Fatalf("Failed to expand hyprland config path: %v", err)
}
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
if err != nil {
log.Fatalf("Failed to parse hyprland window rules: %v", err)
}
allRules := providers.ConvertHyprlandRulesToWindowRules(parseResult.Rules)
provider := providers.NewHyprlandWritableProvider(configDir)
dmsRulesPath := provider.GetOverridePath()
dmsRules, _ := provider.LoadDMSRules()
dmsRuleMap := make(map[int]windowrules.WindowRule)
for i, dr := range dmsRules {
dmsRuleMap[i] = dr
}
dmsIdx := 0
for i, r := range allRules {
if r.Source == dmsRulesPath {
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
allRules[i].ID = dmr.ID
allRules[i].Name = dmr.Name
}
dmsIdx++
}
}
result.Rules = allRules
result.DMSStatus = parseResult.DMSStatus
default:
log.Fatalf("Unknown compositor: %s", compositor)
}
output, _ := json.Marshal(result)
fmt.Fprintln(os.Stdout, string(output))
}
func runWindowrulesAdd(cmd *cobra.Command, args []string) {
compositor := strings.ToLower(args[0])
ruleJSON := args[1]
var rule windowrules.WindowRule
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
}
if rule.ID == "" {
rule.ID = generateRuleID()
}
rule.Enabled = true
provider := getWindowRulesProvider(compositor)
if provider == nil {
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
}
if err := provider.SetRule(rule); err != nil {
writeRuleError(err.Error())
}
writeRuleSuccess(rule.ID, provider.GetOverridePath())
}
func runWindowrulesUpdate(cmd *cobra.Command, args []string) {
compositor := strings.ToLower(args[0])
ruleID := args[1]
ruleJSON := args[2]
var rule windowrules.WindowRule
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
}
rule.ID = ruleID
provider := getWindowRulesProvider(compositor)
if provider == nil {
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
}
if err := provider.SetRule(rule); err != nil {
writeRuleError(err.Error())
}
writeRuleSuccess(rule.ID, provider.GetOverridePath())
}
func runWindowrulesRemove(cmd *cobra.Command, args []string) {
compositor := strings.ToLower(args[0])
ruleID := args[1]
provider := getWindowRulesProvider(compositor)
if provider == nil {
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
}
if err := provider.RemoveRule(ruleID); err != nil {
writeRuleError(err.Error())
}
writeRuleSuccess(ruleID, provider.GetOverridePath())
}
func runWindowrulesReorder(cmd *cobra.Command, args []string) {
compositor := strings.ToLower(args[0])
idsJSON := args[1]
var ids []string
if err := json.Unmarshal([]byte(idsJSON), &ids); err != nil {
writeRuleError(fmt.Sprintf("Invalid JSON array: %v", err))
}
provider := getWindowRulesProvider(compositor)
if provider == nil {
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
}
if err := provider.ReorderRules(ids); err != nil {
writeRuleError(err.Error())
}
writeRuleSuccess("", provider.GetOverridePath())
}
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
switch compositor {
case "niri":
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
return nil
}
return providers.NewNiriWritableProvider(configDir)
case "hyprland":
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
return nil
}
return providers.NewHyprlandWritableProvider(configDir)
default:
return nil
}
}
func generateRuleID() string {
return fmt.Sprintf("wr_%d", time.Now().UnixNano())
}

View File

@@ -87,20 +87,14 @@ func newDPMSClient() (*dpmsClient, error) {
switch e.Interface {
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
version := e.Version
if version > 1 {
version = 1
}
version := min(e.Version, 1)
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
c.powerMgr = powerMgr
}
case "wl_output":
output := wlclient.NewOutput(c.ctx)
version := e.Version
if version > 4 {
version = 4
}
version := min(e.Version, 4)
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := fmt.Sprintf("output-%d", output.ID())
state := &outputState{

View File

@@ -0,0 +1,285 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}
// preRunPrivileged combines the immutable-system check with a privesc tool
// selection prompt (shown only when multiple tools are available and the
// $DMS_PRIVESC env var isn't set).
func preRunPrivileged(cmd *cobra.Command, args []string) error {
if err := requireMutableSystemCommand(cmd, args); err != nil {
return err
}
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
return err
}
return nil
}

View File

@@ -5,6 +5,7 @@ package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
@@ -16,16 +17,10 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to update
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd)
@@ -34,7 +29,9 @@ func init() {
}
func main() {
if os.Geteuid() == 0 {
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
log.Fatal("This program should not be run as root. Exiting.")
}

View File

@@ -5,33 +5,30 @@ package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
var Version = "dev"
func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to plugins
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate())
}
func main() {
// Block root
if os.Geteuid() == 0 {
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
log.Fatal("This program should not be run as root. Exiting.")
}

View File

@@ -0,0 +1,172 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type randrOutput struct {
Name string `json:"name"`
Scale float64 `json:"scale"`
Width int32 `json:"width"`
Height int32 `json:"height"`
Refresh int32 `json:"refresh"`
Enabled bool `json:"enabled"`
}
type randrHead struct {
name string
enabled bool
scale float64
currentModeID uint32
modeIDs []uint32
}
type randrMode struct {
width int32
height int32
refresh int32
}
type randrClient struct {
display *wlclient.Display
ctx *wlclient.Context
manager *wlr_output_management.ZwlrOutputManagerV1
heads map[uint32]*randrHead
modes map[uint32]*randrMode
done bool
err error
}
func queryRandr() ([]randrOutput, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
}
c := &randrClient{
display: display,
ctx: display.Context(),
heads: make(map[uint32]*randrHead),
modes: make(map[uint32]*randrMode),
}
defer c.ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, fmt.Errorf("failed to get registry: %w", err)
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
version := min(e.Version, 4)
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
c.handleHead(e)
})
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
c.done = true
})
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
c.manager = mgr
}
}
})
// First roundtrip: discover globals and bind manager
syncCallback, err := display.Sync()
if err != nil {
return nil, fmt.Errorf("failed to sync display: %w", err)
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
if c.manager == nil {
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
c.done = true
}
// Otherwise wait for manager's DoneHandler
})
for !c.done {
if err := c.ctx.Dispatch(); err != nil {
return nil, fmt.Errorf("dispatch error: %w", err)
}
}
if c.err != nil {
return nil, c.err
}
return c.buildOutputs(), nil
}
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
handle := e.Head
headID := handle.ID()
head := &randrHead{
modeIDs: make([]uint32, 0),
}
c.heads[headID] = head
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
head.name = e.Name
})
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
head.enabled = e.Enabled != 0
})
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
head.scale = e.Scale
})
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
head.currentModeID = e.Mode.ID()
})
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
modeHandle := e.Mode
modeID := modeHandle.ID()
head.modeIDs = append(head.modeIDs, modeID)
mode := &randrMode{}
c.modes[modeID] = mode
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
mode.width = e.Width
mode.height = e.Height
})
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
mode.refresh = e.Refresh
})
})
}
func (c *randrClient) buildOutputs() []randrOutput {
outputs := make([]randrOutput, 0, len(c.heads))
for _, head := range c.heads {
out := randrOutput{
Name: head.name,
Scale: head.scale,
Enabled: head.enabled,
}
if mode, ok := c.modes[head.currentModeID]; ok {
out.Width = mode.width
out.Height = mode.height
out.Refresh = mode.refresh
}
outputs = append(outputs, out)
}
return outputs
}

View File

@@ -0,0 +1,114 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func sendServerRequest(req models.Request) (*models.Response[any], error) {
socketPath := getServerSocketPath()
conn, err := net.Dial("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
}
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := conn.Write(reqData); err != nil {
return nil, fmt.Errorf("failed to write request: %w", err)
}
if _, err := conn.Write([]byte("\n")); err != nil {
return nil, fmt.Errorf("failed to write newline: %w", err)
}
if !scanner.Scan() {
return nil, fmt.Errorf("failed to read response")
}
var resp models.Response[any]
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &resp, nil
}
// sendServerRequestFireAndForget sends a request without waiting for a response.
// Useful for commands that trigger UI or async operations.
func sendServerRequestFireAndForget(req models.Request) error {
socketPath := getServerSocketPath()
conn, err := net.Dial("unix", socketPath)
if err != nil {
return fmt.Errorf("failed to connect to server (is it running?): %w", err)
}
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := conn.Write(reqData); err != nil {
return fmt.Errorf("failed to write request: %w", err)
}
if _, err := conn.Write([]byte("\n")); err != nil {
return fmt.Errorf("failed to write newline: %w", err)
}
return nil
}
// tryServerRequest attempts to send a request but returns false if server unavailable.
// Does not log errors - caller can decide what to do on failure.
func tryServerRequest(req models.Request) (*models.Response[any], bool) {
resp, err := sendServerRequest(req)
if err != nil {
return nil, false
}
return resp, true
}
func getServerSocketPath() string {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
runtimeDir = os.TempDir()
}
entries, err := os.ReadDir(runtimeDir)
if err != nil {
return filepath.Join(runtimeDir, "danklinux.sock")
}
for _, entry := range entries {
name := entry.Name()
if name == "danklinux.sock" {
return filepath.Join(runtimeDir, name)
}
if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" {
return filepath.Join(runtimeDir, name)
}
}
return server.GetSocketPath()
}

View File

@@ -7,8 +7,10 @@ import (
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -18,6 +20,25 @@ import (
type ipcTargets map[string]map[string][]string
// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
if state == nil {
return 1
}
if code := state.ExitCode(); code != -1 {
return code
}
// Process was killed by signal - extract signal number
if status, ok := state.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
return 128 + int(status.Signal())
}
}
return 1
}
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
@@ -155,6 +176,7 @@ func runShellInteractive(session bool) {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
@@ -164,8 +186,10 @@ func runShellInteractive(session bool) {
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
@@ -179,6 +203,16 @@ func runShellInteractive(session bool) {
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -213,14 +247,28 @@ func runShellInteractive(session bool) {
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
log.Infof("\nReceived signal %v, shutting down...", sig)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
@@ -234,7 +282,7 @@ func runShellInteractive(session bool) {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(1)
os.Exit(getProcessExitCode(cmd.ProcessState))
}
}
}
@@ -327,13 +375,7 @@ func killShell() {
func runShellDaemon(session bool) {
isSessionManaged = session
isDaemonChild := false
for _, arg := range os.Args {
if arg == "--daemon-child" {
isDaemonChild = true
break
}
}
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
if !isDaemonChild {
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
@@ -384,8 +426,10 @@ func runShellDaemon(session bool) {
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
@@ -399,6 +443,16 @@ func runShellDaemon(session bool) {
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)
@@ -439,15 +493,28 @@ func runShellDaemon(session bool) {
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return
}
// All other signals: clean shutdown
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
@@ -459,17 +526,25 @@ func runShellDaemon(session bool) {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(1)
os.Exit(getProcessExitCode(cmd.ProcessState))
}
}
}
var qsHasAnyDisplay = sync.OnceValue(func() bool {
out, err := exec.Command("qs", "ipc", "--help").Output()
if err != nil {
return false
}
return strings.Contains(string(out), "--any-display")
})
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
targets := make(ipcTargets)
var currentTarget string
for _, line := range strings.Split(output, "\n") {
if strings.HasPrefix(line, "target ") {
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
for line := range strings.SplitSeq(output, "\n") {
if after, ok := strings.CutPrefix(line, "target "); ok {
currentTarget = strings.TrimSpace(after)
targets[currentTarget] = make(map[string][]string)
}
if strings.HasPrefix(line, " function") && currentTarget != "" {
@@ -494,7 +569,11 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
}
func getShellIPCCompletions(args []string, _ string) []string {
cmdArgs := []string{"-p", configPath, "ipc", "show"}
cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets
@@ -537,18 +616,70 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil
}
func getFirstDMSPID() (int, bool) {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
if proc.Signal(syscall.Signal(0)) != nil {
continue
}
return pid, true
}
return 0, false
}
func runShellIPCCommand(args []string) {
if len(args) == 0 {
log.Error("IPC command requires arguments")
log.Info("Usage: dms ipc <command> [args...]")
os.Exit(1)
printIPCHelp()
return
}
if args[0] != "call" {
args = append([]string{"call"}, args...)
}
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
cmdArgs := []string{"ipc"}
switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err)
}
// ! TODO - remove check when QS 0.3 is released
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
}
cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@@ -558,3 +689,45 @@ func runShellIPCCommand(args []string) {
log.Fatalf("Error running IPC command: %v", err)
}
}
func printIPCHelp() {
fmt.Println("Usage: dms ipc <target> <function> [args...]")
fmt.Println()
cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmd := exec.Command("qs", cmdArgs...)
output, err := cmd.Output()
if err != nil {
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
return
}
targets := parseTargetsFromIPCShowOutput(string(output))
if len(targets) == 0 {
fmt.Println("No IPC targets available")
return
}
fmt.Println("Targets:")
targetNames := make([]string, 0, len(targets))
for name := range targets {
targetNames = append(targetNames, name)
}
slices.Sort(targetNames)
for _, targetName := range targetNames {
funcs := targets[targetName]
funcNames := make([]string, 0, len(funcs))
for fn := range funcs {
funcNames = append(funcNames, fn)
}
slices.Sort(funcNames)
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
}
}

View File

@@ -17,8 +17,8 @@ func getThemedASCII() string {
logo := `
██████╗ █████╗ ███╗ ██╗██╗ ██╗
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
██║ ██║███████║██╔██╗ ██║█████╔╝
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
██║ ██║███████║██╔██╗ ██║█████╔╝
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
██████╔╝██║ ██║██║ ╚████║██║ ██╗
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`

View File

@@ -3,15 +3,24 @@ package main
import (
"fmt"
"os/exec"
"slices"
"strings"
)
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
// isReadOnlyCommand returns true if the CLI args indicate a command that is
// safe to run as root (e.g. shell completion, help).
func isReadOnlyCommand(args []string) bool {
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "-") {
continue
}
switch arg {
case "completion", "help", "__complete":
return true
}
return false
}
return path, nil
return false
}
func isArchPackageInstalled(packageName string) bool {
@@ -36,13 +45,7 @@ func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
if err != nil {
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
isKnownState := false
for _, known := range knownStates {
if stateStr == known {
isKnownState = true
break
}
}
isKnownState := slices.Contains(knownStates, stateStr)
if !isKnownState {
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)

View File

@@ -1,56 +1,61 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.24.6
go 1.25.0
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/charmbracelet/bubbles v0.21.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.2.0
github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/pilebones/go-udev v0.9.1
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
github.com/spf13/cobra v1.10.1
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
golang.org/x/image v0.36.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -64,7 +69,11 @@ require (
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1
)
// v0.0.1 tag is missing a LICENSE file; master has it.
// See: https://github.com/mattn/go-localereader/issues/2
replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75

View File

@@ -4,6 +4,14 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -12,40 +20,40 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -56,28 +64,30 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -91,8 +101,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -112,14 +122,14 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -127,29 +137,43 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
set -e
@@ -9,8 +9,8 @@ NC='\033[0m' # No Color
# Check for root privileges
if [ "$(id -u)" == "0" ]; then
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
exit 1
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
exit 1
fi
# Check if running on Linux
@@ -22,17 +22,17 @@ fi
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
*)
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
exit 1
;;
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
*)
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
exit 1
;;
esac
# Get the latest release version
@@ -55,7 +55,7 @@ curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LAT
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
# Get the expected checksum
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256)
# Calculate actual checksum
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
@@ -67,7 +67,7 @@ if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
printf "The downloaded file may be corrupted or tampered with\n"
cd - > /dev/null
cd - >/dev/null
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -82,5 +82,5 @@ printf "%bRunning installer...%b\n" "$GREEN" "$NC"
./installer
# Cleanup
cd - > /dev/null
rm -rf "$TEMP_DIR"
cd - >/dev/null
rm -rf "$TEMP_DIR"

View File

@@ -0,0 +1,35 @@
package blur
import (
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
func ProbeSupport() (bool, error) {
display, err := client.Connect("")
if err != nil {
return false, err
}
defer display.Context().Close()
registry, err := display.GetRegistry()
if err != nil {
return false, err
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case extBackgroundEffectInterface:
found = true
}
})
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
return false, err
}
return found, nil
}

View File

@@ -0,0 +1,630 @@
package clipboard
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const envServe = "_DMS_CLIPBOARD_SERVE"
const envMime = "_DMS_CLIPBOARD_MIME"
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
// child. Reads source data into memory, deletes any cache file, then serves.
func MaybeServeAndExit() {
if os.Getenv(envServe) == "" {
return
}
mimeType := os.Getenv(envMime)
pasteOnce := os.Getenv(envPasteOnce) == "1"
cachePath := os.Getenv(envCacheFile)
var data []byte
var err error
switch {
case cachePath != "":
data, err = os.ReadFile(cachePath)
os.Remove(cachePath)
default:
data, err = io.ReadAll(os.Stdin)
}
if err != nil {
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
os.Exit(1)
}
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func Copy(data []byte, mimeType string) error {
return copyForkCached(data, mimeType, false)
}
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return serveClipboard(data, mimeType, pasteOnce)
}
return copyForkCached(data, mimeType, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if foreground {
buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
}
return copyFork(data, mimeType, pasteOnce)
}
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
cmd := exec.Command(os.Args[0])
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
cmd.Env = append(os.Environ(),
envServe+"=1",
envMime+"="+mimeType,
)
if pasteOnce {
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
}
cmd.Env = append(cmd.Env, extra...)
return cmd
}
func waitReady(cmd *exec.Cmd) error {
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
cacheFile, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create cache file: %w", err)
}
cachePath := cacheFile.Name()
if _, err := cacheFile.Write(data); err != nil {
cacheFile.Close()
os.Remove(cachePath)
return fmt.Errorf("write cache file: %w", err)
}
if err := cacheFile.Close(); err != nil {
os.Remove(cachePath)
return fmt.Errorf("close cache file: %w", err)
}
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
cmd.Stdin = nil
if err := waitReady(cmd); err != nil {
os.Remove(cachePath)
return err
}
return nil
}
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
cmd := newForkCmd(mimeType, pasteOnce)
switch src := data.(type) {
case *os.File:
cmd.Stdin = src
return waitReady(cmd)
default:
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := io.Copy(stdin, data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
}
func signalReady() {
if os.Getenv(envServe) == "" {
return
}
os.Stdout.Write([]byte{1})
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
source, err := dataControlMgr.CreateDataSource()
if err != nil {
return fmt.Errorf("create data source: %w", err)
}
if err := source.Offer(mimeType); err != nil {
return fmt.Errorf("offer mime type: %w", err)
}
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
if err := source.Offer("text/plain"); err != nil {
return fmt.Errorf("offer text/plain: %w", err)
}
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
}
if err := source.Offer("UTF8_STRING"); err != nil {
return fmt.Errorf("offer UTF8_STRING: %w", err)
}
if err := source.Offer("STRING"); err != nil {
return fmt.Errorf("offer STRING: %w", err)
}
if err := source.Offer("TEXT"); err != nil {
return fmt.Errorf("offer TEXT: %w", err)
}
}
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
_, _ = file.Write(data)
select {
case pasted <- struct{}{}:
default:
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
close(cancelled)
})
if err := device.SetSelection(source); err != nil {
return fmt.Errorf("set selection: %w", err)
}
display.Roundtrip()
signalReady()
for {
select {
case <-cancelled:
return nil
case <-pasted:
if pasteOnce {
return nil
}
default:
if err := ctx.Dispatch(); err != nil {
return nil
}
}
}
}
func CopyText(text string) error {
return Copy([]byte(text), "text/plain;charset=utf-8")
}
func Paste() ([]byte, string, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, "", fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return nil, "", fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return nil, "", fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return nil, "", fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
if e.Id == nil {
return
}
offerMimeTypes[e.Id] = nil
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
})
})
var selectionOffer *ext_data_control.ExtDataControlOfferV1
gotSelection := false
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
selectionOffer = e.Id
gotSelection = true
})
display.Roundtrip()
display.Roundtrip()
if !gotSelection || selectionOffer == nil {
return nil, "", fmt.Errorf("no clipboard data")
}
mimeTypes := offerMimeTypes[selectionOffer]
selectedMime := selectPreferredMimeType(mimeTypes)
if selectedMime == "" {
return nil, "", fmt.Errorf("no supported mime type")
}
r, w, err := os.Pipe()
if err != nil {
return nil, "", fmt.Errorf("create pipe: %w", err)
}
defer r.Close()
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
return nil, "", fmt.Errorf("receive: %w", err)
}
w.Close()
display.Roundtrip()
data, err := io.ReadAll(r)
if err != nil {
return nil, "", fmt.Errorf("read: %w", err)
}
return data, selectedMime, nil
}
func PasteText() (string, error) {
data, _, err := Paste()
if err != nil {
return "", err
}
return string(data), nil
}
func selectPreferredMimeType(mimes []string) string {
preferred := []string{
"text/plain;charset=utf-8",
"text/plain",
"UTF8_STRING",
"STRING",
"TEXT",
"image/png",
"image/jpeg",
}
for _, pref := range preferred {
for _, mime := range mimes {
if mime == pref {
return mime
}
}
}
if len(mimes) > 0 {
return mimes[0]
}
return ""
}
func IsImageMimeType(mime string) bool {
return len(mime) > 6 && mime[:6] == "image/"
}
type Offer struct {
MimeType string
Data []byte
}
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
if !foreground {
return copyMultiFork(offers, pasteOnce)
}
return copyMultiServe(offers, pasteOnce)
}
func copyMultiFork(offers []Offer, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
if pasteOnce {
args = append(args, "--paste-once")
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
for _, offer := range offers {
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
if _, err := stdin.Write(offer.Data); err != nil {
stdin.Close()
return fmt.Errorf("write offer data: %w", err)
}
}
stdin.Close()
return nil
}
func copyMultiServe(offers []Offer, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
source, err := dataControlMgr.CreateDataSource()
if err != nil {
return fmt.Errorf("create data source: %w", err)
}
offerMap := make(map[string][]byte)
for _, offer := range offers {
if err := source.Offer(offer.MimeType); err != nil {
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
}
offerMap[offer.MimeType] = offer.Data
}
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if data, ok := offerMap[e.MimeType]; ok {
_, _ = file.Write(data)
}
select {
case pasted <- struct{}{}:
default:
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
close(cancelled)
})
if err := device.SetSelection(source); err != nil {
return fmt.Errorf("set selection: %w", err)
}
display.Roundtrip()
for {
select {
case <-cancelled:
return nil
case <-pasted:
if pasteOnce {
return nil
}
default:
if err := ctx.Dispatch(); err != nil {
return nil
}
}
}
}

View File

@@ -0,0 +1,248 @@
package clipboard
import (
"bytes"
"encoding/binary"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"path/filepath"
"strings"
"time"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"hash/fnv"
bolt "go.etcd.io/bbolt"
)
type StoreConfig struct {
MaxHistory int
MaxEntrySize int64
}
func DefaultStoreConfig() StoreConfig {
return StoreConfig{
MaxHistory: 100,
MaxEntrySize: 5 * 1024 * 1024,
}
}
type Entry struct {
ID uint64
Data []byte
MimeType string
Preview string
Size int
Timestamp time.Time
IsImage bool
Hash uint64
}
func Store(data []byte, mimeType string) error {
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
}
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
if len(data) == 0 {
return nil
}
if int64(len(data)) > cfg.MaxEntrySize {
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
}
dbPath, err := GetDBPath()
if err != nil {
return fmt.Errorf("get db path: %w", err)
}
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
entry := Entry{
Data: data,
MimeType: mimeType,
Size: len(data),
Timestamp: time.Now(),
IsImage: IsImageMimeType(mimeType),
Hash: computeHash(data),
}
switch {
case entry.IsImage:
entry.Preview = imagePreview(data, mimeType)
default:
entry.Preview = textPreview(data)
}
return db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
if err != nil {
return err
}
if err := deduplicateInTx(b, entry.Hash); err != nil {
return err
}
id, err := b.NextSequence()
if err != nil {
return err
}
entry.ID = id
encoded, err := encodeEntry(entry)
if err != nil {
return err
}
if err := b.Put(itob(id), encoded); err != nil {
return err
}
return trimLengthInTx(b, cfg.MaxHistory)
})
}
func GetDBPath() (string, error) {
cacheDir, err := os.UserCacheDir()
if err != nil {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
cacheDir = filepath.Join(homeDir, ".cache")
}
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
newPath := filepath.Join(newDir, "db")
if _, err := os.Stat(newPath); err == nil {
return newPath, nil
}
oldDir := filepath.Join(cacheDir, "dms-clipboard")
oldPath := filepath.Join(oldDir, "db")
if _, err := os.Stat(oldPath); err == nil {
if err := os.MkdirAll(newDir, 0o700); err != nil {
return "", err
}
if err := os.Rename(oldPath, newPath); err != nil {
return "", err
}
os.Remove(oldDir)
return newPath, nil
}
if err := os.MkdirAll(newDir, 0o700); err != nil {
return "", err
}
return newPath, nil
}
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
if extractHash(v) != hash {
continue
}
if err := b.Delete(k); err != nil {
return err
}
}
return nil
}
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
c := b.Cursor()
var count int
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
if count < maxHistory {
count++
continue
}
if err := b.Delete(k); err != nil {
return err
}
}
return nil
}
func encodeEntry(e Entry) ([]byte, error) {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, e.ID)
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
buf.Write(e.Data)
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
buf.WriteString(e.MimeType)
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
buf.WriteString(e.Preview)
binary.Write(buf, binary.BigEndian, int32(e.Size))
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
if e.IsImage {
buf.WriteByte(1)
} else {
buf.WriteByte(0)
}
binary.Write(buf, binary.BigEndian, e.Hash)
return buf.Bytes(), nil
}
func itob(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
return b
}
func computeHash(data []byte) uint64 {
h := fnv.New64a()
h.Write(data)
return h.Sum64()
}
func extractHash(data []byte) uint64 {
if len(data) < 8 {
return 0
}
return binary.BigEndian.Uint64(data[len(data)-8:])
}
func textPreview(data []byte) string {
text := string(data)
text = strings.TrimSpace(text)
text = strings.Join(strings.Fields(text), " ")
if len(text) > 100 {
return text[:100] + "…"
}
return text
}
func imagePreview(data []byte, format string) string {
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
}
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
}
func sizeStr(size int) string {
units := []string{"B", "KiB", "MiB"}
var i int
fsize := float64(size)
for fsize >= 1024 && i < len(units)-1 {
fsize /= 1024
i++
}
return fmt.Sprintf("%.0f %s", fsize, units[i])
}

View File

@@ -0,0 +1,303 @@
package clipboard
import (
"context"
"errors"
"fmt"
"io"
"os"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type ClipboardChange struct {
Data []byte
MimeType string
MimeTypes []string
}
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
wlCtx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
if e.Id == nil {
return
}
offerMimeTypes[e.Id] = nil
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
})
})
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
if e.Id == nil {
return
}
mimes := offerMimeTypes[e.Id]
selectedMime := selectPreferredMimeType(mimes)
if selectedMime == "" {
return
}
r, w, err := os.Pipe()
if err != nil {
return
}
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
r.Close()
return
}
w.Close()
go func() {
defer r.Close()
data, err := io.ReadAll(r)
if err != nil || len(data) == 0 {
return
}
callback(data, selectedMime)
}()
})
display.Roundtrip()
display.Roundtrip()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
return fmt.Errorf("set read deadline: %w", err)
}
if err := wlCtx.Dispatch(); err != nil {
if isTimeoutError(err) {
continue
}
return fmt.Errorf("dispatch: %w", err)
}
}
}
}
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
wlCtx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
if e.Id == nil {
return
}
offerMimeTypes[e.Id] = nil
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
})
})
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
if e.Id == nil {
return
}
mimes := offerMimeTypes[e.Id]
selectedMime := selectPreferredMimeType(mimes)
if selectedMime == "" {
return
}
mimesCopy := make([]string, len(mimes))
copy(mimesCopy, mimes)
r, w, err := os.Pipe()
if err != nil {
return
}
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
r.Close()
return
}
w.Close()
go func() {
defer r.Close()
data, err := io.ReadAll(r)
if err != nil || len(data) == 0 {
return
}
callback(data, selectedMime, mimesCopy)
}()
})
display.Roundtrip()
display.Roundtrip()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
return fmt.Errorf("set read deadline: %w", err)
}
if err := wlCtx.Dispatch(); err != nil {
if isTimeoutError(err) {
continue
}
return fmt.Errorf("dispatch: %w", err)
}
}
}
}
func isTimeoutError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, os.ErrDeadlineExceeded) {
return true
}
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
return true
}
return false
}
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
ch := make(chan ClipboardChange, 16)
errCh := make(chan error, 1)
go func() {
defer close(ch)
err := Watch(ctx, func(data []byte, mimeType string) {
select {
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
default:
}
})
if err != nil && err != context.Canceled {
errCh <- err
}
close(errCh)
}()
time.Sleep(50 * time.Millisecond)
return ch, errCh
}

View File

@@ -221,10 +221,7 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
case client.OutputInterfaceName:
output := client.NewOutput(p.ctx)
version := e.Version
if version > 4 {
version = 4
}
version := min(e.Version, 4)
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
p.outputsMu.Lock()
p.outputs[e.Name] = &Output{
@@ -239,20 +236,14 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
version := e.Version
if version > 4 {
version = 4
}
version := min(e.Version, 4)
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
p.layerShell = layerShell
}
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
version := e.Version
if version > 3 {
version = 3
}
version := min(e.Version, 3)
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
p.screencopy = screencopy
}

View File

@@ -1157,7 +1157,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
rOff, bOff = 2, 0
}
for row := 0; row < fontH; row++ {
for row := range fontH {
yy := y + row
if yy < 0 || yy >= height {
continue
@@ -1165,7 +1165,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
rowPattern := g[row]
dstRowOff := yy * stride
for colIdx := 0; colIdx < fontW; colIdx++ {
for colIdx := range fontW {
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
continue
}

View File

@@ -0,0 +1,314 @@
package colorpicker
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 50
const iterations = 100
for i := range goroutines {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range iterations {
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
}
}(i)
}
wg.Wait()
}
func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 30
const iterations = 100
for i := range goroutines / 2 {
wg.Add(1)
go func(id int) {
defer wg.Done()
for range iterations {
s.SetScale(int32(id%3 + 1))
}
}(i)
}
for range goroutines / 2 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
scale := s.Scale()
assert.GreaterOrEqual(t, scale, int32(1))
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 20
const iterations = 100
for i := range goroutines / 2 {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range iterations {
_ = s.OnLayerConfigure(1920+id, 1080+j)
}
}(i)
}
for range goroutines / 2 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
w, h := s.LogicalSize()
_ = w
_ = h
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 30
const iterations = 100
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
s.OnPointerButton(0x110, 1)
}
}()
}
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
s.OnKey(1, 1)
}
}()
}
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
picked, cancelled := s.IsDone()
_ = picked
_ = cancelled
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 20
const iterations = 100
for range goroutines {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
_ = s.IsReady()
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 20
const iterations = 100
for range goroutines {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
s.SwapBuffers()
}
}()
}
wg.Wait()
}
func TestSurfaceState_ZeroScale(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
s.SetScale(0)
assert.Equal(t, int32(1), s.Scale())
}
func TestSurfaceState_NegativeScale(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
s.SetScale(-5)
assert.Equal(t, int32(1), s.Scale())
}
func TestSurfaceState_ZeroDimensionConfigure(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
err := s.OnLayerConfigure(0, 100)
assert.NoError(t, err)
err = s.OnLayerConfigure(100, 0)
assert.NoError(t, err)
err = s.OnLayerConfigure(-1, 100)
assert.NoError(t, err)
w, h := s.LogicalSize()
assert.Equal(t, 0, w)
assert.Equal(t, 0, h)
}
func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
color, ok := s.PickColor()
assert.False(t, ok)
assert.Equal(t, Color{}, color)
}
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.Redraw()
assert.Nil(t, buf)
}
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.RedrawScreenOnly()
assert.Nil(t, buf)
}
func TestSurfaceState_FrontRenderBufferNil(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.FrontRenderBuffer()
assert.Nil(t, buf)
}
func TestSurfaceState_ScreenBufferNil(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.ScreenBuffer()
assert.Nil(t, buf)
}
func TestSurfaceState_DestroyMultipleTimes(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
s.Destroy()
s.Destroy()
}
func TestClamp(t *testing.T) {
tests := []struct {
v, lo, hi, expected int
}{
{5, 0, 10, 5},
{-5, 0, 10, 0},
{15, 0, 10, 10},
{0, 0, 10, 0},
{10, 0, 10, 10},
}
for _, tt := range tests {
result := clamp(tt.v, tt.lo, tt.hi)
assert.Equal(t, tt.expected, result)
}
}
func TestClampF(t *testing.T) {
tests := []struct {
v, lo, hi, expected float64
}{
{5.0, 0.0, 10.0, 5.0},
{-5.0, 0.0, 10.0, 0.0},
{15.0, 0.0, 10.0, 10.0},
{0.0, 0.0, 10.0, 0.0},
{10.0, 0.0, 10.0, 10.0},
}
for _, tt := range tests {
result := clampF(tt.v, tt.lo, tt.hi)
assert.InDelta(t, tt.expected, result, 0.001)
}
}
func TestAbs(t *testing.T) {
tests := []struct {
v, expected int
}{
{5, 5},
{-5, 5},
{0, 0},
}
for _, tt := range tests {
result := abs(tt.v)
assert.Equal(t, tt.expected, result)
}
}
func TestBlendColors(t *testing.T) {
bg := Color{R: 0, G: 0, B: 0, A: 255}
fg := Color{R: 255, G: 255, B: 255, A: 255}
result := blendColors(bg, fg, 0.0)
assert.Equal(t, bg.R, result.R)
assert.Equal(t, bg.G, result.G)
assert.Equal(t, bg.B, result.B)
result = blendColors(bg, fg, 1.0)
assert.Equal(t, fg.R, result.R)
assert.Equal(t, fg.G, result.G)
assert.Equal(t, fg.B, result.B)
result = blendColors(bg, fg, 0.5)
assert.InDelta(t, 127, int(result.R), 1)
assert.InDelta(t, 127, int(result.G), 1)
assert.InDelta(t, 127, int(result.B), 1)
result = blendColors(bg, fg, -1.0)
assert.Equal(t, bg.R, result.R)
result = blendColors(bg, fg, 2.0)
assert.Equal(t, fg.R, result.R)
}

View File

@@ -126,13 +126,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
}
configDir := filepath.Dir(result.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
result.Error = fmt.Errorf("failed to create config directory: %w", err)
return result, result.Error
}
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
@@ -150,7 +150,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
timestamp := time.Now().Format("2006-01-02_15-04-05")
result.BackupPath = result.Path + ".backup." + timestamp
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
result.Error = fmt.Errorf("failed to create backup: %w", err)
return result, result.Error
}
@@ -176,7 +176,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
}
if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig, dmsDir)
if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
} else {
@@ -185,7 +185,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
}
}
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
result.Error = fmt.Errorf("failed to write config: %w", err)
return result, result.Error
}
@@ -209,11 +209,19 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
{"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.kdl", ""},
{"cursor.kdl", ""},
{"windowrules.kdl", ""},
}
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
// Skip if file already exists and is not empty to preserve user modifications
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
@@ -231,7 +239,7 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
}
configDir := filepath.Dir(mainResult.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
@@ -247,14 +255,14 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
timestamp := time.Now().Format("2006-01-02_15-04-05")
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
}
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0o644); err != nil {
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
@@ -265,10 +273,16 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
colorResult := DeploymentResult{
ConfigType: "Ghostty Colors",
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "themes", "dankcolors"),
}
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
themesDir := filepath.Dir(colorResult.Path)
if err := os.MkdirAll(themesDir, 0o755); err != nil {
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0o644); err != nil {
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
return results, colorResult.Error
}
@@ -289,7 +303,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
}
configDir := filepath.Dir(mainResult.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
@@ -305,14 +319,14 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
timestamp := time.Now().Format("2006-01-02_15-04-05")
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
}
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0o644); err != nil {
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
@@ -326,7 +340,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
}
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0o644); err != nil {
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
return results, themeResult.Error
}
@@ -340,7 +354,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
}
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0o644); err != nil {
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
return results, tabsResult.Error
}
@@ -361,7 +375,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
}
configDir := filepath.Dir(mainResult.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
@@ -377,14 +391,14 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
timestamp := time.Now().Format("2006-01-02_15-04-05")
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
}
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0o644); err != nil {
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
@@ -398,7 +412,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
}
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0o644); err != nil {
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
return results, themeResult.Error
}
@@ -410,24 +424,31 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones)
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
// Find all output sections in the existing config
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
if len(existingOutputs) == 0 {
// No output sections to merge
return newConfig, nil
}
// Remove the example output section from the new config
outputsPath := filepath.Join(dmsDir, "outputs.kdl")
if _, err := os.Stat(outputsPath); err != nil {
var outputsContent strings.Builder
for _, output := range existingOutputs {
outputsContent.WriteString(output)
outputsContent.WriteString("\n\n")
}
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
} else {
cd.log("Migrated output sections to dms/outputs.kdl")
}
}
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
// Find where to insert the output sections (after the input section)
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
@@ -435,7 +456,6 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
return "", fmt.Errorf("could not find insertion point for output sections")
}
// Insert after the first closing brace (end of input section)
insertPos := inputMatches[0][1]
var builder strings.Builder
@@ -460,11 +480,17 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
}
configDir := filepath.Dir(result.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
result.Error = fmt.Errorf("failed to create config directory: %w", err)
return result, result.Error
}
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
var existingConfig string
if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Hyprland configuration")
@@ -478,7 +504,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
timestamp := time.Now().Format("2006-01-02_15-04-05")
result.BackupPath = result.Path + ".backup." + timestamp
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
result.Error = fmt.Errorf("failed to create backup: %w", err)
return result, result.Error
}
@@ -504,7 +530,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
}
if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir)
if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
} else {
@@ -513,18 +539,51 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
}
}
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
result.Error = fmt.Errorf("failed to write config: %w", err)
return result, result.Error
}
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error
}
result.Deployed = true
cd.log("Successfully deployed Hyprland configuration")
return result, nil
}
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
configs := []struct {
name string
content string
}{
{"colors.conf", HyprColorsConfig},
{"layout.conf", HyprLayoutConfig},
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.conf", ""},
{"cursor.conf", ""},
{"windowrules.conf", ""},
}
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
// Skip if file already exists and is not empty to preserve user modifications
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
}
return nil
}
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
@@ -532,6 +591,20 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return newConfig, nil
}
outputsPath := filepath.Join(dmsDir, "outputs.conf")
if _, err := os.Stat(outputsPath); err != nil {
var outputsContent strings.Builder
for _, monitor := range existingMonitors {
outputsContent.WriteString(monitor)
outputsContent.WriteString("\n")
}
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
} else {
cd.log("Migrated monitor sections to dms/outputs.conf")
}
}
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
@@ -571,7 +644,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
@@ -586,7 +659,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = QT_QPA_PLATFORM,wayland;xcb",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
@@ -604,7 +677,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
QT_QPA_PLATFORM "wayland;xcb"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
@@ -615,10 +688,11 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
config = strings.Replace(config,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
1)
// Insert spawn-at-startup for dms after the environment block
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
}
}
return config

View File

@@ -161,7 +161,8 @@ layout {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
tmpDir := t.TempDir()
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig, tmpDir)
if tt.wantError {
assert.Error(t, err)
@@ -219,9 +220,9 @@ func TestConfigDeploymentFlow(t *testing.T) {
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
existingContent := "# Old config\nfont-size = 14\n"
ghosttyPath := getGhosttyPath()
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0o644)
require.NoError(t, err)
results, err := cd.deployGhosttyConfig()
@@ -362,7 +363,8 @@ input {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
tmpDir := t.TempDir()
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
if tt.wantError {
assert.Error(t, err)
@@ -406,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
assert.Contains(t, string(content), "source = ./dms/binds.conf")
assert.Contains(t, string(content), "exec-once = ")
})
@@ -420,9 +422,9 @@ general {
}
`
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
@@ -442,7 +444,7 @@ general {
require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
assert.NotContains(t, string(newContent), "monitor = eDP-2")
})
}
@@ -459,16 +461,13 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
}
func TestGhosttyConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyConfig, "window-decoration = false")
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
assert.Contains(t, GhosttyConfig, "theme = dankcolors")
}
func TestGhosttyColorConfigStructure(t *testing.T) {
@@ -601,9 +600,9 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0o644)
require.NoError(t, err)
results, err := cd.deployAlacrittyConfig()

View File

@@ -5,15 +5,13 @@ import (
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func LocateDMSConfig() (string, error) {
var primaryPaths []string
configHome := utils.XDGConfigHome()
if configHome != "" {
configHome, err := os.UserConfigDir()
if err == nil && configHome != "" {
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
}
@@ -23,7 +21,7 @@ func LocateDMSConfig() (string, error) {
dataDirs = "/usr/local/share:/usr/share"
}
for _, dir := range strings.Split(dataDirs, ":") {
for dir := range strings.SplitSeq(dataDirs, ":") {
if dir != "" {
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
}
@@ -35,7 +33,7 @@ func LocateDMSConfig() (string, error) {
configDirs = "/etc/xdg"
}
for _, dir := range strings.Split(configDirs, ":") {
for dir := range strings.SplitSeq(configDirs, ":") {
if dir != "" {
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
}

View File

@@ -48,4 +48,4 @@ keybind = shift+enter=text:\n
gtk-single-instance = true
# Dank color generation
config-file = ./config-dankcolors
theme = dankcolors

View File

@@ -0,0 +1,162 @@
# === Application Launchers ===
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
bind = SUPER, space, exec, dms ipc call spotlight toggle
bind = SUPER, V, exec, dms ipc call clipboard toggle
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
bind = SUPER, N, exec, dms ipc call notifications toggle
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
bind = SUPER, X, exec, dms ipc call powermenu toggle
# === Cheat sheet
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = SUPER ALT, L, exec, dms ipc call lock lock
bind = SUPER SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
bindl = , XF86AudioNext, exec, dms ipc call mpris next
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
# === Window Management ===
bind = SUPER, Q, killactive
bind = SUPER, F, fullscreen, 1
bind = SUPER SHIFT, F, fullscreen, 0
bind = SUPER SHIFT, T, togglefloating
bind = SUPER, W, togglegroup
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
# === Focus Navigation ===
bind = SUPER, left, movefocus, l
bind = SUPER, down, movefocus, d
bind = SUPER, up, movefocus, u
bind = SUPER, right, movefocus, r
bind = SUPER, H, movefocus, l
bind = SUPER, J, movefocus, d
bind = SUPER, K, movefocus, u
bind = SUPER, L, movefocus, r
# === Window Movement ===
bind = SUPER SHIFT, left, movewindow, l
bind = SUPER SHIFT, down, movewindow, d
bind = SUPER SHIFT, up, movewindow, u
bind = SUPER SHIFT, right, movewindow, r
bind = SUPER SHIFT, H, movewindow, l
bind = SUPER SHIFT, J, movewindow, d
bind = SUPER SHIFT, K, movewindow, u
bind = SUPER SHIFT, L, movewindow, r
# === Column Navigation ===
bind = SUPER, Home, focuswindow, first
bind = SUPER, End, focuswindow, last
# === Monitor Navigation ===
bind = SUPER CTRL, left, focusmonitor, l
bind = SUPER CTRL, right, focusmonitor, r
bind = SUPER CTRL, H, focusmonitor, l
bind = SUPER CTRL, J, focusmonitor, d
bind = SUPER CTRL, K, focusmonitor, u
bind = SUPER CTRL, L, focusmonitor, r
# === Move to Monitor ===
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
# === Workspace Navigation ===
bind = SUPER, Page_Down, workspace, e+1
bind = SUPER, Page_Up, workspace, e-1
bind = SUPER, U, workspace, e+1
bind = SUPER, I, workspace, e-1
bind = SUPER CTRL, down, movetoworkspace, e+1
bind = SUPER CTRL, up, movetoworkspace, e-1
bind = SUPER CTRL, U, movetoworkspace, e+1
bind = SUPER CTRL, I, movetoworkspace, e-1
# === Workspace Management ===
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
# === Move Workspaces ===
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
bind = SUPER SHIFT, U, movetoworkspace, e+1
bind = SUPER SHIFT, I, movetoworkspace, e-1
# === Mouse Wheel Navigation ===
bind = SUPER, mouse_down, workspace, e+1
bind = SUPER, mouse_up, workspace, e-1
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
# === Numbered Workspaces ===
bind = SUPER, 1, workspace, 1
bind = SUPER, 2, workspace, 2
bind = SUPER, 3, workspace, 3
bind = SUPER, 4, workspace, 4
bind = SUPER, 5, workspace, 5
bind = SUPER, 6, workspace, 6
bind = SUPER, 7, workspace, 7
bind = SUPER, 8, workspace, 8
bind = SUPER, 9, workspace, 9
# === Move to Numbered Workspaces ===
bind = SUPER SHIFT, 1, movetoworkspace, 1
bind = SUPER SHIFT, 2, movetoworkspace, 2
bind = SUPER SHIFT, 3, movetoworkspace, 3
bind = SUPER SHIFT, 4, movetoworkspace, 4
bind = SUPER SHIFT, 5, movetoworkspace, 5
bind = SUPER SHIFT, 6, movetoworkspace, 6
bind = SUPER SHIFT, 7, movetoworkspace, 7
bind = SUPER SHIFT, 8, movetoworkspace, 8
bind = SUPER SHIFT, 9, movetoworkspace, 9
# === Column Management ===
bind = SUPER, bracketleft, layoutmsg, preselect l
bind = SUPER, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = SUPER, R, layoutmsg, togglesplit
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = SUPER, mouse:272, Move window, movewindow
bindmd = SUPER, mouse:273, Resize window, resizewindow
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
# === Manual Sizing ===
binde = SUPER, minus, resizeactive, -10% 0
binde = SUPER, equal, resizeactive, 10% 0
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
binde = SUPER SHIFT, equal, resizeactive, 0 10%
# === Screenshots ===
bind = , Print, exec, dms screenshot
bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window
# === System Controls ===
bind = SUPER SHIFT, P, dpms, toggle

View File

@@ -0,0 +1,25 @@
# ! Auto-generated file. Do not edit directly.
# Remove source = ./dms/colors.conf from your config to override.
$primary = rgb(d0bcff)
$outline = rgb(948f99)
$error = rgb(f2b8b5)
general {
col.active_border = $primary
col.inactive_border = $outline
}
group {
col.border_active = $primary
col.border_inactive = $outline
col.border_locked_active = $error
col.border_locked_inactive = $outline
groupbar {
col.active = $primary
col.inactive = $outline
col.locked_active = $error
col.locked_inactive = $outline
}
}

View File

@@ -0,0 +1,11 @@
# Auto-generated by DMS - do not edit manually
general {
gaps_in = 4
gaps_out = 4
border_size = 2
}
decoration {
rounding = 12
}

View File

@@ -12,7 +12,6 @@ monitor = , preferred,auto,auto
# ==================
exec-once = dbus-update-activation-environment --systemd --all
exec-once = systemctl --user start hyprland-session.target
exec-once = bash -c "wl-paste --watch cliphist store &"
# ==================
# INPUT CONFIG
@@ -28,10 +27,7 @@ input {
general {
gaps_in = 5
gaps_out = 5
border_size = 0 # off in niri
col.active_border = rgba(707070ff)
col.inactive_border = rgba(d0d0d0ff)
border_size = 2
layout = dwindle
}
@@ -43,7 +39,7 @@ decoration {
rounding = 12
active_opacity = 1.0
inactive_opacity = 0.9
inactive_opacity = 1.0
shadow {
enabled = true
@@ -85,196 +81,41 @@ master {
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
vrr = 1
}
# ==================
# WINDOW RULES
# ==================
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrule = rounding 12, match:class ^(org\.gnome\.)
windowrulev2 = tile, class:^(gnome-control-center)$
windowrulev2 = tile, class:^(pavucontrol)$
windowrulev2 = tile, class:^(nm-connection-editor)$
windowrule = tile on, match:class ^(gnome-control-center)$
windowrule = tile on, match:class ^(pavucontrol)$
windowrule = tile on, match:class ^(nm-connection-editor)$
windowrulev2 = float, class:^(gnome-calculator)$
windowrulev2 = float, class:^(galculator)$
windowrulev2 = float, class:^(blueman-manager)$
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
windowrulev2 = float, class:^(steam)$
windowrulev2 = float, class:^(xdg-desktop-portal)$
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
windowrule = float on, match:class ^(gnome-calculator)$
windowrule = float on, match:class ^(galculator)$
windowrule = float on, match:class ^(blueman-manager)$
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
windowrule = float on, match:class ^(xdg-desktop-portal)$
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = noborder, class:^(Alacritty)$
windowrulev2 = noborder, class:^(zen)$
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
windowrulev2 = noborder, class:^(kitty)$
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
windowrule = float on, match:class ^(zoom)$
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
# ! Hyprland doesn't size these windows correctly so disabling by default here
# windowrule = float on, match:class ^(org.quickshell)$
layerrule = noanim, ^(quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = no_anim on, match:namespace ^dms:.*
# ==================
# KEYBINDINGS
# ==================
$mod = SUPER
# === Application Launchers ===
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
bind = $mod, N, exec, dms ipc call notifications toggle
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
# === Cheat sheet
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = $mod ALT, L, exec, dms ipc call lock lock
bind = $mod SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
# === Window Management ===
bind = $mod, Q, killactive
bind = $mod, F, fullscreen, 1
bind = $mod SHIFT, F, fullscreen, 0
bind = $mod SHIFT, T, togglefloating
bind = $mod, W, togglegroup
# === Focus Navigation ===
bind = $mod, left, movefocus, l
bind = $mod, down, movefocus, d
bind = $mod, up, movefocus, u
bind = $mod, right, movefocus, r
bind = $mod, H, movefocus, l
bind = $mod, J, movefocus, d
bind = $mod, K, movefocus, u
bind = $mod, L, movefocus, r
# === Window Movement ===
bind = $mod SHIFT, left, movewindow, l
bind = $mod SHIFT, down, movewindow, d
bind = $mod SHIFT, up, movewindow, u
bind = $mod SHIFT, right, movewindow, r
bind = $mod SHIFT, H, movewindow, l
bind = $mod SHIFT, J, movewindow, d
bind = $mod SHIFT, K, movewindow, u
bind = $mod SHIFT, L, movewindow, r
# === Column Navigation ===
bind = $mod, Home, focuswindow, first
bind = $mod, End, focuswindow, last
# === Monitor Navigation ===
bind = $mod CTRL, left, focusmonitor, l
bind = $mod CTRL, right, focusmonitor, r
bind = $mod CTRL, H, focusmonitor, l
bind = $mod CTRL, J, focusmonitor, d
bind = $mod CTRL, K, focusmonitor, u
bind = $mod CTRL, L, focusmonitor, r
# === Move to Monitor ===
bind = $mod SHIFT CTRL, left, movewindow, mon:l
bind = $mod SHIFT CTRL, down, movewindow, mon:d
bind = $mod SHIFT CTRL, up, movewindow, mon:u
bind = $mod SHIFT CTRL, right, movewindow, mon:r
bind = $mod SHIFT CTRL, H, movewindow, mon:l
bind = $mod SHIFT CTRL, J, movewindow, mon:d
bind = $mod SHIFT CTRL, K, movewindow, mon:u
bind = $mod SHIFT CTRL, L, movewindow, mon:r
# === Workspace Navigation ===
bind = $mod, Page_Down, workspace, e+1
bind = $mod, Page_Up, workspace, e-1
bind = $mod, U, workspace, e+1
bind = $mod, I, workspace, e-1
bind = $mod CTRL, down, movetoworkspace, e+1
bind = $mod CTRL, up, movetoworkspace, e-1
bind = $mod CTRL, U, movetoworkspace, e+1
bind = $mod CTRL, I, movetoworkspace, e-1
# === Move Workspaces ===
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
bind = $mod SHIFT, U, movetoworkspace, e+1
bind = $mod SHIFT, I, movetoworkspace, e-1
# === Mouse Wheel Navigation ===
bind = $mod, mouse_down, workspace, e+1
bind = $mod, mouse_up, workspace, e-1
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
# === Numbered Workspaces ===
bind = $mod, 1, workspace, 1
bind = $mod, 2, workspace, 2
bind = $mod, 3, workspace, 3
bind = $mod, 4, workspace, 4
bind = $mod, 5, workspace, 5
bind = $mod, 6, workspace, 6
bind = $mod, 7, workspace, 7
bind = $mod, 8, workspace, 8
bind = $mod, 9, workspace, 9
# === Move to Numbered Workspaces ===
bind = $mod SHIFT, 1, movetoworkspace, 1
bind = $mod SHIFT, 2, movetoworkspace, 2
bind = $mod SHIFT, 3, movetoworkspace, 3
bind = $mod SHIFT, 4, movetoworkspace, 4
bind = $mod SHIFT, 5, movetoworkspace, 5
bind = $mod SHIFT, 6, movetoworkspace, 6
bind = $mod SHIFT, 7, movetoworkspace, 7
bind = $mod SHIFT, 8, movetoworkspace, 8
bind = $mod SHIFT, 9, movetoworkspace, 9
# === Column Management ===
bind = $mod, bracketleft, layoutmsg, preselect l
bind = $mod, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = $mod, R, layoutmsg, togglesplit
bind = $mod CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = $mod, mouse:272, Move window, movewindow
bindmd = $mod, mouse:273, Resize window, resizewindow
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
# === Manual Sizing ===
binde = $mod, minus, resizeactive, -10% 0
binde = $mod, equal, resizeactive, 10% 0
binde = $mod SHIFT, minus, resizeactive, 0 -10%
binde = $mod SHIFT, equal, resizeactive, 0 10%
# === Screenshots ===
bind = , Print, exec, dms screenshot
bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window
# === System Controls ===
bind = $mod SHIFT, P, dpms, off
source = ./dms/colors.conf
source = ./dms/outputs.conf
source = ./dms/layout.conf
source = ./dms/cursor.conf
source = ./dms/binds.conf

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows {
highlight {
corner-radius 12

View File

@@ -15,6 +15,8 @@ binds {
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
@@ -46,6 +48,24 @@ binds {
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
XF86AudioPause allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPlay allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPrev allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "previous";
}
XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next";
}
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3";
}
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
@@ -62,6 +82,7 @@ binds {
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
@@ -119,6 +140,11 @@ binds {
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Workspace Management ===
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
spawn "dms" "ipc" "call" "workspace-rename" "open";
}
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
@@ -192,4 +218,4 @@ binds {
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}
}

View File

@@ -1,16 +1,19 @@
// ! Auto-generated file. Do not edit directly.
// Remove `include "dms/colors.kdl"` from your config to override.
layout {
background-color "transparent"
focus-ring {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
border {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
shadow {
@@ -18,19 +21,19 @@ layout {
}
tab-indicator {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
insert-hint {
color "#9dcbfb80"
color "#d0bcff80"
}
}
recent-windows {
highlight {
active-color "#124a73"
urgent-color "#ffb4ab"
active-color "#4f378b"
urgent-color "#f2b8b5"
}
}
}

View File

@@ -0,0 +1,17 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
gaps 4

View File

@@ -18,15 +18,64 @@ gestures {
input {
keyboard {
xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
@@ -109,7 +158,6 @@ overview {
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
environment {
XDG_CURRENT_DESKTOP "niri"
}
@@ -176,14 +224,19 @@ window-rule {
open-floating false
}
window-rule {
match app-id=r#"^org\.gnome\.Calculator$"#
match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"#
match app-id=r#"^org\.gnome\.Nautilus$"#
match app-id=r#"^steam$"#
match app-id=r#"^xdg-desktop-portal$"#
open-floating true
}
window-rule {
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
default-floating-position x=10 y=10 relative-to="bottom-right"
open-focused false
}
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
match app-id="Alacritty"
@@ -192,10 +245,6 @@ window-rule {
match app-id="kitty"
draw-border-with-background false
}
window-rule {
match is-active=false
opacity 0.9
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom"
@@ -204,6 +253,7 @@ window-rule {
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true
}
debug {
@@ -225,3 +275,5 @@ include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"

View File

@@ -4,3 +4,12 @@ import _ "embed"
//go:embed embedded/hyprland.conf
var HyprlandConfig string
//go:embed embedded/hypr-colors.conf
var HyprColorsConfig string
//go:embed embedded/hypr-layout.conf
var HyprLayoutConfig string
//go:embed embedded/hypr-binds.conf
var HyprBindsConfig string

View File

@@ -16,3 +16,6 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl
var NiriBindsConfig string
//go:embed embedded/niri-greeter.kdl
var NiriGreeterConfig string

View File

@@ -199,31 +199,6 @@ func labToHex(L, a, b float64) string {
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
}
// Adjust brightness while keeping the same hue
func retoneToL(hex string, Ltarget float64) string {
rgb := HexToRGB(hex)
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
L, a, b := col.Lab()
L100 := L * 100.0
scale := 1.0
if L100 != 0 {
scale = Ltarget / L100
}
a2, b2 := a*scale, b*scale
// Don't let it get too saturated
maxChroma := 0.4
if math.Hypot(a2, b2) > maxChroma {
k := maxChroma / math.Hypot(a2, b2)
a2 *= k
b2 *= k
}
return labToHex(Ltarget, a2, b2)
}
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
Lf := getLstar(hexFg)
Lb := getLstar(hexBg)
@@ -345,7 +320,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
}
step := 0.5
for i := 0; i < 120; i++ {
for range 120 {
Lf = math.Max(0, math.Min(100, Lf+dir*step))
cand := labToHex(Lf, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
@@ -356,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
return hexColor
}
// Bidirectional contrast - tries both lighter and darker, picks closest to original
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
if current >= minLc {
return hexColor
}
fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
origL, af, bf := cf.Lab()
var darkerResult, lighterResult string
darkerL, lighterL := origL, origL
darkerFound, lighterFound := false, false
step := 0.5
for i := range 120 {
if !darkerFound {
darkerL = math.Max(0, origL-float64(i)*step)
cand := labToHex(darkerL, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
darkerResult = cand
darkerFound = true
}
}
if !lighterFound {
lighterL = math.Min(100, origL+float64(i)*step)
cand := labToHex(lighterL, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
lighterResult = cand
lighterFound = true
}
}
if darkerFound && lighterFound {
break
}
}
if darkerFound && lighterFound {
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
return darkerResult
}
return lighterResult
}
if darkerFound {
return darkerResult
}
if lighterFound {
return lighterResult
}
return hexColor
}
type PaletteOptions struct {
IsLight bool
Background string
@@ -369,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
}
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
if opts.UseDPS {
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
}
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
}
func blendHue(base, target, factor float64) float64 {
diff := target - base
if diff > 0.5 {
diff -= 1.0
} else if diff < -0.5 {
diff += 1.0
}
result := base + diff*factor
if result < 0 {
result += 1.0
} else if result >= 1.0 {
result -= 1.0
}
return result
}
func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb)
@@ -389,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
pr := HexToRGB(primaryColor)
ph := RGBToHSV(pr)
var palette Palette
var normalTextTarget, secondaryTarget float64
@@ -410,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
}
palette.Color0 = NewColorInfo(bgColor)
hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15
baseSat := math.Max(ph.S, 0.5)
baseVal := math.Max(ph.V, 0.5)
redH := math.Mod(0.0+hueShift+1.0, 1.0)
var redColor string
if opts.IsLight {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} else {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
}
redH := blendHue(0.0, ph.H, 0.12)
greenH := blendHue(0.33, ph.H, 0.10)
yellowH := blendHue(0.14, ph.H, 0.04)
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string
if opts.IsLight {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
}
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string
if opts.IsLight {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} else {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
}
var blueColor string
if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
}
magH := hsv.H - 0.03
if magH < 0 {
magH += 1.0
}
var magColor string
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
}
cyanH := hsv.H + 0.08
if cyanH > 1.0 {
cyanH -= 1.0
}
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
accentTarget := secondaryTarget * 0.7
if opts.IsLight {
palette.Color7 = NewColorInfo("#1a1a1a")
palette.Color8 = NewColorInfo("#2e2e2e")
} else {
palette.Color7 = NewColorInfo("#abb2bf")
palette.Color8 = NewColorInfo("#5c6370")
}
redS := math.Min(baseSat*1.2, 1.0)
redV := baseVal * 0.95
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
if opts.IsLight {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
brightBlue := retoneToL(primaryColor, 85.0)
palette.Color12 = NewColorInfo(brightBlue)
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 {
brightCyanH -= 1.0
}
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
}
greenS := math.Min(baseSat*1.3, 1.0)
greenV := baseVal * 0.75
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
if opts.IsLight {
palette.Color15 = NewColorInfo("#1a1a1a")
yellowS := math.Min(baseSat*1.5, 1.0)
yellowV := math.Min(baseVal*1.2, 1.0)
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
blueS := math.Min(ph.S*1.05, 1.0)
blueV := math.Min(ph.V*1.05, 1.0)
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
// Color5 matches primary_container exactly (light container in light mode)
container5 := DeriveContainer(primaryColor, true)
palette.Color5 = NewColorInfo(container5)
palette.Color6 = NewColorInfo(primaryColor)
gray7S := baseSat * 0.08
gray7V := baseVal * 0.28
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.05
gray8V := baseVal * 0.85
dimTarget := secondaryTarget * 0.5
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
brightRedS := math.Min(baseSat*1.0, 1.0)
brightRedV := math.Min(baseVal*1.2, 1.0)
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
brightGreenS := math.Min(baseSat*1.1, 1.0)
brightGreenV := math.Min(baseVal*1.1, 1.0)
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
brightYellowS := math.Min(baseSat*1.4, 1.0)
brightYellowV := math.Min(baseVal*1.3, 1.0)
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
brightBlueS := math.Min(ph.S*1.1, 1.0)
brightBlueV := math.Min(ph.V*1.15, 1.0)
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
lightContainer := DeriveContainer(primaryColor, true)
palette.Color13 = NewColorInfo(lightContainer)
brightCyanS := ph.S * 0.5
brightCyanV := math.Min(ph.V*1.3, 1.0)
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
white15S := baseSat * 0.04
white15V := math.Min(baseVal*1.5, 1.0)
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
} else {
palette.Color15 = NewColorInfo("#ffffff")
redS := math.Min(baseSat*1.1, 1.0)
redV := math.Min(baseVal*1.15, 1.0)
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
greenS := math.Min(baseSat*1.0, 1.0)
greenV := math.Min(baseVal*1.0, 1.0)
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
yellowS := math.Min(baseSat*1.1, 1.0)
yellowV := math.Min(baseVal*1.25, 1.0)
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
// Slightly more saturated variant of primary
blueS := math.Min(ph.S*1.2, 1.0)
blueV := ph.V * 0.95
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
// Color5 matches primary_container exactly (dark container in dark mode)
darkContainer := DeriveContainer(primaryColor, false)
palette.Color5 = NewColorInfo(darkContainer)
palette.Color6 = NewColorInfo(primaryColor)
gray7S := baseSat * 0.12
gray7V := math.Min(baseVal*1.05, 1.0)
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.15
gray8V := baseVal * 0.65
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
brightRedS := math.Min(baseSat*0.75, 1.0)
brightRedV := math.Min(baseVal*1.35, 1.0)
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
brightGreenS := math.Min(baseSat*0.7, 1.0)
brightGreenV := math.Min(baseVal*1.2, 1.0)
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
brightYellowS := math.Min(baseSat*0.7, 1.0)
brightYellowV := math.Min(baseVal*1.5, 1.0)
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
// Color12: Start of the lighter gradient - slightly desaturated
brightBlueS := ph.S * 0.85
brightBlueV := math.Min(ph.V*1.1, 1.0)
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
// Medium-high saturation pastel primary
color13S := ph.S * 0.7
color13V := math.Min(ph.V*1.3, 1.0)
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
// Lower saturation, lighter variant
color14S := ph.S * 0.45
color14V := math.Min(ph.V*1.4, 1.0)
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
white15S := baseSat * 0.05
white15V := math.Min(baseVal*1.45, 1.0)
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
}
return palette

View File

@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
}
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
// Color15 is now derived from primary, so just verify it's a valid color
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
color15Lum := Luminance(result.Color15.Hex)
if tt.opts.IsLight {
// Light mode: Color15 should still be relatively light
if color15Lum < 0.5 {
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
}
} else {
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
if color15Lum < 0.5 {
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
}
}
})
}
@@ -579,6 +588,10 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
bgColor := result.Color0.Hex
for i := 1; i < 8; i++ {
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
if i == 5 || i == 6 {
continue
}
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
minLc := 30.0
if lc < minLc && lc > 0 {
@@ -658,7 +671,7 @@ func TestContrastAlgorithmComparison(t *testing.T) {
}
differentCount := 0
for i := 0; i < 16; i++ {
for i := range 16 {
if wcagColors[i].Hex != dpsColors[i].Hex {
differentCount++
}

View File

@@ -112,3 +112,24 @@ func GenerateWeztermTheme(p Palette) string {
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
return result.String()
}
func GenerateNeovimTheme(p Palette) string {
var result strings.Builder
fmt.Fprintf(&result, "vim.g.terminal_color_0 = \"%s\"\n", p.Color0.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_1 = \"%s\"\n", p.Color1.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_2 = \"%s\"\n", p.Color2.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_3 = \"%s\"\n", p.Color3.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_4 = \"%s\"\n", p.Color4.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_5 = \"%s\"\n", p.Color5.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_6 = \"%s\"\n", p.Color6.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_7 = \"%s\"\n", p.Color7.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_8 = \"%s\"\n", p.Color8.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_9 = \"%s\"\n", p.Color9.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_10 = \"%s\"\n", p.Color10.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_11 = \"%s\"\n", p.Color11.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_12 = \"%s\"\n", p.Color12.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_13 = \"%s\"\n", p.Color13.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_14 = \"%s\"\n", p.Color14.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_15 = \"%s\"\n", p.Color15.Hex)
return result.String()
}

View File

@@ -7,9 +7,11 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
func init() {
@@ -25,6 +27,9 @@ func init() {
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
@@ -40,6 +45,9 @@ func init() {
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
}
type ArchDistribution struct {
@@ -90,6 +98,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectGit())
dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectDMSGreeter())
dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectAccountsService())
@@ -103,10 +112,8 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectClipboardTools()...)
return dependencies, nil
}
@@ -119,12 +126,52 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
}
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
}
func (a *ArchDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("pacman", "-Q", pkg)
err := cmd.Run()
return err == nil
}
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
data, err := os.ReadFile(srcinfoPath)
if err != nil {
return nil, nil, err
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
var pkg string
var target *[]string
switch {
case strings.HasPrefix(line, "makedepends = "):
pkg = strings.TrimPrefix(line, "makedepends = ")
target = &makedeps
case strings.HasPrefix(line, "depends = "):
pkg = strings.TrimPrefix(line, "depends = ")
target = &deps
default:
continue
}
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
pkg = pkg[:idx]
}
pkg = strings.TrimSpace(pkg)
if pkg != "" {
*target = append(*target, pkg)
}
}
return deps, makedeps, nil
}
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
return exec.Command("pacman", "-Si", pkg).Run() == nil
}
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
@@ -134,13 +181,12 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
}
@@ -197,11 +243,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
}
if a.packageInstalled("dms-shell-bin") {
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -251,7 +293,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
LogOutput: "Installing base-devel development tools",
}
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
return fmt.Errorf("failed to install base-devel: %w", err)
}
@@ -283,6 +325,13 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
}
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
}
// Phase 3: System Packages
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
@@ -344,7 +393,7 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := a.EnableDMSService(ctx); err != nil {
if err := a.EnableDMSService(ctx, wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -400,6 +449,37 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
return systemPkgs, aurPkgs, manualPkgs, variantMap
}
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if a.packageInstalled("quickshell-git") {
return nil
}
if a.packageInstalled("quickshell") {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.15,
Step: "Removing stable quickshell...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
}
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
return fmt.Errorf("failed to remove stable quickshell: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.18,
Step: "Building quickshell-git before system packages...",
IsComplete: false,
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
}
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
}
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
@@ -408,6 +488,9 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
if slices.Contains(packages, "dms-shell") {
args = append(args, "--assume-installed", "dms-shell-compositor=1")
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
@@ -419,7 +502,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
@@ -431,29 +514,10 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false
hasQuickshell := false
for _, pkg := range packages {
if pkg == "niri-git" {
hasNiri = true
}
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
}
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -514,16 +578,13 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
var dmsShell []string
for _, pkg := range packages {
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
if pkg == "dms-shell-git" {
dmsShell = append(dmsShell, pkg)
} else {
isDep := false
for _, dep := range dmsDepencies {
if pkg == dep {
deps = append(deps, pkg)
isDep = true
break
}
if slices.Contains(dmsDepencies, pkg) {
deps = append(deps, pkg)
isDep = true
}
if !isDep {
others = append(others, pkg)
@@ -537,6 +598,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
}
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
}
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
if visited[pkg] {
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
return nil
}
visited[pkg] = true
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
@@ -549,7 +620,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
}
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
defer func() {
@@ -588,7 +659,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
}
}
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
if pkg == "dms-shell-git" {
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
depsToRemove := []string{
"depends = quickshell",
@@ -610,54 +681,66 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
}
// Skip dependency installation for dms-shell-git and dms-shell-bin
// since we manually manage those dependencies
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
// Pre-install dependencies from .SRCINFO
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
{
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Installing package dependencies and makedepends",
CommandInfo: "Classifying dependencies as system or AUR",
}
// Install dependencies and makedepends explicitly
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi
`, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
if err != nil {
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
}
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
if [ ! -z "$makedeps" ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
fi
`, srcinfoPath, sudoPassword))
seen := make(map[string]bool)
var systemPkgs []string
var aurPkgs []string
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
for _, dep := range append(runtimeDeps, makeDeps...) {
if seen[dep] || a.packageInstalled(dep) {
continue
}
seen[dep] = true
if a.isInSystemRepo(dep) {
systemPkgs = append(systemPkgs, dep)
} else {
aurPkgs = append(aurPkgs, dep)
}
}
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.32*(endProgress-startProgress),
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
}
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
}
}
for _, aurDep := range aurPkgs {
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
}
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
startProgress+0.35*(endProgress-startProgress),
startProgress+0.39*(endProgress-startProgress),
visited,
); err != nil {
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
}
}
}
@@ -671,7 +754,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err)
@@ -686,42 +769,9 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
CommandInfo: "sudo pacman -U built-package",
}
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
var files []string
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
// For DMS split packages, install base package
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
matches, err := filepath.Glob(pattern)
if err == nil {
for _, match := range matches {
basename := filepath.Base(match)
// Always include base package
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
files = append(files, match)
}
}
}
// Also update compositor-specific packages if they're installed
if strings.HasSuffix(pkg, "-git") {
if a.packageInstalled("dms-shell-hyprland-git") {
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
files = append(files, hyprlandMatches[0])
}
}
if a.packageInstalled("dms-shell-niri-git") {
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
files = append(files, niriMatches[0])
}
}
}
} else {
// For other packages, install all built packages
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
files = matches
}
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
files = matches
if len(files) == 0 {
return fmt.Errorf("no package files found after building %s", pkg)
@@ -730,7 +780,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
installArgs := []string{"pacman", "-U", "--noconfirm"}
installArgs = append(installArgs, files...)
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
fileNames := make([]string, len(files))
for i, f := range files {

View File

@@ -14,6 +14,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
)
@@ -55,27 +56,6 @@ func (b *BaseDistribution) logError(message string, err error) {
b.log(errorMsg)
}
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
func escapeSingleQuotes(s string) string {
return strings.ReplaceAll(s, "'", "'\\''")
}
// MakeSudoCommand creates a command string that safely passes password to sudo.
// This helper escapes special characters in the password to prevent shell injection
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
func MakeSudoCommand(sudoPassword string, command string) string {
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
}
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
// The password is properly escaped to prevent shell injection and syntax errors.
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
cmdStr := MakeSudoCommand(sudoPassword, command)
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
}
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
status := deps.StatusMissing
if b.commandExists(name) {
@@ -102,6 +82,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
}
}
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: false,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system")
}
@@ -185,37 +178,6 @@ func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.D
}
}
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
var dependencies []deps.Dependency
cliphist := deps.StatusMissing
if b.commandExists("cliphist") {
cliphist = deps.StatusInstalled
}
wlClipboard := deps.StatusMissing
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
wlClipboard = deps.StatusInstalled
}
dependencies = append(dependencies,
deps.Dependency{
Name: "cliphist",
Status: cliphist,
Description: "Wayland clipboard manager",
Required: true,
},
deps.Dependency{
Name: "wl-clipboard",
Status: wlClipboard,
Description: "Wayland clipboard utilities",
Required: true,
},
)
return dependencies
}
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency
@@ -565,7 +527,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
}
envDir := filepath.Join(homeDir, ".config", "environment.d")
if err := os.MkdirAll(envDir, 0755); err != nil {
if err := os.MkdirAll(envDir, 0o755); err != nil {
return fmt.Errorf("failed to create environment.d directory: %w", err)
}
@@ -581,15 +543,12 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
terminalCmd = "ghostty"
}
content := fmt.Sprintf(`QT_QPA_PLATFORM=wayland
ELECTRON_OZONE_PLATFORM_HINT=auto
QT_QPA_PLATFORMTHEME=gtk3
QT_QPA_PLATFORMTHEME_QT6=gtk3
content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
TERMINAL=%s
`, terminalCmd)
envFile := filepath.Join(envDir, "90-dms.conf")
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
if err := os.WriteFile(envFile, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to write environment config: %w", err)
}
@@ -597,12 +556,18 @@ TERMINAL=%s
return nil
}
func (b *BaseDistribution) EnableDMSService(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to enable dms service: %w", err)
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
switch wm {
case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service")
}
case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target")
}
}
b.log("Enabled dms systemd user service")
return nil
}
@@ -622,7 +587,7 @@ func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0755); err != nil {
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
@@ -633,7 +598,7 @@ Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
@@ -725,7 +690,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
}
// Install to /usr/local/bin
installCmd := ExecSudoCommand(ctx, sudoPassword,
installCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install DMS binary: %w", err)

View File

@@ -43,7 +43,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0755)
os.MkdirAll(dmsPath, 0o755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
@@ -55,7 +55,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
testFile := filepath.Join(dmsPath, "test.txt")
os.WriteFile(testFile, []byte("test"), 0644)
os.WriteFile(testFile, []byte("test"), 0o644)
exec.Command("git", "-C", dmsPath, "add", ".").Run()
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
@@ -87,7 +87,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0755)
os.MkdirAll(dmsPath, 0o755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
@@ -99,7 +99,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
testFile := filepath.Join(dmsPath, "test.txt")
os.WriteFile(testFile, []byte("test"), 0644)
os.WriteFile(testFile, []byte("test"), 0o644)
exec.Command("git", "-C", dmsPath, "add", ".").Run()
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
@@ -125,7 +125,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0755)
os.MkdirAll(dmsPath, 0o755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)

View File

@@ -7,6 +7,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
func init() {
@@ -60,6 +61,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectGit())
dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectDMSGreeter())
dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectAccountsService())
@@ -69,7 +71,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectClipboardTools()...)
return dependencies, nil
}
@@ -86,10 +87,41 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
}
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
}
func (d *DebianDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
return err == nil
return debianPackageInstalledPrecisely(pkg)
}
func debianPackageInstalledPrecisely(pkg string) bool {
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
output, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(output)) == "installed"
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func debianRepoArchitecture(arch string) string {
switch arch {
case "amd64", "x86_64":
return "amd64"
case "arm64", "aarch64":
return "arm64"
default:
return arch
}
}
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -102,16 +134,15 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
// DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
@@ -161,7 +192,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Updating APT package lists",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
return fmt.Errorf("failed to update package lists: %w", err)
}
@@ -178,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err)
}
@@ -190,12 +221,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: "Installing development dependencies...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools",
}
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err)
}
@@ -312,7 +343,7 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := d.EnableDMSService(ctx); err != nil {
if err := d.EnableDMSService(ctx, wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -375,6 +406,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
return names
}
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
@@ -387,6 +426,8 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
debianVersion := "Debian_13"
if osInfo.VersionID == "testing" {
debianVersion = "Debian_Testing"
} else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") {
debianVersion = "Debian_Unstable"
}
for _, pkg := range obsPkgs {
@@ -410,7 +451,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
// Create keyrings directory if it doesn't exist
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
if err := mkdirCmd.Run(); err != nil {
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
}
@@ -424,13 +465,13 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
}
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
// Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -440,7 +481,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
}
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
@@ -460,7 +501,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
}
@@ -476,20 +517,46 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
args = append(args, packages...)
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
startProgress := 0.40
endProgress := 0.60
if totalGroups > 1 {
if groupIndex == 1 {
endProgress = 0.50
} else {
startProgress = 0.50
}
}
args := d.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: startProgress,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
}
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -549,7 +616,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err)
}
case "cliphist", "dgop":
case "dgop":
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
@@ -568,7 +635,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
args := []string{"apt-get", "install", "-y"}
args = append(args, depList...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
}
@@ -586,7 +653,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}
@@ -625,7 +692,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
}

View File

@@ -7,12 +7,16 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
func init() {
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
@@ -75,6 +79,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectGit())
dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectDMSGreeter())
dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectAccountsService())
@@ -88,10 +93,8 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectClipboardTools()...)
return dependencies, nil
}
@@ -117,14 +120,13 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
// COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
}
@@ -157,7 +159,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
}
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
}
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
@@ -195,6 +197,10 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
}
}
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
}
func (f *FedoraDistribution) getPrerequisites() []string {
return []string{
"dnf-plugins-core",
@@ -249,7 +255,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
args := []string{"dnf", "install", "-y"}
args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
f.logError("failed to install prerequisites", err)
@@ -353,7 +359,7 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := f.EnableDMSService(ctx); err != nil {
if err := f.EnableDMSService(ctx, wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -432,7 +438,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
cmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
output, err := cmd.CombinedOutput()
if err != nil {
@@ -456,7 +462,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
}
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
priorityOutput, err := priorityCmd.CombinedOutput()
if err != nil {
@@ -479,28 +485,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
}
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -510,26 +495,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"}
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
args := []string{"dnf", "install", "-y"}
if minimal {
args = append(args, "--setopt=install_weak_deps=False")
}
return append(args, packages...)
}
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := f.dnfInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing COPR packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
var GentooGlobalUseFlags = []string{
@@ -107,7 +108,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectClipboardTools()...)
return dependencies, nil
}
@@ -140,7 +140,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
@@ -151,7 +150,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
}
@@ -204,9 +202,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
var cmd *exec.Cmd
if hasUse {
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
} else {
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
}
output, err := cmd.CombinedOutput()
@@ -284,7 +282,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Syncing Portage tree with emerge --sync",
}
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
syncOutput, syncErr := syncCmd.CombinedOutput()
if syncErr != nil {
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
@@ -305,7 +303,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
g.logError("failed to install prerequisites", err)
@@ -410,7 +408,7 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := g.EnableDMSService(ctx); err != nil {
if err := g.EnableDMSService(ctx, wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -506,14 +504,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
}
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
packageUseDir := "/etc/portage/package.use"
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("mkdir -p %s", packageUseDir))
if output, err := mkdirCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
@@ -527,7 +525,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
if checkExistingCmd.Run() == nil {
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
if output, err := replaceCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
@@ -535,7 +533,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
}
}
appendCmd := ExecSudoCommand(ctx, sudoPassword,
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
output, err := appendCmd.CombinedOutput()
@@ -560,7 +558,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
}
// Enable GURU repository
enableCmd := ExecSudoCommand(ctx, sudoPassword,
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
output, err := enableCmd.CombinedOutput()
@@ -592,7 +590,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
LogOutput: "Syncing GURU repository",
}
syncCmd := ExecSudoCommand(ctx, sudoPassword,
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
syncOutput, syncErr := syncCmd.CombinedOutput()
@@ -625,7 +623,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
if output, err := mkdirCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
@@ -639,7 +637,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
if checkExistingCmd.Run() == nil {
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
if output, err := replaceCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
@@ -647,7 +645,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
}
}
appendCmd := ExecSudoCommand(ctx, sudoPassword,
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
output, err := appendCmd.CombinedOutput()
@@ -698,6 +696,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
}

View File

@@ -55,6 +55,7 @@ const (
PhaseAURPackages
PhaseCursorTheme
PhaseConfiguration
PhaseGreeterSetup
PhaseComplete
)

View File

@@ -9,6 +9,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
// ManualPackageInstaller provides methods for installing packages from source
@@ -18,8 +19,8 @@ type ManualPackageInstaller struct {
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
lines := strings.Split(output, "\n")
for _, line := range lines {
lines := strings.SplitSeq(output, "\n")
for line := range lines {
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
parts := strings.Split(line, "refs/tags/")
if len(parts) > 1 {
@@ -74,10 +75,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install hyprland: %w", err)
}
case "hyprpicker":
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err)
}
case "ghostty":
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
@@ -86,10 +83,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install matugen: %w", err)
}
case "cliphist":
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install cliphist: %w", err)
}
case "xwayland-satellite":
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
@@ -111,12 +104,12 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "dgop-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -151,7 +144,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
CommandInfo: "sudo make install",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
m.logError("failed to install dgop", err)
@@ -168,10 +161,10 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
homeDir, _ := os.UserHomeDir()
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() {
@@ -221,7 +214,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
CommandInfo: "dpkg -i niri.deb",
}
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
output, err := installDebCmd.CombinedOutput()
@@ -245,12 +238,12 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "quickshell-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -281,7 +274,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
}
buildDir := tmpDir + "/build"
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
@@ -332,7 +325,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err)
@@ -351,12 +344,12 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "hyprland-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -395,7 +388,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
CommandInfo: "sudo make install",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install Hyprland: %w", err)
@@ -405,184 +398,6 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
return nil
}
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing hyprpicker from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
// Install hyprutils first
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.05,
Step: "Building hyprutils dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
}
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
return fmt.Errorf("failed to create hyprutils directory: %w", err)
}
defer os.RemoveAll(hyprutilsDir)
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
if err := cloneUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprutils: %w", err)
}
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-DBUILD_TESTING=off",
"-S", ".",
"-B", "./build")
configureUtilsCmd.Dir = hyprutilsDir
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
return fmt.Errorf("failed to configure hyprutils: %w", err)
}
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
buildUtilsCmd.Dir = hyprutilsDir
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
return fmt.Errorf("failed to build hyprutils: %w", err)
}
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installUtilsCmd.Dir = hyprutilsDir
if err := installUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprutils: %w", err)
}
// Install hyprwayland-scanner
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Building hyprwayland-scanner dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
}
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
if err := os.MkdirAll(scannerDir, 0755); err != nil {
return fmt.Errorf("failed to create scanner directory: %w", err)
}
defer os.RemoveAll(scannerDir)
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
if err := cloneScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
}
configureScannerCmd := exec.CommandContext(ctx, "cmake",
"-DCMAKE_INSTALL_PREFIX=/usr",
"-B", "build")
configureScannerCmd.Dir = scannerDir
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
}
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
buildScannerCmd.Dir = scannerDir
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
}
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installScannerCmd.Dir = scannerDir
if err := installScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
}
// Now build hyprpicker
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: "Cloning hyprpicker repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprpicker: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.45,
Step: "Configuring hyprpicker build...",
IsComplete: false,
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
}
configureCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-S", ".",
"-B", "./build")
configureCmd.Dir = tmpDir
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
output, err := configureCmd.CombinedOutput()
if err != nil {
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
Step: "Building hyprpicker...",
IsComplete: false,
CommandInfo: "cmake --build build --target hyprpicker",
}
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
return fmt.Errorf("failed to build hyprpicker: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing hyprpicker...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err)
}
m.log("hyprpicker installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing Ghostty from source...")
@@ -592,12 +407,12 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "ghostty-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -639,7 +454,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
installCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
@@ -678,16 +493,11 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
}
// Make it executable
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
return fmt.Errorf("failed to make matugen executable: %w", err)
}
@@ -714,7 +524,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
}
configDir := filepath.Dir(dmsPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("failed to create quickshell config directory: %w", err)
}
@@ -803,52 +613,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing cliphist from source...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Installing cliphist via go install...",
IsComplete: false,
CommandInfo: "go install go.senan.xyz/cliphist@latest",
}
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
return fmt.Errorf("failed to install cliphist: %w", err)
}
homeDir := os.Getenv("HOME")
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
targetPath := "/usr/local/bin/cliphist"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing cliphist binary to system...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
}
// Make it executable
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make cliphist executable: %w", err)
}
m.log("cliphist installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing xwayland-satellite from source...")
@@ -878,15 +642,11 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
}
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
}

View File

@@ -0,0 +1,44 @@
package distros
type minimalInstallGroup struct {
packages []string
minimal bool
}
func shouldPreferMinimalInstall(pkg string) bool {
switch pkg {
case "niri", "niri-git":
return true
default:
return false
}
}
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
for _, pkg := range packages {
if shouldPreferMinimalInstall(pkg) {
minimal = append(minimal, pkg)
continue
}
normal = append(normal, pkg)
}
return normal, minimal
}
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
normal, minimal := splitMinimalInstallPackages(packages)
groups := make([]minimalInstallGroup, 0, 2)
if len(minimal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: minimal,
minimal: true,
})
}
if len(normal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: normal,
minimal: false,
})
}
return groups
}

View File

@@ -9,12 +9,19 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
func init() {
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
return NewOpenSUSEDistribution(config, logChan)
})
Register("opensuse-leap", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
return NewOpenSUSEDistribution(config, logChan)
})
Register("opensuse-slowroll", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
return NewOpenSUSEDistribution(config, logChan)
})
}
type OpenSUSEDistribution struct {
@@ -23,6 +30,8 @@ type OpenSUSEDistribution struct {
config DistroConfig
}
const openSUSENiriWaylandServerPackage = "libwayland-server0"
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{
@@ -65,6 +74,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectGit())
dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectDMSGreeter())
dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectAccountsService())
@@ -78,10 +88,8 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectClipboardTools()...)
return dependencies, nil
}
@@ -96,6 +104,10 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
return err == nil
}
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
}
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
@@ -104,17 +116,16 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
packages := map[string]PackageMapping{
// Standard zypper packages
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
// DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
@@ -191,35 +202,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
}
func (o *OpenSUSEDistribution) getPrerequisites() []string {
return []string{
"make",
"unzip",
"gcc",
"gcc-c++",
"cmake",
"ninja",
"pkgconf-pkg-config",
"git",
"qt6-base-devel",
"qt6-declarative-devel",
"qt6-declarative-private-devel",
"qt6-shadertools",
"qt6-shadertools-devel",
"qt6-wayland-devel",
"qt6-waylandclient-private-devel",
"spirv-tools-devel",
"cli11-devel",
"wayland-protocols-devel",
"libgbm-devel",
"libdrm-devel",
"pipewire-devel",
"jemalloc-devel",
"wayland-utils",
"Mesa-libGLESv3-devel",
"pam-devel",
"glib2-devel",
"polkit-devel",
}
return []string{}
}
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -267,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
args := []string{"zypper", "install", "-y"}
args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
o.logError("failed to install prerequisites", err)
@@ -289,6 +272,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting prerequisite check...",
}
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to disable install media repositories: %w", err)
}
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
@@ -319,7 +306,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
}
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
return fmt.Errorf("failed to install zypper packages: %w", err)
}
}
@@ -334,7 +321,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
}
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err)
}
}
@@ -371,7 +358,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := o.EnableDMSService(ctx); err != nil {
if err := o.EnableDMSService(ctx, wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
@@ -424,9 +411,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
}
}
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
return systemPkgs, obsPkgs, manualPkgs, variantMap
}
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
return nil
}
return []string{openSUSENiriWaylandServerPackage}
}
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
for _, pkg := range extraPkgs {
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
continue
}
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
systemPkgs = append(systemPkgs, pkg)
}
return systemPkgs
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
@@ -438,6 +448,19 @@ func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
osInfo, err := GetOSInfo()
if err != nil {
return fmt.Errorf("failed to get OS info: %w", err)
}
obsDistroVersion := "openSUSE_Tumbleweed"
switch osInfo.Distribution.ID {
case "opensuse-leap":
obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID)
case "opensuse-slowroll":
obsDistroVersion = "openSUSE_Slowroll"
}
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
@@ -445,8 +468,8 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
repoPath, pkg.RepoURL)
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s/%s.repo",
repoPath, obsDistroVersion, pkg.RepoURL)
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
if checkCmd.Run() == nil {
@@ -463,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
cmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
@@ -484,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
}
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to refresh repositories: %w", err)
}
@@ -493,27 +516,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
return nil
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
func isOpenSUSEInstallMediaURI(uri string) bool {
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
return strings.HasPrefix(normalizedURI, "cd:/") ||
strings.HasPrefix(normalizedURI, "dvd:/") ||
strings.HasPrefix(normalizedURI, "hd:/") ||
strings.HasPrefix(normalizedURI, "iso:/")
}
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 7 {
continue
}
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
alias := parts[1]
enabled := strings.ToLower(parts[3])
uri := parts[len(parts)-1]
if alias == "" || strings.EqualFold(alias, "alias") {
continue
}
if enabled != "" && enabled != "yes" {
continue
}
if !isOpenSUSEInstallMediaURI(uri) {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
output, err := listCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
return fmt.Errorf("failed to list zypper repositories: %w", err)
}
aliases := parseZypperInstallMediaAliases(string(output))
if len(aliases) == 0 {
return nil
}
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.055,
Step: "Disabling install media repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
}
for _, alias := range aliases {
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
}
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
}
return nil
}
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
args := []string{"zypper", "install", "-y"}
if minimal {
args = append(args, "--no-recommends")
}
return append(args, packages...)
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
if len(packages) == 0 {
return nil
}
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
args := []string{"zypper", "install", "-y"}
args = append(args, packages...)
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := o.zypperInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
}
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -525,12 +667,12 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "quickshell-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -561,7 +703,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
}
buildDir := tmpDir + "/build"
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
@@ -632,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err)
@@ -656,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
CommandInfo: "sudo zypper install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}

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