1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Compare commits

...

253 Commits

Author SHA1 Message Date
xdenotte
cbd1fd908c Fix ProcessList context menu visibility in DankPopout (#857) 2025-11-30 11:21:15 -05:00
bbedward
b2cf20f3d8 core: add pre-commit hooks for go CI checks 2025-11-30 11:04:12 -05:00
bbedward
915f1a5036 cli: remove distribution enforcement from tui 2025-11-30 10:51:38 -05:00
bbedward
a55ec6416c dankinstall: remove dead nix code, add doc link 2025-11-30 10:22:54 -05:00
yayuuu
b1834b1958 Adde Loader to only load shapes once the correct path has been generated (#851) 2025-11-30 10:11:53 -05:00
Willem Schipper
1beeb9fb55 fix: recreate plugin popout binding even if contentHeight is already set (#852) 2025-11-30 10:11:18 -05:00
bbedward
18d86354ec wallpaper: revert last changes
fixes #855
2025-11-30 10:06:01 -05:00
dms-ci[bot]
6297b0679c nix: update vendorHash for go.mod changes 2025-11-30 06:43:50 +00:00
bbedward
d62ef635a7 ci: use gh app 2025-11-30 01:42:15 -05:00
bbedward
c53836040f dankbar: add width/height deps to binding 2025-11-30 01:28:04 -05:00
bbedward
0b638bf85f ci: add update-vendor trigger 2025-11-30 01:23:23 -05:00
bbedward
7f6a71b964 ci: switch to gh pat 2025-11-30 01:20:19 -05:00
bbedward
1b4363a54a dankbar: dont early return in path functions 2025-11-30 01:08:38 -05:00
bbedward
16d168c970 core: update deps 2025-11-30 01:05:15 -05:00
bbedward
4606d7960e dankbar: remove caching redraw prevention 2025-11-30 00:56:36 -05:00
bbedward
4eee126d26 media: suppress media OSD on new players for 2s
fixes #838
2025-11-30 00:35:24 -05:00
bbedward
dde426658f core: fix golang-ci lints and add a config 2025-11-30 00:12:45 -05:00
bbedward
f6874fbcad workflow: run go CI on PRs 2025-11-29 23:35:40 -05:00
bbedward
621d4e4d92 dankbar: remove barTint Shape 2025-11-29 23:12:12 -05:00
bbedward
76062231fd dankbar: another hack to try and fix opacity 2025-11-29 23:06:49 -05:00
bbedward
261f55fea5 dankbar: simplify transparency binding 2025-11-29 22:55:14 -05:00
bbedward
202cf4bcc9 dankbar: try something else for binding 2025-11-29 22:43:55 -05:00
Willem Schipper
b7572f727f feat: allow popout to resize to its contents (#847) 2025-11-29 22:39:30 -05:00
bbedward
50ab346d58 dankbar: try to fix binding issues on creation 2025-11-29 22:36:20 -05:00
bbedward
b11b375848 settings: optimize mem usage
- keep un-loaded unless called upon
2025-11-29 18:32:45 -05:00
bbedward
e6c3ae9397 cups: add comprehensive CUPs setting page
- Add printers
- Delete printers
- Use polkit APIs as fallback on auth errors
- Fix ref system to conditionally subscribe to cups when wanted
2025-11-29 17:35:21 -05:00
bbedward
df663aceb9 net: less Theme.success 2025-11-29 11:14:15 -05:00
bbedward
db7e597f67 DankDash: fix per-monitor wallpapers 2025-11-29 11:10:10 -05:00
bbedward
1d3fe81ff7 network: big feature enrichment
- Dedicated view in settings
- VPN profile management
- Ethernet disconnection
- Turn prompts into floating windows
2025-11-29 10:00:05 -05:00
Lucas
9c887fbe63 spotlight: fix mouse action menu click (#841) 2025-11-28 23:32:35 -05:00
Lucas
4723bffcd2 spotlight: fix clipping and add context menu keyboard navigation (#840)
* spotlight: fix clipping and add context menu keyboard navigation

* prime: also detect nvidia-offload command

* spotlight: fix review nitpicks
2025-11-28 19:36:35 -05:00
purian23
9643de3ca0 Update greet sync to rec ACL 2025-11-28 18:45:55 -05:00
purian23
3bf3a54916 Enhance DMS Greeter logic 2025-11-28 18:10:54 -05:00
Marcus Ramberg
bcffc8856a nix: install completion support for dms cli (#836) 2025-11-28 19:59:37 -03:00
purian23
6b8c35c27b feat: DMS Greeter for Ubuntu 2025-11-28 16:32:48 -05:00
bbedward
dd409b4d1c osd/audio: bind audio change to pipewire, suppress OSDs on startup and
resume from suspend
2025-11-28 11:05:53 -05:00
bbedward
94a1aebe2b dgop: use dgop for uptime 2025-11-28 10:41:59 -05:00
bbedward
d3030c3ec6 color picker: fall back to niri picker when on niri
fixes #828
2025-11-28 09:47:19 -05:00
purian23
0221021078 Enhance DMS Greeter automation
- Thanks @brunodsf05 for doing some legwork to hunt this down!
2025-11-27 23:12:33 -05:00
purian23
966021bfd4 fix: DankBar binding loop & sth transparency 2025-11-27 22:13:13 -05:00
bbedward
f06e6e85d5 niri: support compact kb layout display
fixes #818
fixes #500
2025-11-27 10:53:37 -05:00
bbedward
28ad641070 displays: workaround for duplicate models 2025-11-27 10:34:18 -05:00
bbedward
384c775f1a dank16: enrich with hex, hex stripped, rgb 2025-11-27 09:46:45 -05:00
bbedward
ce40c691e9 niri: remove waitingForResults since it doesnt work and bind to search
term length
2025-11-27 01:47:33 -05:00
bbedward
5b0c38b0ed niri: fix warnings in overview 2025-11-27 01:01:35 -05:00
bbedward
734456785f matugen: log worker messages 2025-11-27 00:53:32 -05:00
bbedward
4f24312432 matugen: always set color scheme on exit 2025-11-27 00:31:56 -05:00
bbedward
d79b1ff3b4 displays: show physical resolution/mode instead of logical
fixes #819
2025-11-26 23:54:19 -05:00
bbedward
bbe1c1f1e0 filebrowser: re-add layer surface version 2025-11-26 23:51:59 -05:00
purian23
1978e67401 Update dms-cli for OBS packages 2025-11-26 23:27:33 -05:00
purian23
e129e4a2d0 Update dms-cli for nightly builds 2025-11-26 22:17:49 -05:00
Lucas
f7f1bbbdd2 nix: fix NixOS systemd service PATH (#823) 2025-11-26 18:30:06 -05:00
Saurabh
de8f2e6a68 feat/matugen3 (#771)
* added matugen 3 terminal templates and logic

fixed version check and light terminal check

refactored json generation

fixed syntax

keep tmp debug

fixed file outputs

fixed syntax issues and implicit passing

added debug stderr output

* moved calls to matugen after template is built correctly

added --json hex

disabled debug message

cleaned up code into modular functions, re-added second full matugen call

fixed args

added shift

commented vs code section

debug changes

* arg format fixes

fixed json import flag

fixed string quotation

fix arg order

* cleaned up

fix cfg naming

* removed mt2.0 templates and refactored worker

removed/replaced matugen 2 templates

fix formatter diffs + consistent styling

* fixed last json output

* fixed syntax error

* vs code templates

* matugen: inject all stock/custom theme colors as overrides
- also some general architectural changes

* dank16: remove vscode enrich option

---------

Co-authored-by: bbedward
2025-11-26 16:34:53 -05:00
Álvaro
85704e3947 Improved applications naming in AudioOutputDetail (#821) 2025-11-26 16:28:26 -05:00
bbedward
4d661ff41d dankinstall: add artix 2025-11-26 16:18:11 -05:00
bbedward
d7b39634e6 hyprland: fix focus grab 2025-11-26 12:46:19 -05:00
bbedward
039c98b9e3 power: switch to hold-style confirmation
fixes #775
2025-11-26 11:19:18 -05:00
bbedward
172c4bf0a9 confirm: add keepPopoutsOpen 2025-11-26 10:34:59 -05:00
bbedward
1f2a1c5dec niri: keep overview focus when open 2025-11-26 09:38:15 -05:00
bbedward
e5a6a00282 improve border 2025-11-26 00:35:21 -05:00
bbedward
d8153f7611 dankbar: improve config reactivity 2025-11-25 22:35:38 -05:00
bbedward
8b6ae3f39b bar: use shape > canvas 2025-11-25 18:51:47 -05:00
bbedward
24537781b7 remove UPower import from Theme 2025-11-25 17:24:52 -05:00
Álvaro
d2a29506aa Add middle-click close and collapse popout (#813)
* Add middle-click close and collapse popout

* Revert ControlCenterPopout
2025-11-25 16:21:01 -05:00
bbedward
adf51d5264 cava: tweak options 2025-11-25 16:17:52 -05:00
bbedward
0864179085 media: change icon for player volume 2025-11-25 15:02:59 -05:00
bbedward
8de77f283d niri: fix exit anims on overview launcher 2025-11-25 14:54:29 -05:00
bbedward
004a014000 windows: add minimum sizes 2025-11-25 13:58:08 -05:00
bbedward
80f6eb94aa appdrawer: fix not getting mouse events sometimes 2025-11-25 12:25:40 -05:00
bbedward
4035c9cc5f plugins: fix reactivity, tooltips, new IPCs to reload 2025-11-25 11:02:38 -05:00
bbedward
3a365f6807 settings: make plugin browser and widget browser floating 2025-11-25 10:33:32 -05:00
purian23
9920a0a59f Tweak Workflows 2025-11-25 10:09:11 -05:00
github-actions[bot]
c17bb9e171 chore: update packaging versions
🤖 Automated update by GitHub Actions
Workflow run: https://github.com/AvengeMedia/DankMaterialShell/actions/runs/19673220228
2025-11-25 14:37:10 +00:00
purian23
03073f6875 Refactor distro logic & automation 2025-11-25 09:32:24 -05:00
bbedward
609caf6e5f windows: disable QT CSD 2025-11-25 09:24:40 -05:00
bbedward
411141ff88 wallpaper: fix cycling
fixes #812
2025-11-25 09:24:00 -05:00
purian23
3e472e18bd Merge pull request #809 from LuckShiba/fix-scroll
bar: fix scroll on widgets that doesn't handle scroll
2025-11-25 01:24:45 -05:00
LuckShiba
e5b6fbd12a bar: fix scroll on widgets that doesn't handle scroll 2025-11-25 03:21:35 -03:00
bbedward
c2787f1282 wallpaper: disable cycling if any toplevel is full screen 2025-11-24 22:28:53 -05:00
bbedward
df940124b1 net: allow overriding wifi device 2025-11-24 21:27:18 -05:00
bbedward
5288d042ca media: fix player button control popup things 2025-11-24 20:51:05 -05:00
bbedward
fa98a27c90 dankbar: add generic bar widget IPC for popouts
fixes #750
2025-11-24 19:52:26 -05:00
bbedward
d341a5a60b dankbar/controlcenter: add VPN, mic, brightness, battery, and printer
options for widget
2025-11-24 16:36:49 -05:00
purian23
7f15227de1 Reduce dups & add workflow hotfix 2025-11-24 13:58:22 -05:00
purian23
bb45240665 Further optimize OBS build scripts 2025-11-24 13:10:16 -05:00
bbedward
29f84aeab5 dankbar: fix monitoring widgets with no background option
fixes #806
2025-11-24 12:26:29 -05:00
bbedward
5a52edcad8 ws: add option for occupied only 2025-11-24 12:03:34 -05:00
bbedward
b078e23aa1 settings: fix scrollable area in window 2025-11-24 11:56:10 -05:00
bbedward
7fa87125b5 audio: optimize visualizations 2025-11-24 11:37:24 -05:00
bbedward
f618df46d8 audio: optimize non-cava fallback 2025-11-24 11:08:03 -05:00
bbedward
ee03853901 idle: add fade to lock option
fixes #694
fixes #805
2025-11-24 10:59:36 -05:00
bbedward
6c4a9bcfb8 modals: restore Top layer as default
- Cut a mask in the background window
- restores virt kb compat
2025-11-24 09:38:03 -05:00
bbedward
1bec20ecef dankbar: fix individual widget settings 2025-11-24 00:48:35 -05:00
bbedward
08c9bf570d widgets: add an outline option
fixes #804
2025-11-24 00:14:19 -05:00
bbedward
5e77a10a81 dankbar: make border shape respect goth radius
part of #804
2025-11-23 23:55:07 -05:00
bbedward
3bc6461e2a sysmon: change spacing of monitor widgets 2025-11-23 23:26:00 -05:00
bbedward
d3194e15e2 dock: hide pin to dock for internal windows 2025-11-23 22:55:47 -05:00
bbedward
2db79ef202 dankbar: de-bounce bar settings 2025-11-23 22:23:18 -05:00
bbedward
b3c07edef6 notifications: fix DnD tooltip 2025-11-23 20:37:08 -05:00
bbedward
b773fdca34 cc: fix brightness tooltip 2025-11-23 20:33:52 -05:00
bbedward
2e9f9f7b7e media: restore tooltips 2025-11-23 20:31:54 -05:00
bbedward
30cbfe729d dank tooltip v2: apply to settings 2025-11-23 20:00:45 -05:00
Álvaro
b036da2446 Added per app volume control (#801)
* Added per app volume control

* format and lint fixes
2025-11-23 19:46:21 -05:00
bbedward
c8a9fb1674 media: make controls more usable since popout change 2025-11-23 19:38:10 -05:00
bbedward
43bea80cad power: disable profile osd by default, ensure dbus activation doesnt
happen
2025-11-23 18:17:35 -05:00
Lucas
23538c0323 bar: fix auto-hide hiding when tray popout is opened (#802) 2025-11-23 18:06:55 -05:00
bbedward
2ae911230d osd: try to optimize power profile osd more 2025-11-23 17:29:56 -05:00
bbedward
5ce1cb87ea power profile: put OSD in a lazyloader 2025-11-23 16:55:22 -05:00
bbedward
2a37028b6a dock: touch of inner padding to dms icon 2025-11-23 16:00:51 -05:00
bbedward
8130feb2a0 paths: show dms icon & title for dms windows 2025-11-23 15:57:03 -05:00
purian23
c49a875ec2 Workflow updates 2025-11-23 14:34:07 -05:00
bbedward
2a002304b9 migrate default font family props to Theme 2025-11-23 13:26:04 -05:00
bbedward
d9522818ae greeter: fix custom themes and font family
fixes #776
2025-11-23 13:21:16 -05:00
bbedward
800588e121 modal: remove targetScreen usage
fixes #798
2025-11-23 13:03:32 -05:00
bbedward
991c31ebdb i18n: update translations 2025-11-23 12:49:29 -05:00
bbedward
48f77e1691 processlist: convert to floating window 2025-11-23 12:16:03 -05:00
bbedward
42de6fd074 modals: apply same pattern of multi-window
- fixes excessive repaints
fixes #716
2025-11-23 12:07:45 -05:00
bbedward
62845b470c popout: fix excessive repaints
- Size content window to content size, buffer for shadow
- Add second window for click outside behavior
- User overriding the layer disables the click outside behavior

part of #716
2025-11-23 10:49:59 -05:00
bbedward
fd20986cf8 settings: make responsive, view-stack style 2025-11-23 10:01:26 -05:00
purian23
61369cde9e Update gitignore 2025-11-23 03:16:00 -05:00
purian23
644384ce8b feat: Mult-Distro support - Debian, Ubuntu, OpenSuse 2025-11-23 02:39:24 -05:00
Lucas
97c11a2482 bar: fix auto-hide not hiding after popout closes (#796) 2025-11-23 01:38:58 -05:00
bbedward
1e7e1c2d78 settings: clamp max content width 2025-11-23 01:38:16 -05:00
bbedward
1c7201fb04 settings: make settings and file browser normal windows
- add default floating rules for dankinstall
2025-11-23 01:23:06 -05:00
bbedward
61ec0c697a gamma: dont transition before destroying controls 2025-11-23 00:48:23 -05:00
bbedward
4b5fce1bfc dankbar: hide settings when bar is disabled 2025-11-23 00:45:12 -05:00
Lucas
6cc6e7c8e9 Media volume scroll on DankBar widget and media volume OSD (#795)
* osd: add media volume OSD

* media: scroll on widget changes media volume

* dash: use media volume in media tab
2025-11-23 00:42:06 -05:00
bbedward
89298fce30 bar: don't apply opacity to sth color
- legacy thing that already has it
2025-11-22 16:15:07 -05:00
bbedward
a3a27e07fa dankbar: support multiple bars and per-display bars
- Migrate settings to v2
  - Up to 4 bars
  - Per-bar settings instead of global
2025-11-22 15:28:06 -05:00
bbedward
4f32376f22 gamma: remove display sync on destruction 2025-11-22 15:26:05 -05:00
bbedward
58bf189941 launcher: set default launch prefix, if launching from systemd
- prevents apps dying when stopping the systemd unit
2025-11-22 00:23:06 -05:00
bbedward
bcfa508da5 weather: fix fahrenheit conversion 2025-11-21 22:07:44 -05:00
mbpowers
c0ae3ef58b fix: bar and dock flickering autohide (#784) 2025-11-21 21:49:31 -05:00
mbpowers
1e70d7b4c3 fix: remove useFahrenheit refresh, fetch Celcius convert locally (#785)
* fix: remove useFahrenheit refresh, fetch Celcius convert locally

* fix: typo in change unit button
2025-11-21 21:41:12 -05:00
bbedward
f8dc6ad2bc update CONTRIBUTING 2025-11-21 17:30:54 -05:00
bbedward
e22482988f weather: fix display when 0 temp
fixes #782
2025-11-21 17:06:57 -05:00
bbedward
4eb896629d net: fix VPN prompting for password 2025-11-21 12:59:12 -05:00
bbedward
b310e66275 themes: shift catpuccin palete 2025-11-21 09:30:58 -05:00
bbedward
b39da1bea7 cc: bit of extra height for some detail items 2025-11-21 09:15:59 -05:00
Pi Home Server
fa575d0574 Fix background color of the privacy widget (#779) 2025-11-21 09:05:56 -05:00
bbedward
dfe2f3771b theme: add colorful bar widget option 2025-11-21 00:07:23 -05:00
bbedward
46caeb0445 sounds: only play audio changed when trigger by us 2025-11-20 23:38:06 -05:00
bbedward
59cc9c7006 niri: ensure overview spotlight is hidden when main window is brought up 2025-11-20 21:23:56 -05:00
bbedward
12e91534eb niri: empty input region & disable spotlight content when not open 2025-11-20 16:44:46 -05:00
bbedward
d9da88ceb5 niri: embed spotlight to same window as overview layer 2025-11-20 16:28:26 -05:00
bbedward
2dbfec0307 niri: close spotlight when closing overview 2025-11-20 13:56:35 -05:00
Lucas
09cf8c9641 niri: add spotlight on overview typing functionality (#774) 2025-11-20 13:48:30 -05:00
Pi Home Server
f1bed4d6a3 Feature/privacy widget fix (#772)
* Fix active camera icon

* Fix active camera icon
2025-11-20 12:30:23 -05:00
bbedward
2ed6c33c83 missing import 2025-11-19 19:14:47 -05:00
bbedward
7ad532ed17 dankinstall: add ultramarine 2025-11-19 18:53:41 -05:00
bbedward
92fe8c5b14 hyprland: restore focus grab to tray menus 2025-11-19 17:24:14 -05:00
bbedward
8e95572589 modals: move HyprFocusGrab out of common Modal 2025-11-19 17:16:51 -05:00
bbedward
62da862a66 modal: round textureSize pixels 2025-11-19 14:36:08 -05:00
bbedward
993e34f548 dankinstall: weakdeps for niri/system 2025-11-19 09:35:22 -05:00
github-actions[bot]
e39465aece chore: bump version to v0.6.2 2025-11-19 13:54:50 +00:00
bbedward
8fd616b680 osd: suppression fix from cc 2025-11-19 08:52:37 -05:00
bbedward
cc054b27de filebrowser: fix auto closing from ddash 2025-11-19 08:33:07 -05:00
github-actions[bot]
dfdaa82245 chore: bump version to v0.6.1 2025-11-19 03:16:35 +00:00
bbedward
99a307e0ad dankbar: hot fix color moda & systm tray item positions 2025-11-18 22:13:06 -05:00
github-actions[bot]
5ddea836a1 chore: bump version to v0.6.0 2025-11-18 23:52:39 +00:00
bbedward
208d92aa06 launcher: re-create grid on open 2025-11-18 18:50:42 -05:00
bbedward
6ef9ddd4f3 hyprland: fix right click overview 2025-11-18 17:53:00 -05:00
bbedward
1c92d39185 i18n: update translations 2025-11-18 17:21:45 -05:00
bbedward
c0f072217c dankbar: split up monolithic file 2025-11-18 16:18:24 -05:00
bbedward
542562f988 dankbar: missing background click handler for plugin popout 2025-11-18 16:03:30 -05:00
bbedward
4e6f0d5e87 bluez: fix disappearing popouts with modal maanger 2025-11-18 14:36:10 -05:00
bbedward
10639a5ead re-add bound lost my qmlfmt 2025-11-17 20:53:55 -05:00
bbedward
06d668e710 launcher: new search algo
- replace fzf.js with custom levenshtein distance matching
- tweak scoring system
- more graceful fuzzy, more weight to prefixes
- basic tokenization
2025-11-17 20:52:04 -05:00
bbedward
d1472dfcba osd: also have left center and right center options 2025-11-17 14:05:04 -05:00
bbedward
ccb4da3cd8 extws: fix force option 2025-11-17 10:08:06 -05:00
bbedward
46e96b49f0 extws: fix capability check & don't show names 2025-11-17 09:50:06 -05:00
bbedward
984cfe7f98 labwc: use dms dpms off/on for idle service 2025-11-17 09:12:38 -05:00
bbedward
d769300137 core/cli: add dpms off/on via wlr-output-power-management 2025-11-17 00:31:00 -05:00
Hikiru
d175d66828 Add NixOS module (#734)
* default.nix: fix "wavelength" typo

* Add nixos module

typo

fix

* nix: refactor and fix nix modules

* nix: fix NixOS module import

* nix: revert quickshell option change

* nix: fix nixosModules dmsPkgs definition

---------

Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2025-11-16 21:12:01 -05:00
bbedward
c1a314332e wallpaper: rename blur layer option 2025-11-16 19:50:19 -05:00
bbedward
046ac59d21 core/extworkspace: only register outputs on name received 2025-11-16 19:40:46 -05:00
bbedward
00c06f07d0 workspace: fix ext-ws hiding 2025-11-16 18:52:12 -05:00
bbedward
3e2ab40c6a ws: 0 width when 0 workspaces, restore labwc to README 2025-11-16 17:53:50 -05:00
bbedward
350ffd0052 i18n: update terms 2025-11-16 16:33:55 -05:00
bbedward
ecd1a622d2 display: fix wallpaper when using monitor model 2025-11-16 16:33:21 -05:00
bbedward
f13968aa61 osd: configurable position 2025-11-16 16:27:01 -05:00
bbedward
4d1ffde54c launcher: allow launch prefix to run in shell 2025-11-16 16:14:19 -05:00
bbedward
d69017a706 also update per-monitor wallpaper to accout for display setting 2025-11-16 16:01:11 -05:00
bbedward
f2deaeccdb scaling: snap value reported by wlr-output 2025-11-16 15:56:59 -05:00
bbedward
ea9b0d2a79 powermenu: use consistent new-style on locker + greeter
fixes #739
2025-11-16 15:05:06 -05:00
bbedward
2e6dbedb8b dwl/mango: support keyboard layout 2025-11-16 14:24:56 -05:00
bbedward
6f359df8f9 displays: allow filtering by model over name 2025-11-16 13:58:53 -05:00
claymorwan
f6db20cd06 confirm-modal:add layer namespace (#743) 2025-11-16 13:09:44 -05:00
bbedward
6287fae065 running apps: don't wrap on scroll wheel
fixes #740
2025-11-16 13:06:40 -05:00
bbedward
e441607ce3 colorpicker: don't include line break in copy
fixes #741
2025-11-16 13:00:13 -05:00
bbedward
b5379a95fa qs/dankbar/meta: add a mask region to the bar
- Allows bar items to be clickable evn when popouts open
- Add state machines to manage state across monitors
- change focuses to ondemand on hyprland
2025-11-16 12:52:13 -05:00
bbedward
64ec5be919 wallpaper: empty input region 2025-11-15 23:41:24 -05:00
bbedward
3916512d66 systemtray: fix erroneous undefined condition 2025-11-15 21:46:34 -05:00
bbedward
e2f426a1bd Revert "systemtray: fix UI thread freeze when opening menu on Hyprland"
This reverts commit 4cb652abd9.
2025-11-15 21:42:50 -05:00
bbedward
aa1df8dfcf core: more syncmap conversions 2025-11-15 20:00:47 -05:00
bbedward
67557555f2 core: refactor to use a generic-compatible syncmap 2025-11-15 19:45:19 -05:00
bbedward
4cb652abd9 systemtray: fix UI thread freeze when opening menu on Hyprland
- Similar pattern as fix from Noctalia
2025-11-15 17:57:23 -05:00
bbedward
d11868b99f systray: don't try to force focus of menus 2025-11-15 14:57:47 -05:00
bbedward
1798417e6a systemtray: don't take keyboard focus
- bricks hyprland
2025-11-15 14:48:13 -05:00
github-actions[bot]
43dc3e5bb1 nix: update vendorHash for go.mod changes 2025-11-15 19:43:35 +00:00
bbedward
91891a14ed core/wayland: thread-safety meta fixes + cleanups + hypr workaround
- fork go-wayland/client and modify to make it thread-safe internally
- use sync.Map and atomic values in many places to cut down on mutex
  boilerplate
- do not create extworkspace client unless explicitly requested
2025-11-15 14:41:00 -05:00
bbedward
20f7d60147 settings: various consistency issues fixed
part of #725
2025-11-15 12:05:44 -05:00
bbedward
7e17e7d37a osd: fix opacity
part of #725
2025-11-15 11:43:05 -05:00
bbedward
cbb244f785 osd: add option to disable each OSD 2025-11-15 11:36:33 -05:00
Sunner
1c264d858b Follow symlinks when searching for sessions (#728) 2025-11-15 10:29:34 -05:00
bbedward
217037c2ae evdev: fix test 2025-11-14 23:26:14 -05:00
bbedward
b4dbd0b69c evdev: enhance keyboard detection for capslock 2025-11-14 23:22:06 -05:00
github-actions[bot]
89a2b5c00b chore: bump version to v0.5.2 2025-11-15 00:31:06 +00:00
bbedward
929b6dae1a widgets: fix some 0-width issues 2025-11-14 19:26:51 -05:00
Pi Home Server
52fe493da9 Feature/privacy widget - Settings to force icons on (#715)
* Update

* Update

* Update

* Update

* Update

* Set default to false

* Update SettingsData.qml

Set default visibility to false

* privacy widget: fix truncated settings menu

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-11-14 19:16:17 -05:00
purian23
3e6be3e762 Greet path updates 2025-11-14 17:54:35 -05:00
purian23
7a8cc449b9 Add local ACL greeter permissions to dms core installer 2025-11-14 16:32:06 -05:00
purian23
8f5a9d6e9f Update dms greeter to scan system & local directories 2025-11-14 15:36:14 -05:00
bbedward
1c5e31fea9 greeter: allow mangowc as compositor 2025-11-14 14:51:28 -05:00
claymorwan
fd08ae18ab feat: plugin layer namespace (#717) 2025-11-14 14:50:29 -05:00
bbedward
a7eb3de06e dankbar: configurable auto-hide delay 2025-11-14 14:00:37 -05:00
bbedward
8902dd7c44 launcher: grid re-style and customizable column counts 2025-11-14 13:54:44 -05:00
bbedward
6387d8400c osd: account for bar position when on bottom 2025-11-14 13:47:26 -05:00
bbedward
597cacb9cc matugen: update gtk4/gtk3-dark colors
- also some change to dankinstall to use niri/xwls from system repos,
  too lazy to split the commits
2025-11-14 13:20:59 -05:00
bbedward
3e285ad9ff dankdash: remove useless tint rectangle
part of #716
2025-11-14 13:09:46 -05:00
bbedward
cc1fa89790 clock: use precision minutes instead of seconds, unless needed
part of #716
2025-11-14 12:42:23 -05:00
bbedward
b0ed007751 core/dankinstall: more deb fixes 2025-11-14 12:22:13 -05:00
bbedward
e1e2650d2b core/dankinstall: fix hyprland util manual compile on debian 2025-11-14 12:13:49 -05:00
bbedward
b23f17b633 core/dankinstall: fix hyprpicker build 2025-11-14 12:07:03 -05:00
github-actions[bot]
818e40b2df nix: update vendorHash for go.mod changes 2025-11-14 17:06:06 +00:00
bbedward
5685e39631 core: improve evdev capslock detection, wayland context fixes 2025-11-14 12:04:47 -05:00
kritag
72534b7674 adding tokyonight, everforest, nord and rose-pine themes (#714)
Co-authored-by: Kristian Tagesen <kristian.tagesen@tietoevry.com>
2025-11-14 11:40:26 -05:00
bbedward
328490d23d powermenu: smarter positioning in control center 2025-11-14 10:45:16 -05:00
bbedward
97a0696930 clock: fix overview clock when seconds is on 2025-11-14 10:29:41 -05:00
bbedward
cb4e0660e0 dock: add reveal IPCs 2025-11-14 10:08:16 -05:00
bbedward
67c642de4c keybinds: add toggleWithPath 2025-11-14 09:03:27 -05:00
bbedward
0d7c2e1024 core/cli: fix keybind provider path override 2025-11-14 08:56:16 -05:00
bbedward
16a779a41b powermenu: restore grid as an option
fixes #712
2025-11-14 08:51:15 -05:00
purian23
c4ca3c8644 Add root dms-cli build script 2025-11-14 00:22:49 -05:00
bbedward
aabcbe34f3 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-14 00:06:50 -05:00
bbedward
f06626e441 dock: use modded app IDs for grouping logic
fixes #710
2025-11-14 00:06:27 -05:00
purian23
c4e1a71776 Relocate notification tests to scripts dir 2025-11-13 23:53:18 -05:00
bbedward
77e6c16bd2 core/extworkspace: fix some thread-safety issues 2025-11-13 23:52:32 -05:00
purian23
9d1fac3570 Relocate Nix dir under distro/nix 2025-11-13 23:47:00 -05:00
bbedward
b7aeaa7fc5 systemtray: better hide/unhide behavioro 2025-11-13 22:49:30 -05:00
bbedward
f6d8c9ff61 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 22:41:47 -05:00
bbedward
0490794d6c dankbar: add caps lock indicator widget 2025-11-13 22:41:33 -05:00
github-actions[bot]
335c83dd3c nix: update vendorHash for go.mod changes 2025-11-14 03:26:50 +00:00
bbedward
91da720c26 i18n:update translations 2025-11-13 22:25:22 -05:00
bbedward
b6ac744a68 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 22:24:51 -05:00
bbedward
526c4092fd evdev: add evdev monitor for caps lock state 2025-11-13 22:24:27 -05:00
github-actions[bot]
ed06dda384 nix: update vendorHash for go.mod changes 2025-11-14 02:54:15 +00:00
bbedward
6465b11e9b core: ensure all NM tests use mock backend + re-orgs + dep updates 2025-11-13 21:44:03 -05:00
purian23
b2879878a1 feat: Priority pinned items in Control Center 2025-11-13 21:23:54 -05:00
bbedward
3e17b086fb ci: add docs to release archive 2025-11-13 20:19:54 -05:00
purian23
0545e6bcda Remove release tags 2025-11-13 20:01:38 -05:00
purian23
27a907433f Test Copr workflow update 2025-11-13 19:40:16 -05:00
purian23
69616800e3 Release update 2025-11-13 18:54:01 -05:00
485 changed files with 67910 additions and 26846 deletions

View File

@@ -1,8 +1,4 @@
#!/bin/bash #!/bin/bash
# DISABLED for now
exit 0
set -euo pipefail set -euo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -10,18 +6,61 @@ REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
cd "$REPO_ROOT" cd "$REPO_ROOT"
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then # =============================================================================
exit 0 # Go CI checks (when core/ files are staged)
fi # =============================================================================
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
if ! command -v python3 &>/dev/null; then if [[ -n "$STAGED_CORE_FILES" ]]; then
exit 0 echo "Go files staged in core/, running CI checks..."
fi cd "$REPO_ROOT/core"
if ! python3 scripts/i18nsync.py check &>/dev/null; then # Format check
echo "Translations out of sync" echo " Checking gofmt..."
echo "run python3 scripts/i18nsync.py sync" 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 exit 1
fi 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..."
go test ./... > /dev/null
# 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 exit 0

View File

@@ -7,9 +7,18 @@ on:
paths: paths:
- 'core/**' - 'core/**'
- '.github/workflows/go-ci.yml' - '.github/workflows/go-ci.yml'
pull_request:
branches: [master, main]
paths:
- 'core/**'
- '.github/workflows/go-ci.yml'
concurrency:
group: go-ci-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
test: lint-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
@@ -32,11 +41,20 @@ jobs:
exit 1 exit 1
fi fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
working-directory: core
- name: Test - name: Test
run: go test -v ./... run: go test -v ./...
- name: Build dms - name: Build dms
run: go build -v ./cmd/dms run: go build -v ./cmd/dms
- name: Build dms (distropkg)
run: go build -v -tags distro_binary ./cmd/dms
- name: Build dankinstall - name: Build dankinstall
run: go build -v ./cmd/dankinstall run: go build -v ./cmd/dankinstall

View File

@@ -35,6 +35,14 @@ jobs:
with: with:
go-version-file: ./core/go.mod 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 tests - name: Run tests
run: go test -v ./... run: go test -v ./...
@@ -124,38 +132,40 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-core needs: build-core
steps: 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 - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ steps.app_token.outputs.token }}
fetch-depth: 0 fetch-depth: 0
- name: Update VERSION - name: Update VERSION
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: | run: |
set -euo pipefail set -euo pipefail
git config user.name "github-actions[bot]" git config user.name "dms-ci[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
version="${GITHUB_REF#refs/tags/}" version="${GITHUB_REF#refs/tags/}"
version_no_v="${version#v}"
echo "Updating to version: $version" echo "Updating to version: $version"
# Update VERSION file in quickshell/
echo "${version}" > quickshell/VERSION echo "${version}" > quickshell/VERSION
git add quickshell/VERSION git add quickshell/VERSION
if ! git diff --cached --quiet; then if ! git diff --cached --quiet; then
git commit -m "chore: bump version to $version" git commit -m "chore: bump version to $version"
git push origin HEAD:master || git push origin HEAD:main git pull --rebase origin master
echo "Pushed version updates to master" git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else
echo "No version changes needed"
fi fi
# Force-push the tag to point to the commit with updated VERSION
git tag -f "${version}" git tag -f "${version}"
git push -f origin "${version}" git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
release: release:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -168,6 +178,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${{ github.ref_name }}
git checkout ${{ github.ref_name }}
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -255,6 +270,9 @@ jobs:
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Create QML source package (exclude build artifacts and git files) # Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
# Tar the CONTENTS of quickshell/, not the directory itself # Tar the CONTENTS of quickshell/, not the directory itself
(cd quickshell && tar --exclude='.git' \ (cd quickshell && tar --exclude='.git' \
--exclude='.github' \ --exclude='.github' \
@@ -291,6 +309,11 @@ jobs:
fi fi
done done
# Copy docs directory
if [ -d "docs" ]; then
cp -r docs _temp_full/
fi
# Create installation guide # Create installation guide
cat > _temp_full/INSTALL.md << 'EOFINSTALL' cat > _temp_full/INSTALL.md << 'EOFINSTALL'
# DankMaterialShell Installation # DankMaterialShell Installation
@@ -365,6 +388,68 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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: copr-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: release needs: release

View File

@@ -1,4 +1,4 @@
name: DMS Copr Stable Release (Manual) name: DMS Copr Stable Release
on: on:
workflow_dispatch: workflow_dispatch:
@@ -7,6 +7,10 @@ on:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)' description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false required: false
default: '' default: ''
release:
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
required: false
default: '1'
jobs: jobs:
build-and-upload: build-and-upload:
@@ -19,6 +23,7 @@ jobs:
- name: Determine version - name: Determine version
id: version id: version
run: | run: |
# Get version from manual input or latest release
if [ -n "${{ github.event.inputs.version }}" ]; then if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}" VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION" echo "Using manual version: $VERSION"
@@ -27,8 +32,14 @@ jobs:
echo "Using latest release version: $VERSION" echo "Using latest release version: $VERSION"
fi fi
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$RELEASE" ]; then
RELEASE="1"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "✅ Building DMS stable version: $VERSION" echo "release=$RELEASE" >> $GITHUB_OUTPUT
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
- name: Setup build environment - name: Setup build environment
run: | run: |
@@ -57,6 +68,7 @@ jobs:
- name: Generate stable spec file - name: Generate stable spec file
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')" CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF' cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
@@ -68,7 +80,7 @@ jobs:
Name: dms Name: dms
Version: %{version} Version: %{version}
Release: 1%{?dist} Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary} Summary: %{pkg_summary}
License: MIT License: MIT
@@ -212,16 +224,17 @@ jobs:
%{_bindir}/dgop %{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1 * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER - Stable release VERSION_PLACEHOLDER
- Built from GitHub release - Built from GitHub release
- Includes latest dms-cli and dgop binaries - Includes latest dms-cli and dgop binaries
SPECEOF SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec 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 sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}" echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
echo "" echo ""
echo "=== Spec file preview ===" echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec head -40 ~/rpmbuild/SPECS/dms.spec
@@ -295,7 +308,7 @@ jobs:
run: | run: |
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $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 "- **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/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY

243
.github/workflows/run-obs.yml vendored Normal file
View File

@@ -0,0 +1,243 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: 'Package to update (dms, dms-git, or all)'
required: false
default: 'all'
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
required: false
default: ''
push:
tags:
- 'v*'
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
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
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 "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get latest commit hash from master branch
LATEST_COMMIT=$(git rev-parse origin/master 2>/dev/null || git rev-parse master 2>/dev/null || echo "")
if [[ -z "$LATEST_COMMIT" ]]; then
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Could not determine git commit, proceeding with update"
else
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Check tarball age - if older than 3 hours, update needed
if [[ -f "dms-git-source.tar.gz" ]]; then
TARBALL_MTIME=$(stat -c%Y "dms-git-source.tar.gz" 2>/dev/null || echo "0")
CURRENT_TIME=$(date +%s)
AGE_SECONDS=$((CURRENT_TIME - TARBALL_MTIME))
AGE_HOURS=$((AGE_SECONDS / 3600))
# If tarball is older than 3 hours, check for new commits
if [[ $AGE_HOURS -ge 3 ]]; then
# Check if there are new commits in the last 3 hours
cd "${{ github.workspace }}"
NEW_COMMITS=$(git log --since="3 hours ago" --oneline origin/master 2>/dev/null | wc -l)
if [[ $NEW_COMMITS -gt 0 ]]; then
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commits detected in last 3 hours, update needed"
else
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 No new commits in last 3 hours, skipping update"
fi
else
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Recent upload exists (< 3 hours), skipping update"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No existing tarball in OBS, update needed"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
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
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
update-obs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
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"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
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 }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
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")
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
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
- name: Update dms stable version
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
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
fi
done
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- 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: Upload to OBS
env:
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ 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
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $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
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

123
.github/workflows/run-ppa.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
required: false
default: 'dms-git'
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
required: false
default: ''
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
jobs:
upload-ppa:
runs-on: ubuntu-latest
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
- 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: Determine packages to upload
id: packages
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $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=dms-git" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
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
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
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

@@ -1,6 +1,7 @@
name: Update Vendor Hash name: Update Vendor Hash
on: on:
workflow_dispatch:
push: push:
paths: paths:
- "core/go.mod" - "core/go.mod"
@@ -8,14 +9,25 @@ on:
branches: branches:
- master - master
permissions:
contents: write
jobs: jobs:
update-vendor-hash: update-vendor-hash:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
@@ -23,68 +35,32 @@ jobs:
- name: Update vendorHash in flake.nix - name: Update vendorHash in flake.nix
run: | run: |
set -euo pipefail set -euo pipefail
# Try to build and capture the expected hash from error message
echo "Attempting nix build to get new vendorHash..." echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dmsCli 2>&1); then if output=$(nix build .#dmsCli 2>&1); then
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
# Extract the expected hash from the error message
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
if [ -z "$new_hash" ]; then
echo "Could not extract new vendorHash from build output"
echo "Build output:"
echo "$output"
exit 1
fi
echo "New vendorHash: $new_hash"
# Get current hash from flake.nix
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
echo "Current vendorHash: $current_hash" [ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
if [ "$current_hash" = "$new_hash" ]; then
echo "vendorHash is already up to date"
exit 0
fi
# Update the hash in flake.nix
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
# Verify the build works with the new hash
echo "Verifying build with new vendorHash..." echo "Verifying build with new vendorHash..."
nix build .#dmsCli nix build .#dmsCli
echo "vendorHash updated successfully!" echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update - name: Commit and push vendorHash update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: | run: |
set -euo pipefail set -euo pipefail
if ! git diff --quiet flake.nix; then if ! git diff --quiet flake.nix; then
git config user.name "github-actions[bot]" git config user.name "dms-ci[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master
for attempt in 1 2 3; do git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
if git push; then
echo "Successfully pushed vendorHash update"
exit 0
fi
echo "Push attempt $attempt failed, pulling and retrying..."
git pull --rebase
sleep $((attempt*2))
done
echo "Failed to push after retries" >&2
exit 1
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi

6
.gitignore vendored
View File

@@ -136,3 +136,9 @@ go.work.sum
# .vscode/ # .vscode/
bin/ bin/
# Extracted source trees in Ubuntu package directories
distro/ubuntu/*/dms-git-repo/
distro/ubuntu/*/DankMaterialShell-*/
distro/ubuntu/danklinux/*/dsearch-*/
distro/ubuntu/danklinux/*/dgop-*/

View File

@@ -2,28 +2,50 @@
Contributions are welcome and encouraged. Contributions are welcome and encouraged.
## Formatting To contribute fork this repository, make your changes, and open a pull request.
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported. ## Setup
We need some consistent style, so this at least gives the same formatter that Qt Creator uses. Enable pre-commit hooks to catch CI failures before pushing:
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json. ```bash
git config core.hooksPath .githooks
```json
"customLocalFormatters.formatters": [
{
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
"languages": ["qml"]
}
],
"[qml]": {
"editor.defaultFormatter": "jkillian.custom-local-formatters",
"editor.formatOnSave": true
},
``` ```
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave. ## VSCode Setup
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
### QML (`quickshell` directory)
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
```json
{
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
}
```
3. Create empty `.qmlls.ini` file in `quickshell/` directory
```bash
cd quickshell
touch .qmlls.ini
```
4. Restart dms to generate the `.qmlls.ini` file
5. Make your changes, test, and open a pull request.
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
2. Ensure code is formatted with `make fmt`
3. Add appropriate test coverage and ensure tests pass with `make test`
4. Run `go mod tidy`
5. Open pull request
## Pull request ## Pull request

View File

@@ -15,11 +15,11 @@
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases) [![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin) [![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git) [![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Favengemediallc)](https://ko-fi.com/avengemediallc) [![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux)
</div> </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), 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/), 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 ## Repository Structure
@@ -36,8 +36,10 @@ DankMaterialShell/
│ ├── cmd/ # dms CLI and dankinstall binaries │ ├── cmd/ # dms CLI and dankinstall binaries
│ ├── internal/ # System integration, IPC, distro support │ ├── internal/ # System integration, IPC, distro support
│ └── pkg/ # Shared packages │ └── pkg/ # Shared packages
├── distro/ # Distribution packaging (Fedora RPM specs) ├── distro/ # Distribution packaging
├── nix/ # NixOS/home-manager modules │ ├── fedora/ # Fedora RPM specs
│ ├── debian/ # Debian packaging
│ └── nix/ # NixOS/home-manager modules
└── flake.nix # Nix flake for declarative installation └── flake.nix # Nix flake for declarative installation
``` ```
@@ -103,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), and [MangoWC](https://github.com/DreamMaoMao/mangowc) 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), and [labwc](https://labwc.github.io/) 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) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
@@ -136,8 +138,7 @@ See component-specific documentation:
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules - **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
- **[core/](core/)** - Go backend, CLI tools, and system integration - **[core/](core/)** - Go backend, CLI tools, and system integration
- **[distro/](distro/)** - Distribution packaging - **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
- **[nix/](nix/)** - NixOS and home-manager modules
### Building from Source ### Building from Source
@@ -182,6 +183,10 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration - [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration - [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
## License ## License
MIT License - See [LICENSE](LICENSE) for details. MIT License - See [LICENSE](LICENSE) for details.

77
core/.golangci.yml Normal file
View File

@@ -0,0 +1,77 @@
linters-settings:
errcheck:
check-type-assertions: false
check-blank: false
exclude-functions:
# Cleanup/destroy operations
- (io.Closer).Close
- (*os.File).Close
- (net.Conn).Close
- (*net.Conn).Close
# Signal handling
- (*os.Process).Signal
- (*os.Process).Kill
# DBus cleanup
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
# Encoding to network connections (if conn is bad, nothing we can do)
- (*encoding/json.Encoder).Encode
- (net.Conn).Write
# Command execution where failure is expected/ignored
- (*os/exec.Cmd).Run
- (*os/exec.Cmd).Start
# Flush operations
- (*bufio.Writer).Flush
# Scanning user input
- fmt.Scanln
- fmt.Scanf
# Parse operations where default value is acceptable
- fmt.Sscanf
# Flag operations
- (*github.com/spf13/pflag.FlagSet).MarkHidden
# Binary encoding to buffer (can't fail for basic types)
- binary.Write
# File operations in cleanup paths
- os.Rename
- os.Remove
- (*os.File).WriteString
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
- gosimple
# Exclude cleanup/teardown method calls from errcheck
- linters:
- errcheck
text: "Error return value of `.+\\.(Destroy|Release|Stop|Close|Roundtrip|Store)` is not checked"
# Exclude internal state update methods that are best-effort
- linters:
- errcheck
text: "Error return value of `[mb]\\.\\w*(update|initialize|recreate|acquire|enumerate|list|List|Ensure|refresh|Lock)\\w*` is not checked"
# Exclude SetMode on wayland power controls (best-effort)
- linters:
- errcheck
text: "Error return value of `.+\\.SetMode` is not checked"
# Exclude AddMatchSignal which is best-effort monitoring setup
- linters:
- errcheck
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
- linters:
- errcheck
- ineffassign
path: pkg/go-wayland/
# Exclude proto pkg from ineffassign (generated protocol code)
- linters:
- ineffassign
path: internal/proto/
# binary.Write to bytes.Buffer can't fail
- linters:
- errcheck
text: "Error return value of `binary\\.Write` is not checked"

58
core/.mockery.yml Normal file
View File

@@ -0,0 +1,58 @@
with-expecter: true
dir: "internal/mocks/{{.InterfaceDirRelative}}"
mockname: "Mock{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
packages:
github.com/Wifx/gonetworkmanager/v2:
interfaces:
NetworkManager:
Device:
DeviceWireless:
AccessPoint:
Connection:
Settings:
ActiveConnection:
IP4Config:
net:
interfaces:
Conn:
github.com/AvengeMedia/danklinux/internal/plugins:
interfaces:
GitClient:
github.com/godbus/dbus/v5:
interfaces:
BusObject:
github.com/AvengeMedia/danklinux/internal/server/brightness:
config:
dir: "internal/mocks/brightness"
outpkg: mocks_brightness
interfaces:
DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"
outpkg: mocks_network
interfaces:
Backend:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
config:
dir: "internal/mocks/cups"
outpkg: mocks_cups
interfaces:
CUPSClientInterface:
PkHelper:
config:
dir: "internal/mocks/cups_pkhelper"
outpkg: mocks_cups_pkhelper
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
config:
dir: "internal/mocks/evdev"
outpkg: mocks_evdev
interfaces:
EvdevDevice:
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
config:
dir: "internal/mocks/version"
outpkg: mocks_version
interfaces:
VersionFetcher:

View File

@@ -31,6 +31,7 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
- DDC/CI protocol - External monitor brightness control (like `ddcutil`) - DDC/CI protocol - External monitor brightness control (like `ddcutil`)
- Backlight control - Internal display brightness via `login1` or sysfs - Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management - LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
**Plugin System** **Plugin System**
- Plugin registry integration - Plugin registry integration
@@ -71,6 +72,13 @@ sudo make install # Install to /usr/local/bin/dms
## Development ## Development
**Setup pre-commit hooks:**
```bash
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:** **Regenerating Wayland Protocol Bindings:**
```bash ```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/logger" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui" "github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -12,7 +12,7 @@ import (
var Version = "dev" var Version = "dev"
func main() { func main() {
fileLogger, err := logger.NewFileLogger() fileLogger, err := log.NewFileLogger()
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err) fmt.Printf("Warning: Failed to create log file: %v\n", err)
fmt.Println("Continuing without file logging...") fmt.Println("Continuing without file logging...")

View File

@@ -140,6 +140,7 @@ func runVersion(cmd *cobra.Command, args []string) {
} }
func startDebugServer() error { func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true) return server.Start(true)
} }
@@ -368,6 +369,7 @@ func getCommonCommands() []*cobra.Command {
pluginsCmd, pluginsCmd,
dank16Cmd, dank16Cmd,
brightnessCmd, brightnessCmd,
dpmsCmd,
keybindsCmd, keybindsCmd,
greeterCmd, greeterCmd,
setupCmd, setupCmd,

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
@@ -26,7 +25,6 @@ func init() {
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format") dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format") dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format") dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
dank16Cmd.Flags().String("vscode-enrich", "", "Enrich existing VSCode theme file with terminal colors")
dank16Cmd.Flags().String("background", "", "Custom background color") dank16Cmd.Flags().String("background", "", "Custom background color")
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag") dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
} }
@@ -44,7 +42,6 @@ func runDank16(cmd *cobra.Command, args []string) {
isAlacritty, _ := cmd.Flags().GetBool("alacritty") isAlacritty, _ := cmd.Flags().GetBool("alacritty")
isGhostty, _ := cmd.Flags().GetBool("ghostty") isGhostty, _ := cmd.Flags().GetBool("ghostty")
isWezterm, _ := cmd.Flags().GetBool("wezterm") isWezterm, _ := cmd.Flags().GetBool("wezterm")
vscodeEnrich, _ := cmd.Flags().GetString("vscode-enrich")
background, _ := cmd.Flags().GetString("background") background, _ := cmd.Flags().GetString("background")
contrastAlgo, _ := cmd.Flags().GetString("contrast") contrastAlgo, _ := cmd.Flags().GetString("contrast")
@@ -65,18 +62,7 @@ func runDank16(cmd *cobra.Command, args []string) {
colors := dank16.GeneratePalette(primaryColor, opts) colors := dank16.GeneratePalette(primaryColor, opts)
if vscodeEnrich != "" { if isJson {
data, err := os.ReadFile(vscodeEnrich)
if err != nil {
log.Fatalf("Error reading file: %v", err)
}
enriched, err := dank16.EnrichVSCodeTheme(data, colors)
if err != nil {
log.Fatalf("Error enriching theme: %v", err)
}
fmt.Println(string(enriched))
} else if isJson {
fmt.Print(dank16.GenerateJSON(colors)) fmt.Print(dank16.GenerateJSON(colors))
} else if isKitty { } else if isKitty {
fmt.Print(dank16.GenerateKittyTheme(colors)) fmt.Print(dank16.GenerateKittyTheme(colors))

View File

@@ -0,0 +1,84 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var dpmsCmd = &cobra.Command{
Use: "dpms",
Short: "Control display power management",
}
var dpmsOnCmd = &cobra.Command{
Use: "on [output]",
Short: "Turn display(s) on",
Args: cobra.MaximumNArgs(1),
Run: runDPMSOn,
}
var dpmsOffCmd = &cobra.Command{
Use: "off [output]",
Short: "Turn display(s) off",
Args: cobra.MaximumNArgs(1),
Run: runDPMSOff,
}
var dpmsListCmd = &cobra.Command{
Use: "list",
Short: "List outputs",
Args: cobra.NoArgs,
Run: runDPMSList,
}
func init() {
dpmsCmd.AddCommand(dpmsOnCmd, dpmsOffCmd, dpmsListCmd)
}
func runDPMSOn(cmd *cobra.Command, args []string) {
outputName := ""
if len(args) > 0 {
outputName = args[0]
}
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
if err := client.SetDPMS(outputName, true); err != nil {
log.Fatalf("%v", err)
}
}
func runDPMSOff(cmd *cobra.Command, args []string) {
outputName := ""
if len(args) > 0 {
outputName = args[0]
}
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
if err := client.SetDPMS(outputName, false); err != nil {
log.Fatalf("%v", err)
}
}
func runDPMSList(cmd *cobra.Command, args []string) {
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
for _, output := range client.ListOutputs() {
fmt.Println(output)
}
}

View File

@@ -77,8 +77,6 @@ func runUpdate() {
switch config.Family { switch config.Family {
case distros.FamilyArch: case distros.FamilyArch:
updateErr = updateArchLinux() updateErr = updateArchLinux()
case distros.FamilyNix:
updateErr = updateNixOS()
case distros.FamilySUSE: case distros.FamilySUSE:
updateErr = updateOtherDistros() updateErr = updateOtherDistros()
default: default:
@@ -152,27 +150,6 @@ func updateArchLinux() error {
return nil return nil
} }
func updateNixOS() error {
fmt.Println("This will update DankMaterialShell using nix profile.")
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Println("\nRunning: nix profile upgrade github:AvengeMedia/DankMaterialShell")
updateCmd := exec.Command("nix", "profile", "upgrade", "github:AvengeMedia/DankMaterialShell")
updateCmd.Stdout = os.Stdout
updateCmd.Stderr = os.Stderr
err := updateCmd.Run()
if err != nil {
fmt.Printf("Error: Failed to update using nix profile: %v\n", err)
fmt.Println("Falling back to git-based update method...")
return updateOtherDistros()
}
fmt.Println("dms successfully updated")
return nil
}
func updateOtherDistros() error { func updateOtherDistros() error {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {

View File

@@ -11,6 +11,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
var greeterCmd = &cobra.Command{ var greeterCmd = &cobra.Command{
@@ -217,6 +219,191 @@ func checkGroupExists(groupName string) bool {
return false return false
} }
func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName)
if err != nil {
return false, fmt.Errorf("failed to check %s state: %w", dmName, err)
}
if !state.Exists {
return false, nil
}
fmt.Printf("\nChecking %s...\n", dmName)
fmt.Printf(" Current state: enabled=%s\n", state.EnabledState)
actionTaken := false
if state.NeedsDisable {
var disableCmd *exec.Cmd
var actionVerb string
if state.EnabledState == "static" {
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
actionVerb = "masked"
} else {
fmt.Printf(" Disabling %s...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
actionVerb = "disabled"
}
disableCmd.Stdout = os.Stdout
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
}
enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName)
if verifyErr != nil {
fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr)
} else if shouldDisable {
return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb)
} else {
fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState)
}
actionTaken = true
} else {
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Printf(" ✓ %s is already masked\n", dmName)
} else {
fmt.Printf(" ✓ %s is already disabled\n", dmName)
}
}
return actionTaken, nil
}
func ensureGreetdEnabled() error {
fmt.Println("\nChecking greetd service status...")
state, err := getSystemdServiceState("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
}
if !state.Exists {
return fmt.Errorf("greetd service not found. Please install greetd first")
}
fmt.Printf(" Current state: %s\n", state.EnabledState)
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Println(" Unmasking greetd...")
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
unmaskCmd.Stdout = os.Stdout
unmaskCmd.Stderr = os.Stderr
if err := unmaskCmd.Run(); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err)
}
fmt.Println(" ✓ Unmasked greetd")
}
switch state.EnabledState {
case "disabled", "masked", "masked-runtime":
fmt.Println(" Enabling greetd service...")
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
fmt.Println(" ✓ Enabled greetd service")
case "enabled", "enabled-runtime":
fmt.Println(" ✓ greetd is already enabled")
default:
fmt.Printf(" greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
}
return nil
}
func ensureGraphicalTarget() error {
getDefaultCmd := exec.Command("systemctl", "get-default")
currentTarget, err := getDefaultCmd.Output()
if err != nil {
fmt.Println("⚠ Warning: Could not detect current default systemd target")
return nil
}
currentTargetStr := strings.TrimSpace(string(currentTarget))
if currentTargetStr != "graphical.target" {
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
setDefaultCmd.Stdout = os.Stdout
setDefaultCmd.Stderr = os.Stderr
if err := setDefaultCmd.Run(); err != nil {
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
fmt.Println(" Greeter may not start on boot. Run manually:")
fmt.Println(" sudo systemctl set-default graphical.target")
return nil
}
fmt.Println("✓ Set graphical.target as default")
} else {
fmt.Println("✓ Default target already set to graphical.target")
}
return nil
}
func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
disabledAny := false
var errors []string
for _, dm := range conflictingDMs {
actionTaken, err := disableDisplayManager(dm)
if err != nil {
errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err)
errors = append(errors, errMsg)
fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg)
continue
}
if actionTaken {
disabledAny = true
}
}
if len(errors) > 0 {
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║")
fmt.Println("╚════════════════════════════════════════════════════════════╝")
fmt.Println("\nSome display managers could not be disabled:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
fmt.Println("\nThis may prevent greetd from starting properly.")
fmt.Println("You may need to manually disable them before greetd will work.")
fmt.Println("\nManual commands to try:")
for _, dm := range conflictingDMs {
fmt.Printf(" sudo systemctl disable %s\n", dm)
fmt.Printf(" sudo systemctl mask %s\n", dm)
}
fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response == "n" || response == "no" {
return fmt.Errorf("aborted due to display manager conflicts")
}
fmt.Println("\nContinuing despite errors...")
}
if !disabledAny && len(errors) == 0 {
fmt.Println("\n✓ No conflicting display managers found")
} else if disabledAny && len(errors) == 0 {
fmt.Println("\n✓ Successfully handled all conflicting display managers")
}
return nil
}
func enableGreeter() error { func enableGreeter() error {
fmt.Println("=== DMS Greeter Enable ===") fmt.Println("=== DMS Greeter Enable ===")
fmt.Println() fmt.Println()
@@ -232,8 +419,29 @@ func enableGreeter() error {
} }
configContent := string(data) configContent := string(data)
if strings.Contains(configContent, "dms-greeter") { configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
if configAlreadyCorrect {
fmt.Println("✓ Greeter is already configured with dms-greeter") fmt.Println("✓ Greeter is already configured with dms-greeter")
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nGreeter configuration verified and system state corrected.")
fmt.Println("To start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil return nil
} }
@@ -322,11 +530,23 @@ func enableGreeter() error {
} }
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor) fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===") fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nTo start the greeter, run:") fmt.Println("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd") fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nTo enable on boot, run:") fmt.Println("\nOr reboot to see the greeter at boot time.")
fmt.Println(" sudo systemctl enable --now greetd")
return nil return nil
} }

View File

@@ -34,9 +34,7 @@ var keybindsShowCmd = &cobra.Command{
} }
func init() { func init() {
keybindsShowCmd.Flags().String("hyprland-path", "$HOME/.config/hypr", "Path to Hyprland config directory") keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsShowCmd.Flags().String("mangowc-path", "$HOME/.config/mango", "Path to MangoWC config directory")
keybindsShowCmd.Flags().String("sway-path", "$HOME/.config/sway", "Path to Sway config directory")
keybindsCmd.AddCommand(keybindsListCmd) keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd) keybindsCmd.AddCommand(keybindsShowCmd)
@@ -89,25 +87,34 @@ func runKeybindsList(cmd *cobra.Command, args []string) {
func runKeybindsShow(cmd *cobra.Command, args []string) { func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0] providerName := args[0]
registry := keybinds.GetDefaultRegistry() registry := keybinds.GetDefaultRegistry()
if providerName == "hyprland" { customPath, _ := cmd.Flags().GetString("path")
hyprlandPath, _ := cmd.Flags().GetString("hyprland-path") if customPath != "" {
hyprlandProvider := providers.NewHyprlandProvider(hyprlandPath) var provider keybinds.Provider
registry.Register(hyprlandProvider) switch providerName {
case "hyprland":
provider = providers.NewHyprlandProvider(customPath)
case "mangowc":
provider = providers.NewMangoWCProvider(customPath)
case "sway":
provider = providers.NewSwayProvider(customPath)
default:
log.Fatalf("Provider %s does not support custom path", providerName)
} }
if providerName == "mangowc" { sheet, err := provider.GetCheatSheet()
mangowcPath, _ := cmd.Flags().GetString("mangowc-path") if err != nil {
mangowcProvider := providers.NewMangoWCProvider(mangowcPath) log.Fatalf("Error getting cheatsheet: %v", err)
registry.Register(mangowcProvider)
} }
if providerName == "sway" { output, err := json.MarshalIndent(sheet, "", " ")
swayPath, _ := cmd.Flags().GetString("sway-path") if err != nil {
swayProvider := providers.NewSwayProvider(swayPath) log.Fatalf("Error generating JSON: %v", err)
registry.Register(swayProvider) }
fmt.Fprintln(os.Stdout, string(output))
return
} }
provider, err := registry.Get(providerName) provider, err := registry.Get(providerName)

View File

@@ -1,14 +1,12 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms" "github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -76,14 +74,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func runInteractiveMode(cmd *cobra.Command, args []string) { func runInteractiveMode(cmd *cobra.Command, args []string) {
detector, err := dms.NewDetector() detector, _ := dms.NewDetector()
if err != nil && !errors.Is(err, &distros.UnsupportedDistributionError{}) {
log.Fatalf("Error initializing DMS detector: %v", err)
} else if errors.Is(err, &distros.UnsupportedDistributionError{}) {
log.Error("Interactive mode is not supported on this distribution.")
log.Info("Please run 'dms --help' for available commands.")
os.Exit(1)
}
if !detector.IsDMSInstalled() { if !detector.IsDMSInstalled() {
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.") log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")

345
core/cmd/dms/dpms_client.go Normal file
View File

@@ -0,0 +1,345 @@
package main
import (
"fmt"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type cmd struct {
fn func()
done chan error
}
type dpmsClient struct {
display *wlclient.Display
ctx *wlclient.Context
powerMgr *wlr_output_power.ZwlrOutputPowerManagerV1
outputs map[string]*outputState
mu sync.Mutex
syncRound int
done bool
err error
cmdq chan cmd
stopChan chan struct{}
wg sync.WaitGroup
}
type outputState struct {
wlOutput *wlclient.Output
powerCtrl *wlr_output_power.ZwlrOutputPowerV1
name string
mode uint32
failed bool
waitCh chan struct{}
wantMode *uint32
}
func (c *dpmsClient) post(fn func()) {
done := make(chan error, 1)
select {
case c.cmdq <- cmd{fn: fn, done: done}:
<-done
case <-c.stopChan:
}
}
func (c *dpmsClient) waylandActor() {
defer c.wg.Done()
for {
select {
case <-c.stopChan:
return
case cmd := <-c.cmdq:
cmd.fn()
close(cmd.done)
}
}
}
func newDPMSClient() (*dpmsClient, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
}
c := &dpmsClient{
display: display,
ctx: display.Context(),
outputs: make(map[string]*outputState),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
}
c.wg.Add(1)
go c.waylandActor()
registry, err := display.GetRegistry()
if err != nil {
display.Context().Close()
return nil, fmt.Errorf("failed to get registry: %w", err)
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
version := e.Version
if version > 1 {
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
}
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := fmt.Sprintf("output-%d", output.ID())
state := &outputState{
wlOutput: output,
name: outputID,
}
c.mu.Lock()
c.outputs[outputID] = state
c.mu.Unlock()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
c.mu.Lock()
delete(c.outputs, state.name)
state.name = ev.Name
c.outputs[ev.Name] = state
c.mu.Unlock()
})
}
}
})
syncCallback, err := display.Sync()
if err != nil {
c.Close()
return nil, fmt.Errorf("failed to sync display: %w", err)
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
c.handleSync()
})
for !c.done {
if err := c.ctx.Dispatch(); err != nil {
c.Close()
return nil, fmt.Errorf("dispatch error: %w", err)
}
}
if c.err != nil {
c.Close()
return nil, c.err
}
return c, nil
}
func (c *dpmsClient) handleSync() {
c.syncRound++
switch c.syncRound {
case 1:
if c.powerMgr == nil {
c.err = fmt.Errorf("wlr-output-power-management protocol not supported by compositor")
c.done = true
return
}
c.mu.Lock()
for _, state := range c.outputs {
powerCtrl, err := c.powerMgr.GetOutputPower(state.wlOutput)
if err != nil {
continue
}
state.powerCtrl = powerCtrl
powerCtrl.SetModeHandler(func(e wlr_output_power.ZwlrOutputPowerV1ModeEvent) {
c.mu.Lock()
defer c.mu.Unlock()
if state.powerCtrl == nil {
return
}
state.mode = e.Mode
if state.wantMode != nil && e.Mode == *state.wantMode && state.waitCh != nil {
close(state.waitCh)
state.wantMode = nil
}
})
powerCtrl.SetFailedHandler(func(e wlr_output_power.ZwlrOutputPowerV1FailedEvent) {
c.mu.Lock()
defer c.mu.Unlock()
if state.powerCtrl == nil {
return
}
state.failed = true
if state.waitCh != nil {
close(state.waitCh)
state.wantMode = nil
}
})
}
c.mu.Unlock()
syncCallback, err := c.display.Sync()
if err != nil {
c.err = fmt.Errorf("failed to sync display: %w", err)
c.done = true
return
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
c.handleSync()
})
default:
c.done = true
}
}
func (c *dpmsClient) ListOutputs() []string {
c.mu.Lock()
defer c.mu.Unlock()
names := make([]string, 0, len(c.outputs))
for name := range c.outputs {
names = append(names, name)
}
return names
}
func (c *dpmsClient) SetDPMS(outputName string, on bool) error {
var mode uint32
if on {
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOn)
} else {
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOff)
}
var setErr error
c.post(func() {
c.mu.Lock()
var waitStates []*outputState
if outputName == "" || outputName == "all" {
if len(c.outputs) == 0 {
c.mu.Unlock()
setErr = fmt.Errorf("no outputs found")
return
}
for _, state := range c.outputs {
if state.powerCtrl == nil {
continue
}
state.wantMode = &mode
state.waitCh = make(chan struct{})
state.failed = false
waitStates = append(waitStates, state)
state.powerCtrl.SetMode(mode)
}
} else {
state, ok := c.outputs[outputName]
if !ok {
c.mu.Unlock()
setErr = fmt.Errorf("output not found: %s", outputName)
return
}
if state.powerCtrl == nil {
c.mu.Unlock()
setErr = fmt.Errorf("output %s has nil powerCtrl", outputName)
return
}
state.wantMode = &mode
state.waitCh = make(chan struct{})
state.failed = false
waitStates = append(waitStates, state)
state.powerCtrl.SetMode(mode)
}
c.mu.Unlock()
deadline := time.Now().Add(10 * time.Second)
for _, state := range waitStates {
c.mu.Lock()
ch := state.waitCh
c.mu.Unlock()
done := false
for !done {
if err := c.ctx.Dispatch(); err != nil {
setErr = fmt.Errorf("dispatch error: %w", err)
return
}
select {
case <-ch:
c.mu.Lock()
if state.failed {
setErr = fmt.Errorf("compositor reported failed for %s", state.name)
c.mu.Unlock()
return
}
c.mu.Unlock()
done = true
default:
if time.Now().After(deadline) {
setErr = fmt.Errorf("timeout waiting for mode change on %s", state.name)
return
}
time.Sleep(10 * time.Millisecond)
}
}
}
c.mu.Lock()
for _, state := range waitStates {
if state.powerCtrl != nil {
state.powerCtrl.Destroy()
state.powerCtrl = nil
}
}
c.mu.Unlock()
c.display.Roundtrip()
})
return setErr
}
func (c *dpmsClient) Close() {
close(c.stopChan)
c.wg.Wait()
c.mu.Lock()
defer c.mu.Unlock()
for _, state := range c.outputs {
if state.powerCtrl != nil {
state.powerCtrl.Destroy()
}
}
c.outputs = nil
if c.powerMgr != nil {
c.powerMgr.Destroy()
c.powerMgr = nil
}
if c.display != nil {
c.ctx.Close()
c.display = nil
}
}

View File

@@ -57,6 +57,11 @@ func getRuntimeDir() string {
return os.TempDir() return os.TempDir()
} }
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string { func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid())) return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
} }
@@ -165,6 +170,10 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules) cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
} }
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) { if !strings.HasPrefix(configPath, homeDir) {
@@ -374,6 +383,7 @@ func runShellDaemon(session bool) {
errChan <- fmt.Errorf("server panic: %v", r) errChan <- fmt.Errorf("server panic: %v", r)
} }
}() }()
server.CLIVersion = Version
if err := server.Start(false); err != nil { if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err) errChan <- fmt.Errorf("server error: %w", err)
} }
@@ -387,6 +397,10 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules) cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
} }
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) { if !strings.HasPrefix(configPath, homeDir) {

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strings"
) )
func commandExists(cmd string) bool { func commandExists(cmd string) bool {
@@ -24,3 +25,68 @@ func isArchPackageInstalled(packageName string) bool {
err := cmd.Run() err := cmd.Run()
return err == nil return err == nil
} }
type systemdServiceState struct {
Name string
EnabledState string
NeedsDisable bool
Exists bool
}
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
cmd := exec.Command("systemctl", "is-enabled", serviceName)
output, err := cmd.Output()
stateStr := strings.TrimSpace(string(output))
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
}
}
if !isKnownState {
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
}
}
shouldDisable := false
switch stateStr {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
shouldDisable = true
case "disabled", "masked", "masked-runtime", "not-found":
shouldDisable = false
default:
shouldDisable = true
}
return stateStr, shouldDisable, nil
}
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
state := &systemdServiceState{
Name: serviceName,
Exists: false,
}
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
if err != nil {
return nil, fmt.Errorf("failed to check enabled state: %w", err)
}
state.EnabledState = enabledState
state.NeedsDisable = needsDisable
if enabledState == "not-found" {
state.Exists = false
return state, nil
}
state.Exists = true
return state, nil
}

View File

@@ -5,61 +5,64 @@ go 1.24.6
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
github.com/godbus/dbus/v5 v5.1.0 github.com/fsnotify/fsnotify v1.9.0
github.com/spf13/cobra v1.9.1 github.com/godbus/dbus/v5 v5.2.0
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // 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/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.42.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.47.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/ansi v0.11.2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.38.0
golang.org/x/sys v0.36.0 golang.org/x/text v0.31.0
golang.org/x/text v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -14,27 +14,39 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= 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/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 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 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 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
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.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
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 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -44,23 +56,35 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA= github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM= github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
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 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-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd h1:30HEd5KKVM7GgMJ1GSNuYxuZXEg8Pdlngp6T51faxoc= github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd/go.mod h1:lz8PQr/p79XpFq5ODVBwRJu5LnOF8Et7j95ehqmCMJU= github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
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 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 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/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/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
@@ -79,8 +103,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -91,7 +115,6 @@ github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -101,36 +124,37 @@ 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/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 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6/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=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 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/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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 h1:iTAt1me6SBYsuzrl/CmrxtATPlOG/pVviosM3DhUdKE= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34/go.mod h1:jzmUN5lUAv2O8e63OvcauV4S30rIZ1BvF/PNYE37vDo= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -125,6 +125,8 @@ windowrulev2 = noborder, class:^(kitty)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$ windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$ windowrulev2 = float, class:^(zoom)$
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0 windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
layerrule = noanim, ^(quickshell)$ layerrule = noanim, ^(quickshell)$

View File

@@ -218,6 +218,11 @@ window-rule {
geometry-corner-radius 12 geometry-corner-radius 12
clip-to-geometry true clip-to-geometry true
} }
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
open-floating true
}
binds { binds {
// === System & Overview === // === System & Overview ===
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; } Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }

Binary file not shown.

View File

@@ -0,0 +1,6 @@
package config
import _ "embed"
//go:embed embedded/testpage.pdf
var TestPage string

View File

@@ -15,6 +15,48 @@ type HSV struct {
H, S, V float64 H, S, V float64
} }
type ColorInfo struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
}
type Palette struct {
Color0 ColorInfo `json:"color0"`
Color1 ColorInfo `json:"color1"`
Color2 ColorInfo `json:"color2"`
Color3 ColorInfo `json:"color3"`
Color4 ColorInfo `json:"color4"`
Color5 ColorInfo `json:"color5"`
Color6 ColorInfo `json:"color6"`
Color7 ColorInfo `json:"color7"`
Color8 ColorInfo `json:"color8"`
Color9 ColorInfo `json:"color9"`
Color10 ColorInfo `json:"color10"`
Color11 ColorInfo `json:"color11"`
Color12 ColorInfo `json:"color12"`
Color13 ColorInfo `json:"color13"`
Color14 ColorInfo `json:"color14"`
Color15 ColorInfo `json:"color15"`
}
func NewColorInfo(hex string) ColorInfo {
rgb := HexToRGB(hex)
stripped := hex
if len(hex) > 0 && hex[0] == '#' {
stripped = hex[1:]
}
return ColorInfo{
Hex: hex,
HexStripped: stripped,
R: int(math.Round(rgb.R * 255)),
G: int(math.Round(rgb.G * 255)),
B: int(math.Round(rgb.B * 255)),
}
}
func HexToRGB(hex string) RGB { func HexToRGB(hex string) RGB {
if hex[0] == '#' { if hex[0] == '#' {
hex = hex[1:] hex = hex[1:]
@@ -310,13 +352,13 @@ func DeriveContainer(primary string, isLight bool) string {
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV})) return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
} }
func GeneratePalette(primaryColor string, opts PaletteOptions) []string { func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
baseColor := DeriveContainer(primaryColor, opts.IsLight) baseColor := DeriveContainer(primaryColor, opts.IsLight)
rgb := HexToRGB(baseColor) rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb) hsv := RGBToHSV(rgb)
palette := make([]string, 0, 16) var palette Palette
var normalTextTarget, secondaryTarget float64 var normalTextTarget, secondaryTarget float64
if opts.UseDPS { if opts.UseDPS {
@@ -335,7 +377,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
} else { } else {
bgColor = "#1a1a1a" bgColor = "#1a1a1a"
} }
palette = append(palette, bgColor) palette.Color0 = NewColorInfo(bgColor)
hueShift := (hsv.H - 0.6) * 0.12 hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15 satBoost := 1.15
@@ -344,39 +386,39 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
var redColor string var redColor string
if opts.IsLight { if opts.IsLight {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55})) redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts)) palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} else { } else {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80})) redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts)) palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} }
greenH := math.Mod(0.33+hueShift+1.0, 1.0) greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string var greenColor string
if opts.IsLight { 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})) greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts)) palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else { } else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84})) greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts)) palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} }
yellowH := math.Mod(0.15+hueShift+1.0, 1.0) yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string var yellowColor string
if opts.IsLight { if opts.IsLight {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50})) yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts)) palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} else { } else {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86})) yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts)) palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} }
var blueColor string var blueColor string
if opts.IsLight { if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1})) blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts)) palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else { } 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)})) 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 = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts)) palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} }
magH := hsv.H - 0.03 magH := hsv.H - 0.03
@@ -388,65 +430,64 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
hh := RGBToHSV(hr) hh := RGBToHSV(hr)
if opts.IsLight { if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85})) magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts)) palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else { } else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75})) magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts)) palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} }
cyanH := hsv.H + 0.08 cyanH := hsv.H + 0.08
if cyanH > 1.0 { if cyanH > 1.0 {
cyanH -= 1.0 cyanH -= 1.0
} }
palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts)) palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
if opts.IsLight { if opts.IsLight {
palette = append(palette, "#1a1a1a") palette.Color7 = NewColorInfo("#1a1a1a")
palette = append(palette, "#2e2e2e") palette.Color8 = NewColorInfo("#2e2e2e")
} else { } else {
palette = append(palette, "#abb2bf") palette.Color7 = NewColorInfo("#abb2bf")
palette = append(palette, "#5c6370") palette.Color8 = NewColorInfo("#5c6370")
} }
if opts.IsLight { if opts.IsLight {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65})) brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts)) 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})) brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts)) 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})) brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts)) palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor) hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr) 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)})) 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 = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts)) 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)})) 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 = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts)) 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)})) 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 = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts)) palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else { } else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88})) brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts)) 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})) brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts)) 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})) brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts)) palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
// Make it way brighter for type names in dark mode
brightBlue := retoneToL(primaryColor, 85.0) brightBlue := retoneToL(primaryColor, 85.0)
palette = append(palette, brightBlue) 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)})) 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 = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts)) palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02 brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 { if brightCyanH > 1.0 {
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)})) 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 = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts)) palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} }
if opts.IsLight { if opts.IsLight {
palette = append(palette, "#1a1a1a") palette.Color15 = NewColorInfo("#1a1a1a")
} else { } else {
palette = append(palette, "#ffffff") palette.Color15 = NewColorInfo("#ffffff")
} }
return palette return palette

View File

@@ -1,7 +1,6 @@
package dank16 package dank16
import ( import (
"encoding/json"
"math" "math"
"testing" "testing"
) )
@@ -346,106 +345,36 @@ func TestGeneratePalette(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts) result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 { colors := []ColorInfo{
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result)) result.Color0, result.Color1, result.Color2, result.Color3,
result.Color4, result.Color5, result.Color6, result.Color7,
result.Color8, result.Color9, result.Color10, result.Color11,
result.Color12, result.Color13, result.Color14, result.Color15,
} }
for i, color := range result { for i, color := range colors {
if len(color) != 7 || color[0] != '#' { if len(color.Hex) != 7 || color.Hex[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color) t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
} }
} }
if tt.opts.Background != "" && result[0] != tt.opts.Background { if tt.opts.Background != "" && result.Color0.Hex != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background) t.Errorf("Background color = %s, expected %s", result.Color0.Hex, tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" { } else if !tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0]) t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex)
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" { } else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0]) t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
} }
if tt.opts.IsLight && result[15] != "#1a1a1a" { if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15]) t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
} else if !tt.opts.IsLight && result[15] != "#ffffff" { } else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15]) t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
} }
}) })
} }
} }
func TestEnrichVSCodeTheme(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
baseTheme := map[string]interface{}{
"name": "Test Theme",
"type": "dark",
"colors": map[string]interface{}{
"editor.background": "#000000",
},
}
themeJSON, err := json.Marshal(baseTheme)
if err != nil {
t.Fatalf("Failed to marshal base theme: %v", err)
}
result, err := EnrichVSCodeTheme(themeJSON, colors)
if err != nil {
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
}
var enriched map[string]interface{}
if err := json.Unmarshal(result, &enriched); err != nil {
t.Fatalf("Failed to unmarshal result: %v", err)
}
colorsMap, ok := enriched["colors"].(map[string]interface{})
if !ok {
t.Fatal("colors is not a map")
}
terminalColors := []string{
"terminal.ansiBlack",
"terminal.ansiRed",
"terminal.ansiGreen",
"terminal.ansiYellow",
"terminal.ansiBlue",
"terminal.ansiMagenta",
"terminal.ansiCyan",
"terminal.ansiWhite",
"terminal.ansiBrightBlack",
"terminal.ansiBrightRed",
"terminal.ansiBrightGreen",
"terminal.ansiBrightYellow",
"terminal.ansiBrightBlue",
"terminal.ansiBrightMagenta",
"terminal.ansiBrightCyan",
"terminal.ansiBrightWhite",
}
for i, key := range terminalColors {
if val, ok := colorsMap[key]; !ok {
t.Errorf("Missing terminal color: %s", key)
} else if val != colors[i] {
t.Errorf("%s = %s, expected %s", key, val, colors[i])
}
}
if colorsMap["editor.background"] != "#000000" {
t.Error("Original theme colors should be preserved")
}
}
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
invalidJSON := []byte("{invalid json")
_, err := EnrichVSCodeTheme(invalidJSON, colors)
if err == nil {
t.Error("Expected error for invalid JSON, got nil")
}
}
func TestRoundTripConversion(t *testing.T) { func TestRoundTripConversion(t *testing.T) {
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"} testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
@@ -635,23 +564,26 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts) result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 { colors := []ColorInfo{
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result)) result.Color0, result.Color1, result.Color2, result.Color3,
result.Color4, result.Color5, result.Color6, result.Color7,
result.Color8, result.Color9, result.Color10, result.Color11,
result.Color12, result.Color13, result.Color14, result.Color15,
} }
for i, color := range result { for i, color := range colors {
if len(color) != 7 || color[0] != '#' { if len(color.Hex) != 7 || color.Hex[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color) t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
} }
} }
bgColor := result[0] bgColor := result.Color0.Hex
for i := 1; i < 8; i++ { for i := 1; i < 8; i++ {
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight) lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
minLc := 30.0 minLc := 30.0
if lc < minLc && lc > 0 { if lc < minLc && lc > 0 {
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)", t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
i, result[i], lc, bgColor, minLc) i, colors[i].Hex, lc, bgColor, minLc)
} }
} }
}) })
@@ -708,17 +640,26 @@ func TestContrastAlgorithmComparison(t *testing.T) {
paletteWCAG := GeneratePalette(base, optsWCAG) paletteWCAG := GeneratePalette(base, optsWCAG)
paletteDPS := GeneratePalette(base, optsDPS) paletteDPS := GeneratePalette(base, optsDPS)
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 { wcagColors := []ColorInfo{
t.Fatal("Both palettes should have 16 colors") paletteWCAG.Color0, paletteWCAG.Color1, paletteWCAG.Color2, paletteWCAG.Color3,
paletteWCAG.Color4, paletteWCAG.Color5, paletteWCAG.Color6, paletteWCAG.Color7,
paletteWCAG.Color8, paletteWCAG.Color9, paletteWCAG.Color10, paletteWCAG.Color11,
paletteWCAG.Color12, paletteWCAG.Color13, paletteWCAG.Color14, paletteWCAG.Color15,
}
dpsColors := []ColorInfo{
paletteDPS.Color0, paletteDPS.Color1, paletteDPS.Color2, paletteDPS.Color3,
paletteDPS.Color4, paletteDPS.Color5, paletteDPS.Color6, paletteDPS.Color7,
paletteDPS.Color8, paletteDPS.Color9, paletteDPS.Color10, paletteDPS.Color11,
paletteDPS.Color12, paletteDPS.Color13, paletteDPS.Color14, paletteDPS.Color15,
} }
if paletteWCAG[0] != paletteDPS[0] { if paletteWCAG.Color0.Hex != paletteDPS.Color0.Hex {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0]) t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG.Color0.Hex, paletteDPS.Color0.Hex)
} }
differentCount := 0 differentCount := 0
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
if paletteWCAG[i] != paletteDPS[i] { if wcagColors[i].Hex != dpsColors[i].Hex {
differentCount++ differentCount++
} }
} }

View File

@@ -6,135 +6,104 @@ import (
"strings" "strings"
) )
func GenerateJSON(colors []string) string { func GenerateJSON(p Palette) string {
colorMap := make(map[string]string) marshalled, _ := json.Marshal(p)
for i, color := range colors {
colorMap[fmt.Sprintf("color%d", i)] = color
}
marshalled, _ := json.Marshal(colorMap)
return string(marshalled) return string(marshalled)
} }
func GenerateKittyTheme(colors []string) string { func GenerateKittyTheme(p Palette) string {
kittyColors := []struct {
name string
index int
}{
{"color0", 0},
{"color1", 1},
{"color2", 2},
{"color3", 3},
{"color4", 4},
{"color5", 5},
{"color6", 6},
{"color7", 7},
{"color8", 8},
{"color9", 9},
{"color10", 10},
{"color11", 11},
{"color12", 12},
{"color13", 13},
{"color14", 14},
{"color15", 15},
}
var result strings.Builder var result strings.Builder
for _, kc := range kittyColors { fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index]) fmt.Fprintf(&result, "color1 %s\n", p.Color1.Hex)
} fmt.Fprintf(&result, "color2 %s\n", p.Color2.Hex)
fmt.Fprintf(&result, "color3 %s\n", p.Color3.Hex)
fmt.Fprintf(&result, "color4 %s\n", p.Color4.Hex)
fmt.Fprintf(&result, "color5 %s\n", p.Color5.Hex)
fmt.Fprintf(&result, "color6 %s\n", p.Color6.Hex)
fmt.Fprintf(&result, "color7 %s\n", p.Color7.Hex)
fmt.Fprintf(&result, "color8 %s\n", p.Color8.Hex)
fmt.Fprintf(&result, "color9 %s\n", p.Color9.Hex)
fmt.Fprintf(&result, "color10 %s\n", p.Color10.Hex)
fmt.Fprintf(&result, "color11 %s\n", p.Color11.Hex)
fmt.Fprintf(&result, "color12 %s\n", p.Color12.Hex)
fmt.Fprintf(&result, "color13 %s\n", p.Color13.Hex)
fmt.Fprintf(&result, "color14 %s\n", p.Color14.Hex)
fmt.Fprintf(&result, "color15 %s\n", p.Color15.Hex)
return result.String() return result.String()
} }
func GenerateFootTheme(colors []string) string { func GenerateFootTheme(p Palette) string {
footColors := []struct {
name string
index int
}{
{"regular0", 0},
{"regular1", 1},
{"regular2", 2},
{"regular3", 3},
{"regular4", 4},
{"regular5", 5},
{"regular6", 6},
{"regular7", 7},
{"bright0", 8},
{"bright1", 9},
{"bright2", 10},
{"bright3", 11},
{"bright4", 12},
{"bright5", 13},
{"bright6", 14},
{"bright7", 15},
}
var result strings.Builder var result strings.Builder
for _, fc := range footColors { fmt.Fprintf(&result, "regular0=%s\n", p.Color0.HexStripped)
fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#")) fmt.Fprintf(&result, "regular1=%s\n", p.Color1.HexStripped)
} fmt.Fprintf(&result, "regular2=%s\n", p.Color2.HexStripped)
fmt.Fprintf(&result, "regular3=%s\n", p.Color3.HexStripped)
fmt.Fprintf(&result, "regular4=%s\n", p.Color4.HexStripped)
fmt.Fprintf(&result, "regular5=%s\n", p.Color5.HexStripped)
fmt.Fprintf(&result, "regular6=%s\n", p.Color6.HexStripped)
fmt.Fprintf(&result, "regular7=%s\n", p.Color7.HexStripped)
fmt.Fprintf(&result, "bright0=%s\n", p.Color8.HexStripped)
fmt.Fprintf(&result, "bright1=%s\n", p.Color9.HexStripped)
fmt.Fprintf(&result, "bright2=%s\n", p.Color10.HexStripped)
fmt.Fprintf(&result, "bright3=%s\n", p.Color11.HexStripped)
fmt.Fprintf(&result, "bright4=%s\n", p.Color12.HexStripped)
fmt.Fprintf(&result, "bright5=%s\n", p.Color13.HexStripped)
fmt.Fprintf(&result, "bright6=%s\n", p.Color14.HexStripped)
fmt.Fprintf(&result, "bright7=%s\n", p.Color15.HexStripped)
return result.String() return result.String()
} }
func GenerateAlacrittyTheme(colors []string) string { func GenerateAlacrittyTheme(p Palette) string {
alacrittyColors := []struct {
section string
name string
index int
}{
{"normal", "black", 0},
{"normal", "red", 1},
{"normal", "green", 2},
{"normal", "yellow", 3},
{"normal", "blue", 4},
{"normal", "magenta", 5},
{"normal", "cyan", 6},
{"normal", "white", 7},
{"bright", "black", 8},
{"bright", "red", 9},
{"bright", "green", 10},
{"bright", "yellow", 11},
{"bright", "blue", 12},
{"bright", "magenta", 13},
{"bright", "cyan", 14},
{"bright", "white", 15},
}
var result strings.Builder var result strings.Builder
currentSection := "" result.WriteString("[colors.normal]\n")
for _, ac := range alacrittyColors { fmt.Fprintf(&result, "black = '%s'\n", p.Color0.Hex)
if ac.section != currentSection { fmt.Fprintf(&result, "red = '%s'\n", p.Color1.Hex)
if currentSection != "" { fmt.Fprintf(&result, "green = '%s'\n", p.Color2.Hex)
result.WriteString("\n") fmt.Fprintf(&result, "yellow = '%s'\n", p.Color3.Hex)
} fmt.Fprintf(&result, "blue = '%s'\n", p.Color4.Hex)
fmt.Fprintf(&result, "[colors.%s]\n", ac.section) fmt.Fprintf(&result, "magenta = '%s'\n", p.Color5.Hex)
currentSection = ac.section fmt.Fprintf(&result, "cyan = '%s'\n", p.Color6.Hex)
} fmt.Fprintf(&result, "white = '%s'\n", p.Color7.Hex)
fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index]) result.WriteString("\n[colors.bright]\n")
} fmt.Fprintf(&result, "black = '%s'\n", p.Color8.Hex)
fmt.Fprintf(&result, "red = '%s'\n", p.Color9.Hex)
fmt.Fprintf(&result, "green = '%s'\n", p.Color10.Hex)
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color11.Hex)
fmt.Fprintf(&result, "blue = '%s'\n", p.Color12.Hex)
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color13.Hex)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color14.Hex)
fmt.Fprintf(&result, "white = '%s'\n", p.Color15.Hex)
return result.String() return result.String()
} }
func GenerateGhosttyTheme(colors []string) string { func GenerateGhosttyTheme(p Palette) string {
var result strings.Builder var result strings.Builder
for i, color := range colors { fmt.Fprintf(&result, "palette = 0=%s\n", p.Color0.Hex)
fmt.Fprintf(&result, "palette = %d=%s\n", i, color) fmt.Fprintf(&result, "palette = 1=%s\n", p.Color1.Hex)
} fmt.Fprintf(&result, "palette = 2=%s\n", p.Color2.Hex)
fmt.Fprintf(&result, "palette = 3=%s\n", p.Color3.Hex)
fmt.Fprintf(&result, "palette = 4=%s\n", p.Color4.Hex)
fmt.Fprintf(&result, "palette = 5=%s\n", p.Color5.Hex)
fmt.Fprintf(&result, "palette = 6=%s\n", p.Color6.Hex)
fmt.Fprintf(&result, "palette = 7=%s\n", p.Color7.Hex)
fmt.Fprintf(&result, "palette = 8=%s\n", p.Color8.Hex)
fmt.Fprintf(&result, "palette = 9=%s\n", p.Color9.Hex)
fmt.Fprintf(&result, "palette = 10=%s\n", p.Color10.Hex)
fmt.Fprintf(&result, "palette = 11=%s\n", p.Color11.Hex)
fmt.Fprintf(&result, "palette = 12=%s\n", p.Color12.Hex)
fmt.Fprintf(&result, "palette = 13=%s\n", p.Color13.Hex)
fmt.Fprintf(&result, "palette = 14=%s\n", p.Color14.Hex)
fmt.Fprintf(&result, "palette = 15=%s\n", p.Color15.Hex)
return result.String() return result.String()
} }
func GenerateWeztermTheme(colors []string) string { func GenerateWeztermTheme(p Palette) string {
var result strings.Builder var result strings.Builder
labels := []string{"ansi", "brights"} fmt.Fprintf(&result, "ansi = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
for j, label := range labels { p.Color0.Hex, p.Color1.Hex, p.Color2.Hex, p.Color3.Hex,
start := j * 8 p.Color4.Hex, p.Color5.Hex, p.Color6.Hex, p.Color7.Hex)
colorSlice := make([]string, 8) fmt.Fprintf(&result, "brights = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
for i, color := range colors[start : start+8] { p.Color8.Hex, p.Color9.Hex, p.Color10.Hex, p.Color11.Hex,
colorSlice[i] = fmt.Sprintf("'%s'", color) p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
}
fmt.Fprintf(&result, "%s = [%s]\n", label, strings.Join(colorSlice, ", "))
}
return result.String() return result.String()
} }

View File

@@ -1,250 +0,0 @@
package dank16
import (
"encoding/json"
"fmt"
)
type VSCodeTheme struct {
Schema string `json:"$schema"`
Name string `json:"name"`
Type string `json:"type"`
Colors map[string]string `json:"colors"`
TokenColors []VSCodeTokenColor `json:"tokenColors"`
SemanticHighlighting bool `json:"semanticHighlighting"`
SemanticTokenColors map[string]VSCodeTokenSetting `json:"semanticTokenColors"`
}
type VSCodeTokenColor struct {
Scope interface{} `json:"scope"`
Settings VSCodeTokenSetting `json:"settings"`
}
type VSCodeTokenSetting struct {
Foreground string `json:"foreground,omitempty"`
FontStyle string `json:"fontStyle,omitempty"`
}
func updateTokenColor(tc interface{}, scopeToColor map[string]string) {
tcMap, ok := tc.(map[string]interface{})
if !ok {
return
}
scopes, ok := tcMap["scope"].([]interface{})
if !ok {
return
}
settings, ok := tcMap["settings"].(map[string]interface{})
if !ok {
return
}
isYaml := hasScopeContaining(scopes, "yaml")
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
if scopeStr == "string" && isYaml {
continue
}
if applyColorToScope(settings, scope, scopeToColor) {
break
}
}
}
func applyColorToScope(settings map[string]interface{}, scope interface{}, scopeToColor map[string]string) bool {
scopeStr, ok := scope.(string)
if !ok {
return false
}
newColor, exists := scopeToColor[scopeStr]
if !exists {
return false
}
settings["foreground"] = newColor
return true
}
func hasScopeContaining(scopes []interface{}, substring string) bool {
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
for i := 0; i <= len(scopeStr)-len(substring); i++ {
if scopeStr[i:i+len(substring)] == substring {
return true
}
}
}
return false
}
func EnrichVSCodeTheme(themeData []byte, colors []string) ([]byte, error) {
var theme map[string]interface{}
if err := json.Unmarshal(themeData, &theme); err != nil {
return nil, err
}
colorsMap, ok := theme["colors"].(map[string]interface{})
if !ok {
colorsMap = make(map[string]interface{})
theme["colors"] = colorsMap
}
bg := colors[0]
isLight := false
if len(bg) == 7 && bg[0] == '#' {
r, g, b := 0, 0, 0
fmt.Sscanf(bg[1:], "%02x%02x%02x", &r, &g, &b)
luminance := (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255.0
isLight = luminance > 0.5
}
if isLight {
theme["type"] = "light"
} else {
theme["type"] = "dark"
}
colorsMap["terminal.ansiBlack"] = colors[0]
colorsMap["terminal.ansiRed"] = colors[1]
colorsMap["terminal.ansiGreen"] = colors[2]
colorsMap["terminal.ansiYellow"] = colors[3]
colorsMap["terminal.ansiBlue"] = colors[4]
colorsMap["terminal.ansiMagenta"] = colors[5]
colorsMap["terminal.ansiCyan"] = colors[6]
colorsMap["terminal.ansiWhite"] = colors[7]
colorsMap["terminal.ansiBrightBlack"] = colors[8]
colorsMap["terminal.ansiBrightRed"] = colors[9]
colorsMap["terminal.ansiBrightGreen"] = colors[10]
colorsMap["terminal.ansiBrightYellow"] = colors[11]
colorsMap["terminal.ansiBrightBlue"] = colors[12]
colorsMap["terminal.ansiBrightMagenta"] = colors[13]
colorsMap["terminal.ansiBrightCyan"] = colors[14]
colorsMap["terminal.ansiBrightWhite"] = colors[15]
tokenColors, ok := theme["tokenColors"].([]interface{})
if ok {
scopeToColor := map[string]string{
"comment": colors[8],
"punctuation.definition.comment": colors[8],
"keyword": colors[5],
"storage.type": colors[13],
"storage.modifier": colors[5],
"variable": colors[15],
"variable.parameter": colors[7],
"meta.object-literal.key": colors[4],
"meta.property.object": colors[4],
"variable.other.property": colors[4],
"constant.other.symbol": colors[12],
"constant.numeric": colors[12],
"constant.language": colors[12],
"constant.character": colors[3],
"entity.name.type": colors[12],
"support.type": colors[13],
"entity.name.class": colors[12],
"entity.name.function": colors[2],
"support.function": colors[2],
"support.class": colors[15],
"support.variable": colors[15],
"variable.language": colors[12],
"entity.name.tag.yaml": colors[12],
"string.unquoted.plain.out.yaml": colors[15],
"string.unquoted.yaml": colors[15],
"string": colors[3],
}
for i, tc := range tokenColors {
updateTokenColor(tc, scopeToColor)
tokenColors[i] = tc
}
yamlRules := []VSCodeTokenColor{
{
Scope: "entity.name.tag.yaml",
Settings: VSCodeTokenSetting{Foreground: colors[12]},
},
{
Scope: []string{"string.unquoted.plain.out.yaml", "string.unquoted.yaml"},
Settings: VSCodeTokenSetting{Foreground: colors[15]},
},
}
for _, rule := range yamlRules {
tokenColors = append(tokenColors, rule)
}
theme["tokenColors"] = tokenColors
}
if semanticTokenColors, ok := theme["semanticTokenColors"].(map[string]interface{}); ok {
updates := map[string]string{
"variable": colors[15],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
if existing, ok := semanticTokenColors[key].(map[string]interface{}); ok {
existing["foreground"] = color
} else {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
}
} else {
semanticTokenColors := make(map[string]interface{})
updates := map[string]string{
"variable": colors[7],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
theme["semanticTokenColors"] = semanticTokenColors
}
return json.MarshalIndent(theme, "", " ")
}

View File

@@ -37,6 +37,9 @@ func init() {
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan) return NewArchDistribution(config, logChan)
}) })
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
} }
type ArchDistribution struct { type ArchDistribution struct {

View File

@@ -209,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev") "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")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }

View File

@@ -19,10 +19,12 @@ func init() {
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
} }
type FedoraDistribution struct { type FedoraDistribution struct {
@@ -165,7 +167,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = f.getNiriMapping(variants["niri"]) packages["niri"] = f.getNiriMapping(variants["niri"])
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"} packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
} }
return packages return packages
@@ -203,7 +205,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
if variant == deps.VariantGit { if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
} }
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"} return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
} }
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -506,6 +508,14 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"} 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...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{

View File

@@ -478,6 +478,95 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
return fmt.Errorf("failed to create cache directory: %w", err) 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") tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil { if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err) return fmt.Errorf("failed to create temp directory: %w", err)
@@ -486,7 +575,7 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
Progress: 0.2, Progress: 0.35,
Step: "Cloning hyprpicker repository...", Step: "Cloning hyprpicker repository...",
IsComplete: false, IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git", CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
@@ -499,16 +588,39 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
Progress: 0.4, Progress: 0.45,
Step: "Building hyprpicker...", Step: "Configuring hyprpicker build...",
IsComplete: false, IsComplete: false,
CommandInfo: "make all", CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
} }
buildCmd := exec.CommandContext(ctx, "make", "all") 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.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := buildCmd.Run(); err != nil { if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
return fmt.Errorf("failed to build hyprpicker: %w", err) return fmt.Errorf("failed to build hyprpicker: %w", err)
} }
@@ -518,10 +630,10 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
Step: "Installing hyprpicker...", Step: "Installing hyprpicker...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: "sudo make install", CommandInfo: "sudo cmake --install build",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install") installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installCmd.Dir = tmpDir installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err) return fmt.Errorf("failed to install hyprpicker: %w", err)

View File

@@ -1,466 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution {
return NewNixOSDistribution(config, logChan)
})
}
type NixOSDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution {
base := NewBaseDistribution(logChan)
return &NixOSDistribution{
BaseDistribution: base,
config: config,
}
}
func (n *NixOSDistribution) GetID() string {
return n.config.ID
}
func (n *NixOSDistribution) GetColorHex() string {
return n.config.ColorHex
}
func (n *NixOSDistribution) GetFamily() DistroFamily {
return n.config.Family
}
func (n *NixOSDistribution) GetPackageManager() PackageManagerType {
return PackageManagerNix
}
func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
// DMS at the top (shell is prominent)
dependencies = append(dependencies, n.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, n.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, n.detectGit())
dependencies = append(dependencies, n.detectWindowManager(wm))
dependencies = append(dependencies, n.detectQuickshell())
dependencies = append(dependencies, n.detectXDGPortal())
dependencies = append(dependencies, n.detectPolkitAgent())
dependencies = append(dependencies, n.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, n.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, n.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, n.detectMatugen())
dependencies = append(dependencies, n.detectDgop())
dependencies = append(dependencies, n.detectHyprpicker())
dependencies = append(dependencies, n.detectClipboardTools()...)
return dependencies, nil
}
func (n *NixOSDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
// For NixOS, check if quickshell can find the dms config
cmd := exec.Command("qs", "-c", "dms", "--list")
if err := cmd.Run(); err == nil {
status = deps.StatusInstalled
} else if n.packageInstalled("DankMaterialShell") {
// Fallback: check if flake is in profile
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Description: "Desktop Management System configuration (installed as flake)",
Required: true,
}
}
func (n *NixOSDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
switch wm {
case deps.WindowManagerHyprland:
status := deps.StatusMissing
description := "Dynamic tiling Wayland compositor"
if n.commandExists("hyprland") || n.commandExists("Hyprland") {
status = deps.StatusInstalled
} else {
description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix"
}
return deps.Dependency{
Name: "hyprland",
Status: status,
Description: description,
Required: true,
}
case deps.WindowManagerNiri:
status := deps.StatusMissing
description := "Scrollable-tiling Wayland compositor"
if n.commandExists("niri") {
status = deps.StatusInstalled
} else {
description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix"
}
return deps.Dependency{
Name: "niri",
Status: status,
Description: description,
Required: true,
}
default:
return deps.Dependency{
Name: "unknown-wm",
Status: deps.StatusMissing,
Description: "Unknown window manager",
Required: true,
}
}
}
func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency
tools := []struct {
name string
description string
}{
{"grim", "Screenshot utility for Wayland"},
{"slurp", "Region selection utility for Wayland"},
{"hyprctl", "Hyprland control utility (comes with system Hyprland)"},
{"hyprpicker", "Color picker for Hyprland"},
{"grimblast", "Screenshot script for Hyprland"},
{"jq", "JSON processor"},
}
for _, tool := range tools {
status := deps.StatusMissing
// Special handling for hyprctl - it comes with system hyprland
if tool.name == "hyprctl" {
if n.commandExists("hyprctl") {
status = deps.StatusInstalled
}
} else {
if n.commandExists(tool.name) {
status = deps.StatusInstalled
}
}
dependencies = append(dependencies, deps.Dependency{
Name: tool.name,
Status: status,
Description: tool.description,
Required: true,
})
}
return dependencies
}
func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if n.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
}
func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (n *NixOSDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
}
func (n *NixOSDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("nix", "profile", "list")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), pkg)
}
func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "nixpkgs#git", Repository: RepoTypeSystem},
"quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake},
"matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake},
"dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake},
"dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake},
"ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem},
"alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem},
"cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem},
}
// Note: Window managers (hyprland/niri) should be installed system-wide on NixOS
// We only install the tools here
switch wm {
case deps.WindowManagerHyprland:
// Skip hyprland itself - should be installed system-wide
packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem}
packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake}
packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
// Skip niri itself - should be installed system-wide
packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake}
}
return packages
}
func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.10,
Step: "NixOS prerequisites ready",
IsComplete: false,
LogOutput: "NixOS package manager is ready to use",
}
return nil
}
func (n *NixOSDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
// Phase 1: Check Prerequisites
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
nixpkgsPkgs, flakePkgs, _ := n.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 2: Nixpkgs Packages
if len(nixpkgsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")),
}
if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil {
return fmt.Errorf("failed to install nixpkgs packages: %w", err)
}
}
// Phase 3: Flake Packages
if len(flakePkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")),
}
if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil {
return fmt.Errorf("failed to install flake packages: %w", err)
}
}
// Phase 4: Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
if err := n.postInstallConfig(progressChan); err != nil {
return fmt.Errorf("failed to configure system: %w", err)
}
// Phase 5: Complete
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
nixpkgsPkgs := []string{}
flakePkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := n.GetPackageMapping(wm)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name)
case RepoTypeFlake:
flakePkgs = append(flakePkgs, pkgInfo.Name)
}
}
return nixpkgsPkgs, flakePkgs, variantMap
}
func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", ")))
args := []string{"profile", "install"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing nixpkgs packages...",
IsComplete: false,
CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")),
}
cmd := exec.CommandContext(ctx, "nix", args...)
return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", ")))
baseProgress := 0.65
progressStep := 0.20 / float64(len(packages))
for i, pkg := range packages {
currentProgress := baseProgress + (float64(i) * progressStep)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: currentProgress,
Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)),
IsComplete: false,
CommandInfo: fmt.Sprintf("nix profile install %s", pkg),
}
cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg)
if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil {
return fmt.Errorf("failed to install flake package %s: %w", pkg, err)
}
}
return nil
}
func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error {
// For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone
// The flake installation handles both the binary and config files correctly
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.95,
Step: "NixOS configuration complete",
IsComplete: false,
LogOutput: "DMS installed via flake - binary and config handled by Nix",
}
return nil
}

View File

@@ -122,15 +122,8 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
return []DependencyInfo{} return []DependencyInfo{}
} }
isNixOS := d.isNixOS()
var components []DependencyInfo var components []DependencyInfo
for _, dep := range dependencies { for _, dep := range dependencies {
// On NixOS, filter out the window managers themselves but keep their components
if isNixOS && (dep.Name == "hyprland" || dep.Name == "niri") {
continue
}
components = append(components, DependencyInfo{ components = append(components, DependencyInfo{
Name: dep.Name, Name: dep.Name,
Status: dep.Status, Status: dep.Status,
@@ -142,23 +135,6 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
return components return components
} }
func (d *Detector) isNixOS() bool {
_, err := os.Stat("/etc/nixos")
if err == nil {
return true
}
// Alternative check
if _, err := os.Stat("/nix/store"); err == nil {
// Also check for nixos-version command
if d.commandExists("nixos-version") {
return true
}
}
return false
}
type DependencyInfo struct { type DependencyInfo struct {
Name string Name string
Status deps.DependencyStatus Status deps.DependencyStatus

View File

@@ -227,6 +227,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local"), ".local directory"}, {filepath.Join(homeDir, ".local"), ".local directory"},
{filepath.Join(homeDir, ".cache"), ".cache directory"}, {filepath.Join(homeDir, ".cache"), ".cache directory"},
{filepath.Join(homeDir, ".local", "state"), ".local/state directory"}, {filepath.Join(homeDir, ".local", "state"), ".local/state directory"},
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
} }
logFunc("\nSetting up parent directory ACLs for greeter user access...") logFunc("\nSetting up parent directory ACLs for greeter user access...")
@@ -239,8 +240,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
} }
} }
// Set ACL to allow greeter user execute (traverse) permission // Set ACL to allow greeter user read+execute permission (for session discovery)
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:x", dir.path); err != nil { if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path)) logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
continue continue
@@ -287,6 +288,8 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"}, {filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"}, {filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"}, {filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
{filepath.Join(homeDir, ".local", "share", "xsessions"), "xsessions"},
} }
for _, dir := range configDirs { for _, dir := range configDirs {
@@ -364,7 +367,7 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
} }
} }
runSudoCmd(sudoPassword, "rm", "-f", link.target) runSudoCmd(sudoPassword, "rm", "-f", link.target) //nolint:errcheck
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil { if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))

View File

@@ -77,7 +77,7 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
func expandPath(path string) (string, error) { func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path) expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") { if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/hyprland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
) )
@@ -26,7 +25,7 @@ func (h *HyprlandProvider) Name() string {
} }
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := hyprland.ParseKeys(h.configPath) section, err := ParseHyprlandKeys(h.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse hyprland config: %w", err) return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
} }
@@ -41,7 +40,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
}, nil }, nil
} }
func (h *HyprlandProvider) convertSection(section *hyprland.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -86,7 +85,7 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
} }
} }
func (h *HyprlandProvider) convertKeybind(kb *hyprland.KeyBinding, subcategory string) keybinds.Keybind { func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
key := h.formatKey(kb) key := h.formatKey(kb)
desc := kb.Comment desc := kb.Comment
@@ -108,7 +107,7 @@ func (h *HyprlandProvider) generateDescription(dispatcher, params string) string
return dispatcher return dispatcher
} }
func (h *HyprlandProvider) formatKey(kb *hyprland.KeyBinding) string { func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1) parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...) parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key) parts = append(parts, kb.Key)

View File

@@ -1,4 +1,4 @@
package hyprland package providers
import ( import (
"os" "os"
@@ -15,7 +15,7 @@ const (
var ModSeparators = []rune{'+', ' '} var ModSeparators = []rune{'+', ' '}
type KeyBinding struct { type HyprlandKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
Dispatcher string `json:"dispatcher"` Dispatcher string `json:"dispatcher"`
@@ -23,25 +23,25 @@ type KeyBinding struct {
Comment string `json:"comment"` Comment string `json:"comment"`
} }
type Section struct { type HyprlandSection struct {
Children []Section `json:"children"` Children []HyprlandSection `json:"children"`
Keybinds []KeyBinding `json:"keybinds"` Keybinds []HyprlandKeyBinding `json:"keybinds"`
Name string `json:"name"` Name string `json:"name"`
} }
type Parser struct { type HyprlandParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
} }
func NewParser() *Parser { func NewHyprlandParser() *HyprlandParser {
return &Parser{ return &HyprlandParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
} }
} }
func (p *Parser) ReadContent(directory string) error { func (p *HyprlandParser) ReadContent(directory string) error {
expandedDir := os.ExpandEnv(directory) expandedDir := os.ExpandEnv(directory)
expandedDir = filepath.Clean(expandedDir) expandedDir = filepath.Clean(expandedDir)
if strings.HasPrefix(expandedDir, "~") { if strings.HasPrefix(expandedDir, "~") {
@@ -87,7 +87,7 @@ func (p *Parser) ReadContent(directory string) error {
return nil return nil
} }
func autogenerateComment(dispatcher, params string) string { func hyprlandAutogenerateComment(dispatcher, params string) string {
switch dispatcher { switch dispatcher {
case "resizewindow": case "resizewindow":
return "Resize window" return "Resize window"
@@ -196,7 +196,7 @@ func autogenerateComment(dispatcher, params string) string {
} }
} }
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding { func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
line := p.contentLines[lineNumber] line := p.contentLines[lineNumber]
parts := strings.SplitN(line, "=", 2) parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 { if len(parts) < 2 {
@@ -232,7 +232,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
return nil return nil
} }
} else { } else {
comment = autogenerateComment(dispatcher, params) comment = hyprlandAutogenerateComment(dispatcher, params)
} }
var modList []string var modList []string
@@ -256,7 +256,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
return &KeyBinding{ return &HyprlandKeyBinding{
Mods: modList, Mods: modList,
Key: key, Key: key,
Dispatcher: dispatcher, Dispatcher: dispatcher,
@@ -265,7 +265,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section { func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
titleRegex := regexp.MustCompile(TitleRegex) titleRegex := regexp.MustCompile(TitleRegex)
for p.readingLine < len(p.contentLines) { for p.readingLine < len(p.contentLines) {
@@ -283,9 +283,9 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
sectionName := strings.TrimSpace(line[headingScope+1:]) sectionName := strings.TrimSpace(line[headingScope+1:])
p.readingLine++ p.readingLine++
childSection := &Section{ childSection := &HyprlandSection{
Children: []Section{}, Children: []HyprlandSection{},
Keybinds: []KeyBinding{}, Keybinds: []HyprlandKeyBinding{},
Name: sectionName, Name: sectionName,
} }
result := p.getBindsRecursive(childSection, headingScope) result := p.getBindsRecursive(childSection, headingScope)
@@ -312,18 +312,18 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
return currentContent return currentContent
} }
func (p *Parser) ParseKeys() *Section { func (p *HyprlandParser) ParseKeys() *HyprlandSection {
p.readingLine = 0 p.readingLine = 0
rootSection := &Section{ rootSection := &HyprlandSection{
Children: []Section{}, Children: []HyprlandSection{},
Keybinds: []KeyBinding{}, Keybinds: []HyprlandKeyBinding{},
Name: "", Name: "",
} }
return p.getBindsRecursive(rootSection, 0) return p.getBindsRecursive(rootSection, 0)
} }
func ParseKeys(path string) (*Section, error) { func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
parser := NewParser() parser := NewHyprlandParser()
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package hyprland package providers
import ( import (
"os" "os"
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func TestAutogenerateComment(t *testing.T) { func TestHyprlandAutogenerateComment(t *testing.T) {
tests := []struct { tests := []struct {
dispatcher string dispatcher string
params string params string
@@ -51,25 +51,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.dispatcher+"_"+tt.params, func(t *testing.T) { t.Run(tt.dispatcher+"_"+tt.params, func(t *testing.T) {
result := autogenerateComment(tt.dispatcher, tt.params) result := hyprlandAutogenerateComment(tt.dispatcher, tt.params)
if result != tt.expected { if result != tt.expected {
t.Errorf("autogenerateComment(%q, %q) = %q, want %q", t.Errorf("hyprlandAutogenerateComment(%q, %q) = %q, want %q",
tt.dispatcher, tt.params, result, tt.expected) tt.dispatcher, tt.params, result, tt.expected)
} }
}) })
} }
} }
func TestGetKeybindAtLine(t *testing.T) { func TestHyprlandGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *KeyBinding expected *HyprlandKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
line: "bind = SUPER, Q, killactive", line: "bind = SUPER, Q, killactive",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "Q", Key: "Q",
Dispatcher: "killactive", Dispatcher: "killactive",
@@ -80,7 +80,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_params", name: "keybind_with_params",
line: "bind = SUPER, left, movefocus, l", line: "bind = SUPER, left, movefocus, l",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "left", Key: "left",
Dispatcher: "movefocus", Dispatcher: "movefocus",
@@ -91,7 +91,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_comment", name: "keybind_with_comment",
line: "bind = SUPER, T, exec, kitty # Open terminal", line: "bind = SUPER, T, exec, kitty # Open terminal",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "T", Key: "T",
Dispatcher: "exec", Dispatcher: "exec",
@@ -107,7 +107,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_mods", name: "keybind_multiple_mods",
line: "bind = SUPER+SHIFT, F, fullscreen, 0", line: "bind = SUPER+SHIFT, F, fullscreen, 0",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{"SUPER", "SHIFT"}, Mods: []string{"SUPER", "SHIFT"},
Key: "F", Key: "F",
Dispatcher: "fullscreen", Dispatcher: "fullscreen",
@@ -118,7 +118,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_no_mods", name: "keybind_no_mods",
line: "bind = , Print, exec, screenshot", line: "bind = , Print, exec, screenshot",
expected: &KeyBinding{ expected: &HyprlandKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
Dispatcher: "exec", Dispatcher: "exec",
@@ -130,7 +130,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewHyprlandParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -171,7 +171,7 @@ func TestGetKeybindAtLine(t *testing.T) {
} }
} }
func TestParseKeysWithSections(t *testing.T) { func TestHyprlandParseKeysWithSections(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "hyprland.conf") configFile := filepath.Join(tmpDir, "hyprland.conf")
@@ -191,9 +191,9 @@ bind = SUPER, T, exec, kitty # Terminal
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseHyprlandKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseHyprlandKeys failed: %v", err)
} }
if len(section.Children) != 2 { if len(section.Children) != 2 {
@@ -236,7 +236,7 @@ bind = SUPER, T, exec, kitty # Terminal
} }
} }
func TestParseKeysWithCommentBinds(t *testing.T) { func TestHyprlandParseKeysWithCommentBinds(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "test.conf")
@@ -249,9 +249,9 @@ bind = SUPER, B, exec, app2
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseHyprlandKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseHyprlandKeys failed: %v", err)
} }
if len(section.Keybinds) != 3 { if len(section.Keybinds) != 3 {
@@ -269,7 +269,7 @@ bind = SUPER, B, exec, app2
} }
} }
func TestReadContentMultipleFiles(t *testing.T) { func TestHyprlandReadContentMultipleFiles(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "a.conf") file1 := filepath.Join(tmpDir, "a.conf")
@@ -285,7 +285,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewParser() parser := NewHyprlandParser()
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -296,7 +296,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
} }
} }
func TestReadContentErrors(t *testing.T) { func TestHyprlandReadContentErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
@@ -313,7 +313,7 @@ func TestReadContentErrors(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, err := ParseKeys(tt.path) _, err := ParseHyprlandKeys(tt.path)
if err == nil { if err == nil {
t.Error("Expected error, got nil") t.Error("Expected error, got nil")
} }
@@ -321,7 +321,7 @@ func TestReadContentErrors(t *testing.T) {
} }
} }
func TestReadContentWithTildeExpansion(t *testing.T) { func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Skip("Cannot get home directory") t.Skip("Cannot get home directory")
@@ -343,7 +343,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewParser() parser := NewHyprlandParser()
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -352,8 +352,8 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
} }
} }
func TestKeybindWithParamsContainingCommas(t *testing.T) { func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
parser := NewParser() parser := NewHyprlandParser()
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"} parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -368,7 +368,7 @@ func TestKeybindWithParamsContainingCommas(t *testing.T) {
} }
} }
func TestEmptyAndCommentLines(t *testing.T) { func TestHyprlandEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "test.conf")
@@ -385,9 +385,9 @@ bind = SUPER, T, exec, kitty
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseHyprlandKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseHyprlandKeys failed: %v", err)
} }
if len(section.Keybinds) != 2 { if len(section.Keybinds) != 2 {

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
) )
@@ -118,7 +119,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
func expandPath(path string) (string, error) { func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path) expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") { if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
) )
type MangoWCProvider struct { type MangoWCProvider struct {
@@ -26,7 +25,7 @@ func (m *MangoWCProvider) Name() string {
} }
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
keybinds_list, err := mangowc.ParseKeys(m.configPath) keybinds_list, err := ParseMangoWCKeys(m.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse mangowc config: %w", err) return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
} }
@@ -83,7 +82,7 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
} }
} }
func (m *MangoWCProvider) convertKeybind(kb *mangowc.KeyBinding) keybinds.Keybind { func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
key := m.formatKey(kb) key := m.formatKey(kb)
desc := kb.Comment desc := kb.Comment
@@ -104,7 +103,7 @@ func (m *MangoWCProvider) generateDescription(command, params string) string {
return command return command
} }
func (m *MangoWCProvider) formatKey(kb *mangowc.KeyBinding) string { func (m *MangoWCProvider) formatKey(kb *MangoWCKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1) parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...) parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key) parts = append(parts, kb.Key)

View File

@@ -1,4 +1,4 @@
package mangowc package providers
import ( import (
"os" "os"
@@ -8,12 +8,12 @@ import (
) )
const ( const (
HideComment = "[hidden]" MangoWCHideComment = "[hidden]"
) )
var ModSeparators = []rune{'+', ' '} var MangoWCModSeparators = []rune{'+', ' '}
type KeyBinding struct { type MangoWCKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
Command string `json:"command"` Command string `json:"command"`
@@ -21,19 +21,19 @@ type KeyBinding struct {
Comment string `json:"comment"` Comment string `json:"comment"`
} }
type Parser struct { type MangoWCParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
} }
func NewParser() *Parser { func NewMangoWCParser() *MangoWCParser {
return &Parser{ return &MangoWCParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
} }
} }
func (p *Parser) ReadContent(path string) error { func (p *MangoWCParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath := os.ExpandEnv(path)
expandedPath = filepath.Clean(expandedPath) expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") { if strings.HasPrefix(expandedPath, "~") {
@@ -82,7 +82,7 @@ func (p *Parser) ReadContent(path string) error {
return nil return nil
} }
func autogenerateComment(command, params string) string { func mangowcAutogenerateComment(command, params string) string {
switch command { switch command {
case "spawn", "spawn_shell": case "spawn", "spawn_shell":
return params return params
@@ -196,7 +196,7 @@ func autogenerateComment(command, params string) string {
} }
} }
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding { func (p *MangoWCParser) getKeybindAtLine(lineNumber int) *MangoWCKeyBinding {
if lineNumber >= len(p.contentLines) { if lineNumber >= len(p.contentLines) {
return nil return nil
} }
@@ -220,7 +220,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
comment = strings.TrimSpace(parts[1]) comment = strings.TrimSpace(parts[1])
} }
if strings.HasPrefix(comment, HideComment) { if strings.HasPrefix(comment, MangoWCHideComment) {
return nil return nil
} }
@@ -239,16 +239,16 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
if comment == "" { if comment == "" {
comment = autogenerateComment(command, params) comment = mangowcAutogenerateComment(command, params)
} }
var modList []string var modList []string
if mods != "" && !strings.EqualFold(mods, "none") { if mods != "" && !strings.EqualFold(mods, "none") {
modstring := mods + string(ModSeparators[0]) modstring := mods + string(MangoWCModSeparators[0])
p := 0 p := 0
for index, char := range modstring { for index, char := range modstring {
isModSep := false isModSep := false
for _, sep := range ModSeparators { for _, sep := range MangoWCModSeparators {
if char == sep { if char == sep {
isModSep = true isModSep = true
break break
@@ -265,7 +265,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
_ = bindType _ = bindType
return &KeyBinding{ return &MangoWCKeyBinding{
Mods: modList, Mods: modList,
Key: key, Key: key,
Command: command, Command: command,
@@ -274,8 +274,8 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
func (p *Parser) ParseKeys() []KeyBinding { func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
var keybinds []KeyBinding var keybinds []MangoWCKeyBinding
for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ { for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ {
line := p.contentLines[lineNumber] line := p.contentLines[lineNumber]
@@ -296,8 +296,8 @@ func (p *Parser) ParseKeys() []KeyBinding {
return keybinds return keybinds
} }
func ParseKeys(path string) ([]KeyBinding, error) { func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
parser := NewParser() parser := NewMangoWCParser()
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package mangowc package providers
import ( import (
"os" "os"
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func TestAutogenerateComment(t *testing.T) { func TestMangoWCAutogenerateComment(t *testing.T) {
tests := []struct { tests := []struct {
command string command string
params string params string
@@ -60,25 +60,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.command+"_"+tt.params, func(t *testing.T) { t.Run(tt.command+"_"+tt.params, func(t *testing.T) {
result := autogenerateComment(tt.command, tt.params) result := mangowcAutogenerateComment(tt.command, tt.params)
if result != tt.expected { if result != tt.expected {
t.Errorf("autogenerateComment(%q, %q) = %q, want %q", t.Errorf("mangowcAutogenerateComment(%q, %q) = %q, want %q",
tt.command, tt.params, result, tt.expected) tt.command, tt.params, result, tt.expected)
} }
}) })
} }
} }
func TestGetKeybindAtLine(t *testing.T) { func TestMangoWCGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *KeyBinding expected *MangoWCKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
line: "bind=ALT,q,killclient,", line: "bind=ALT,q,killclient,",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "q", Key: "q",
Command: "killclient", Command: "killclient",
@@ -89,7 +89,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_params", name: "keybind_with_params",
line: "bind=ALT,Left,focusdir,left", line: "bind=ALT,Left,focusdir,left",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "Left", Key: "Left",
Command: "focusdir", Command: "focusdir",
@@ -100,7 +100,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_comment", name: "keybind_with_comment",
line: "bind=Alt,t,spawn,kitty # Open terminal", line: "bind=Alt,t,spawn,kitty # Open terminal",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"Alt"}, Mods: []string{"Alt"},
Key: "t", Key: "t",
Command: "spawn", Command: "spawn",
@@ -116,7 +116,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_mods", name: "keybind_multiple_mods",
line: "bind=SUPER+SHIFT,Up,exchange_client,up", line: "bind=SUPER+SHIFT,Up,exchange_client,up",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"SUPER", "SHIFT"}, Mods: []string{"SUPER", "SHIFT"},
Key: "Up", Key: "Up",
Command: "exchange_client", Command: "exchange_client",
@@ -127,7 +127,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_no_mods", name: "keybind_no_mods",
line: "bind=NONE,Print,spawn,screenshot", line: "bind=NONE,Print,spawn,screenshot",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
Command: "spawn", Command: "spawn",
@@ -138,7 +138,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_params", name: "keybind_multiple_params",
line: "bind=Ctrl,1,view,1,0", line: "bind=Ctrl,1,view,1,0",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"Ctrl"}, Mods: []string{"Ctrl"},
Key: "1", Key: "1",
Command: "view", Command: "view",
@@ -149,7 +149,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "bindl_flag", name: "bindl_flag",
line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock", line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"SUPER", "ALT"}, Mods: []string{"SUPER", "ALT"},
Key: "l", Key: "l",
Command: "spawn", Command: "spawn",
@@ -160,7 +160,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_spaces", name: "keybind_with_spaces",
line: "bind = SUPER, r, reload_config", line: "bind = SUPER, r, reload_config",
expected: &KeyBinding{ expected: &MangoWCKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "r", Key: "r",
Command: "reload_config", Command: "reload_config",
@@ -172,7 +172,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewMangoWCParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -213,7 +213,7 @@ func TestGetKeybindAtLine(t *testing.T) {
} }
} }
func TestParseKeys(t *testing.T) { func TestMangoWCParseKeys(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -242,9 +242,9 @@ bind=Ctrl,2,view,2,0
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
keybinds, err := ParseKeys(configFile) keybinds, err := ParseMangoWCKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseMangoWCKeys failed: %v", err)
} }
expectedCount := 7 expectedCount := 7
@@ -267,7 +267,7 @@ bind=Ctrl,2,view,2,0
} }
} }
func TestReadContentMultipleFiles(t *testing.T) { func TestMangoWCReadContentMultipleFiles(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "a.conf") file1 := filepath.Join(tmpDir, "a.conf")
@@ -283,7 +283,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewParser() parser := NewMangoWCParser()
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -294,7 +294,7 @@ func TestReadContentMultipleFiles(t *testing.T) {
} }
} }
func TestReadContentSingleFile(t *testing.T) { func TestMangoWCReadContentSingleFile(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -304,7 +304,7 @@ func TestReadContentSingleFile(t *testing.T) {
t.Fatalf("Failed to write config: %v", err) t.Fatalf("Failed to write config: %v", err)
} }
parser := NewParser() parser := NewMangoWCParser()
if err := parser.ReadContent(configFile); err != nil { if err := parser.ReadContent(configFile); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -315,7 +315,7 @@ func TestReadContentSingleFile(t *testing.T) {
} }
} }
func TestReadContentErrors(t *testing.T) { func TestMangoWCReadContentErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
@@ -332,7 +332,7 @@ func TestReadContentErrors(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, err := ParseKeys(tt.path) _, err := ParseMangoWCKeys(tt.path)
if err == nil { if err == nil {
t.Error("Expected error, got nil") t.Error("Expected error, got nil")
} }
@@ -340,7 +340,7 @@ func TestReadContentErrors(t *testing.T) {
} }
} }
func TestReadContentWithTildeExpansion(t *testing.T) { func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Skip("Cannot get home directory") t.Skip("Cannot get home directory")
@@ -362,7 +362,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewParser() parser := NewMangoWCParser()
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -371,7 +371,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
} }
} }
func TestEmptyAndCommentLines(t *testing.T) { func TestMangoWCEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -388,9 +388,9 @@ bind=Alt,t,spawn,kitty
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
keybinds, err := ParseKeys(configFile) keybinds, err := ParseMangoWCKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseMangoWCKeys failed: %v", err)
} }
if len(keybinds) != 2 { if len(keybinds) != 2 {
@@ -398,7 +398,7 @@ bind=Alt,t,spawn,kitty
} }
} }
func TestInvalidBindLines(t *testing.T) { func TestMangoWCInvalidBindLines(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
@@ -419,7 +419,7 @@ func TestInvalidBindLines(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewMangoWCParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -430,7 +430,7 @@ func TestInvalidBindLines(t *testing.T) {
} }
} }
func TestRealWorldConfig(t *testing.T) { func TestMangoWCRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf") configFile := filepath.Join(tmpDir, "config.conf")
@@ -462,9 +462,9 @@ bind=Ctrl,3,view,3,0
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
keybinds, err := ParseKeys(configFile) keybinds, err := ParseMangoWCKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseMangoWCKeys failed: %v", err)
} }
if len(keybinds) < 14 { if len(keybinds) < 14 {

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
) )
func TestMangoWCProviderName(t *testing.T) { func TestMangoWCProviderName(t *testing.T) {
@@ -88,12 +86,12 @@ func TestMangoWCCategorizeByCommand(t *testing.T) {
func TestMangoWCFormatKey(t *testing.T) { func TestMangoWCFormatKey(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *mangowc.KeyBinding keybind *MangoWCKeyBinding
expected string expected string
}{ }{
{ {
name: "single_mod", name: "single_mod",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "q", Key: "q",
}, },
@@ -101,7 +99,7 @@ func TestMangoWCFormatKey(t *testing.T) {
}, },
{ {
name: "multiple_mods", name: "multiple_mods",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"SUPER", "SHIFT"}, Mods: []string{"SUPER", "SHIFT"},
Key: "Up", Key: "Up",
}, },
@@ -109,7 +107,7 @@ func TestMangoWCFormatKey(t *testing.T) {
}, },
{ {
name: "no_mods", name: "no_mods",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
}, },
@@ -131,13 +129,13 @@ func TestMangoWCFormatKey(t *testing.T) {
func TestMangoWCConvertKeybind(t *testing.T) { func TestMangoWCConvertKeybind(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *mangowc.KeyBinding keybind *MangoWCKeyBinding
wantKey string wantKey string
wantDesc string wantDesc string
}{ }{
{ {
name: "with_comment", name: "with_comment",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"ALT"}, Mods: []string{"ALT"},
Key: "t", Key: "t",
Command: "spawn", Command: "spawn",
@@ -149,7 +147,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
}, },
{ {
name: "without_comment", name: "without_comment",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"SUPER"}, Mods: []string{"SUPER"},
Key: "r", Key: "r",
Command: "reload_config", Command: "reload_config",
@@ -161,7 +159,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
}, },
{ {
name: "with_params_no_comment", name: "with_params_no_comment",
keybind: &mangowc.KeyBinding{ keybind: &MangoWCKeyBinding{
Mods: []string{"CTRL"}, Mods: []string{"CTRL"},
Key: "1", Key: "1",
Command: "view", Command: "view",

View File

@@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
) )
type SwayProvider struct { type SwayProvider struct {
@@ -26,7 +25,7 @@ func (s *SwayProvider) Name() string {
} }
func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := sway.ParseKeys(s.configPath) section, err := ParseSwayKeys(s.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse sway config: %w", err) return nil, fmt.Errorf("failed to parse sway config: %w", err)
} }
@@ -41,7 +40,7 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
}, nil }, nil
} }
func (s *SwayProvider) convertSection(section *sway.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (s *SwayProvider) convertSection(section *SwaySection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -89,7 +88,7 @@ func (s *SwayProvider) categorizeByCommand(command string) string {
} }
} }
func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) keybinds.Keybind { func (s *SwayProvider) convertKeybind(kb *SwayKeyBinding, subcategory string) keybinds.Keybind {
key := s.formatKey(kb) key := s.formatKey(kb)
desc := kb.Comment desc := kb.Comment
@@ -104,7 +103,7 @@ func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) k
} }
} }
func (s *SwayProvider) formatKey(kb *sway.KeyBinding) string { func (s *SwayProvider) formatKey(kb *SwayKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1) parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...) parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key) parts = append(parts, kb.Key)

View File

@@ -1,4 +1,4 @@
package sway package providers
import ( import (
"os" "os"
@@ -8,40 +8,40 @@ import (
) )
const ( const (
TitleRegex = "#+!" SwayTitleRegex = "#+!"
HideComment = "[hidden]" SwayHideComment = "[hidden]"
) )
var ModSeparators = []rune{'+', ' '} var SwayModSeparators = []rune{'+', ' '}
type KeyBinding struct { type SwayKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
Command string `json:"command"` Command string `json:"command"`
Comment string `json:"comment"` Comment string `json:"comment"`
} }
type Section struct { type SwaySection struct {
Children []Section `json:"children"` Children []SwaySection `json:"children"`
Keybinds []KeyBinding `json:"keybinds"` Keybinds []SwayKeyBinding `json:"keybinds"`
Name string `json:"name"` Name string `json:"name"`
} }
type Parser struct { type SwayParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
variables map[string]string variables map[string]string
} }
func NewParser() *Parser { func NewSwayParser() *SwayParser {
return &Parser{ return &SwayParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
variables: make(map[string]string), variables: make(map[string]string),
} }
} }
func (p *Parser) ReadContent(path string) error { func (p *SwayParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath := os.ExpandEnv(path)
expandedPath = filepath.Clean(expandedPath) expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") { if strings.HasPrefix(expandedPath, "~") {
@@ -88,7 +88,7 @@ func (p *Parser) ReadContent(path string) error {
return nil return nil
} }
func (p *Parser) parseVariables() { func (p *SwayParser) parseVariables() {
setRegex := regexp.MustCompile(`^\s*set\s+\$(\w+)\s+(.+)$`) setRegex := regexp.MustCompile(`^\s*set\s+\$(\w+)\s+(.+)$`)
for _, line := range p.contentLines { for _, line := range p.contentLines {
matches := setRegex.FindStringSubmatch(line) matches := setRegex.FindStringSubmatch(line)
@@ -100,7 +100,7 @@ func (p *Parser) parseVariables() {
} }
} }
func (p *Parser) expandVariables(text string) string { func (p *SwayParser) expandVariables(text string) string {
result := text result := text
for varName, varValue := range p.variables { for varName, varValue := range p.variables {
result = strings.ReplaceAll(result, "$"+varName, varValue) result = strings.ReplaceAll(result, "$"+varName, varValue)
@@ -108,7 +108,7 @@ func (p *Parser) expandVariables(text string) string {
return result return result
} }
func autogenerateComment(command string) string { func swayAutogenerateComment(command string) string {
command = strings.TrimSpace(command) command = strings.TrimSpace(command)
if strings.HasPrefix(command, "exec ") { if strings.HasPrefix(command, "exec ") {
@@ -200,7 +200,7 @@ func autogenerateComment(command string) string {
} }
} }
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding { func (p *SwayParser) getKeybindAtLine(lineNumber int) *SwayKeyBinding {
if lineNumber >= len(p.contentLines) { if lineNumber >= len(p.contentLines) {
return nil return nil
} }
@@ -223,7 +223,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
comment = strings.TrimSpace(parts[1]) comment = strings.TrimSpace(parts[1])
} }
if strings.HasPrefix(comment, HideComment) { if strings.HasPrefix(comment, SwayHideComment) {
return nil return nil
} }
@@ -249,11 +249,11 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
var modList []string var modList []string
var key string var key string
modstring := keyCombo + string(ModSeparators[0]) modstring := keyCombo + string(SwayModSeparators[0])
pos := 0 pos := 0
for index, char := range modstring { for index, char := range modstring {
isModSep := false isModSep := false
for _, sep := range ModSeparators { for _, sep := range SwayModSeparators {
if char == sep { if char == sep {
isModSep = true isModSep = true
break break
@@ -262,7 +262,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
if isModSep { if isModSep {
if index-pos > 0 { if index-pos > 0 {
part := modstring[pos:index] part := modstring[pos:index]
if isMod(part) { if swayIsMod(part) {
modList = append(modList, part) modList = append(modList, part)
} else { } else {
key = part key = part
@@ -273,12 +273,12 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
if comment == "" { if comment == "" {
comment = autogenerateComment(command) comment = swayAutogenerateComment(command)
} }
_ = flags _ = flags
return &KeyBinding{ return &SwayKeyBinding{
Mods: modList, Mods: modList,
Key: key, Key: key,
Command: command, Command: command,
@@ -286,7 +286,7 @@ func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
} }
} }
func isMod(s string) bool { func swayIsMod(s string) bool {
s = strings.ToLower(s) s = strings.ToLower(s)
if s == "mod1" || s == "mod2" || s == "mod3" || s == "mod4" || s == "mod5" || if s == "mod1" || s == "mod2" || s == "mod3" || s == "mod4" || s == "mod5" ||
s == "shift" || s == "control" || s == "ctrl" || s == "alt" || s == "super" || s == "shift" || s == "control" || s == "ctrl" || s == "alt" || s == "super" ||
@@ -307,8 +307,8 @@ func isMod(s string) bool {
return false return false
} }
func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section { func (p *SwayParser) getBindsRecursive(currentContent *SwaySection, scope int) *SwaySection {
titleRegex := regexp.MustCompile(TitleRegex) titleRegex := regexp.MustCompile(SwayTitleRegex)
for p.readingLine < len(p.contentLines) { for p.readingLine < len(p.contentLines) {
line := p.contentLines[p.readingLine] line := p.contentLines[p.readingLine]
@@ -325,9 +325,9 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
sectionName := strings.TrimSpace(line[headingScope+1:]) sectionName := strings.TrimSpace(line[headingScope+1:])
p.readingLine++ p.readingLine++
childSection := &Section{ childSection := &SwaySection{
Children: []Section{}, Children: []SwaySection{},
Keybinds: []KeyBinding{}, Keybinds: []SwayKeyBinding{},
Name: sectionName, Name: sectionName,
} }
result := p.getBindsRecursive(childSection, headingScope) result := p.getBindsRecursive(childSection, headingScope)
@@ -348,18 +348,18 @@ func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section
return currentContent return currentContent
} }
func (p *Parser) ParseKeys() *Section { func (p *SwayParser) ParseKeys() *SwaySection {
p.readingLine = 0 p.readingLine = 0
rootSection := &Section{ rootSection := &SwaySection{
Children: []Section{}, Children: []SwaySection{},
Keybinds: []KeyBinding{}, Keybinds: []SwayKeyBinding{},
Name: "", Name: "",
} }
return p.getBindsRecursive(rootSection, 0) return p.getBindsRecursive(rootSection, 0)
} }
func ParseKeys(path string) (*Section, error) { func ParseSwayKeys(path string) (*SwaySection, error) {
parser := NewParser() parser := NewSwayParser()
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,4 @@
package sway package providers
import ( import (
"os" "os"
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func TestAutogenerateComment(t *testing.T) { func TestSwayAutogenerateComment(t *testing.T) {
tests := []struct { tests := []struct {
command string command string
expected string expected string
@@ -46,25 +46,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) { t.Run(tt.command, func(t *testing.T) {
result := autogenerateComment(tt.command) result := swayAutogenerateComment(tt.command)
if result != tt.expected { if result != tt.expected {
t.Errorf("autogenerateComment(%q) = %q, want %q", t.Errorf("swayAutogenerateComment(%q) = %q, want %q",
tt.command, result, tt.expected) tt.command, result, tt.expected)
} }
}) })
} }
} }
func TestGetKeybindAtLine(t *testing.T) { func TestSwayGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *KeyBinding expected *SwayKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
line: "bindsym Mod4+q kill", line: "bindsym Mod4+q kill",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "q", Key: "q",
Command: "kill", Command: "kill",
@@ -74,7 +74,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_exec", name: "keybind_with_exec",
line: "bindsym Mod4+t exec kitty", line: "bindsym Mod4+t exec kitty",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "t", Key: "t",
Command: "exec kitty", Command: "exec kitty",
@@ -84,7 +84,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_comment", name: "keybind_with_comment",
line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher", line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "Space", Key: "Space",
Command: "exec dms ipc call spotlight toggle", Command: "exec dms ipc call spotlight toggle",
@@ -99,7 +99,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_multiple_mods", name: "keybind_multiple_mods",
line: "bindsym Mod4+Shift+e exit", line: "bindsym Mod4+Shift+e exit",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4", "Shift"}, Mods: []string{"Mod4", "Shift"},
Key: "e", Key: "e",
Command: "exit", Command: "exit",
@@ -109,7 +109,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_no_mods", name: "keybind_no_mods",
line: "bindsym Print exec grim screenshot.png", line: "bindsym Print exec grim screenshot.png",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
Command: "exec grim screenshot.png", Command: "exec grim screenshot.png",
@@ -119,7 +119,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_with_flags", name: "keybind_with_flags",
line: "bindsym --release Mod4+x exec notify-send released", line: "bindsym --release Mod4+x exec notify-send released",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "x", Key: "x",
Command: "exec notify-send released", Command: "exec notify-send released",
@@ -129,7 +129,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_focus_direction", name: "keybind_focus_direction",
line: "bindsym Mod4+Left focus left", line: "bindsym Mod4+Left focus left",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "Left", Key: "Left",
Command: "focus left", Command: "focus left",
@@ -139,7 +139,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{ {
name: "keybind_workspace", name: "keybind_workspace",
line: "bindsym Mod4+1 workspace number 1", line: "bindsym Mod4+1 workspace number 1",
expected: &KeyBinding{ expected: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "1", Key: "1",
Command: "workspace number 1", Command: "workspace number 1",
@@ -150,7 +150,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewParser() parser := NewSwayParser()
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -188,7 +188,7 @@ func TestGetKeybindAtLine(t *testing.T) {
} }
} }
func TestVariableExpansion(t *testing.T) { func TestSwayVariableExpansion(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -204,9 +204,9 @@ bindsym $mod+d exec $menu
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(configFile) section, err := ParseSwayKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Keybinds) != 2 { if len(section.Keybinds) != 2 {
@@ -229,7 +229,7 @@ bindsym $mod+d exec $menu
} }
} }
func TestParseKeysWithSections(t *testing.T) { func TestSwayParseKeysWithSections(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -251,9 +251,9 @@ bindsym $mod+t exec kitty # Terminal
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(tmpDir) section, err := ParseSwayKeys(tmpDir)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Children) != 2 { if len(section.Children) != 2 {
@@ -296,7 +296,7 @@ bindsym $mod+t exec kitty # Terminal
} }
} }
func TestReadContentErrors(t *testing.T) { func TestSwayReadContentErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
@@ -313,7 +313,7 @@ func TestReadContentErrors(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, err := ParseKeys(tt.path) _, err := ParseSwayKeys(tt.path)
if err == nil { if err == nil {
t.Error("Expected error, got nil") t.Error("Expected error, got nil")
} }
@@ -321,7 +321,7 @@ func TestReadContentErrors(t *testing.T) {
} }
} }
func TestReadContentWithTildeExpansion(t *testing.T) { func TestSwayReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
t.Skip("Cannot get home directory") t.Skip("Cannot get home directory")
@@ -343,7 +343,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewParser() parser := NewSwayParser()
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -352,7 +352,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
} }
} }
func TestEmptyAndCommentLines(t *testing.T) { func TestSwayEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -369,9 +369,9 @@ bindsym Mod4+t exec kitty
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(configFile) section, err := ParseSwayKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Keybinds) != 2 { if len(section.Keybinds) != 2 {
@@ -379,7 +379,7 @@ bindsym Mod4+t exec kitty
} }
} }
func TestRealWorldConfig(t *testing.T) { func TestSwayRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config") configFile := filepath.Join(tmpDir, "config")
@@ -408,9 +408,9 @@ bindsym $mod+Shift+1 move container to workspace number 1
t.Fatalf("Failed to write test config: %v", err) t.Fatalf("Failed to write test config: %v", err)
} }
section, err := ParseKeys(configFile) section, err := ParseSwayKeys(configFile)
if err != nil { if err != nil {
t.Fatalf("ParseKeys failed: %v", err) t.Fatalf("ParseSwayKeys failed: %v", err)
} }
if len(section.Keybinds) < 9 { if len(section.Keybinds) < 9 {
@@ -444,7 +444,7 @@ bindsym $mod+Shift+1 move container to workspace number 1
} }
} }
func TestIsMod(t *testing.T) { func TestSwayIsMod(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
expected bool expected bool
@@ -462,9 +462,9 @@ func TestIsMod(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) { t.Run(tt.input, func(t *testing.T) {
result := isMod(tt.input) result := swayIsMod(tt.input)
if result != tt.expected { if result != tt.expected {
t.Errorf("isMod(%q) = %v, want %v", tt.input, result, tt.expected) t.Errorf("swayIsMod(%q) = %v, want %v", tt.input, result, tt.expected)
} }
}) })
} }

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
) )
func TestSwayProviderName(t *testing.T) { func TestSwayProviderName(t *testing.T) {
@@ -76,12 +74,12 @@ func TestSwayCategorizeByCommand(t *testing.T) {
func TestSwayFormatKey(t *testing.T) { func TestSwayFormatKey(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *sway.KeyBinding keybind *SwayKeyBinding
expected string expected string
}{ }{
{ {
name: "single_mod", name: "single_mod",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "q", Key: "q",
}, },
@@ -89,7 +87,7 @@ func TestSwayFormatKey(t *testing.T) {
}, },
{ {
name: "multiple_mods", name: "multiple_mods",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4", "Shift"}, Mods: []string{"Mod4", "Shift"},
Key: "e", Key: "e",
}, },
@@ -97,7 +95,7 @@ func TestSwayFormatKey(t *testing.T) {
}, },
{ {
name: "no_mods", name: "no_mods",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{}, Mods: []string{},
Key: "Print", Key: "Print",
}, },
@@ -119,13 +117,13 @@ func TestSwayFormatKey(t *testing.T) {
func TestSwayConvertKeybind(t *testing.T) { func TestSwayConvertKeybind(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
keybind *sway.KeyBinding keybind *SwayKeyBinding
wantKey string wantKey string
wantDesc string wantDesc string
}{ }{
{ {
name: "with_comment", name: "with_comment",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "t", Key: "t",
Command: "exec kitty", Command: "exec kitty",
@@ -136,7 +134,7 @@ func TestSwayConvertKeybind(t *testing.T) {
}, },
{ {
name: "without_comment", name: "without_comment",
keybind: &sway.KeyBinding{ keybind: &SwayKeyBinding{
Mods: []string{"Mod4"}, Mods: []string{"Mod4"},
Key: "r", Key: "r",
Command: "reload", Command: "reload",

View File

@@ -1,4 +1,4 @@
package logger package log
import ( import (
"bufio" "bufio"
@@ -64,7 +64,7 @@ func (l *FileLogger) writeToFile(message string) {
redacted := l.redactPassword(message) redacted := l.redactPassword(message)
timestamp := time.Now().Format("15:04:05.000") timestamp := time.Now().Format("15:04:05.000")
l.writer.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, redacted)) l.writer.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, redacted)) //nolint:errcheck
l.writer.Flush() l.writer.Flush()
} }
@@ -93,7 +93,7 @@ func (l *FileLogger) Close() error {
defer l.mu.Unlock() defer l.mu.Unlock()
footer := fmt.Sprintf("\n=== DankInstall Log End ===\nCompleted: %s\n", time.Now().Format(time.RFC3339)) footer := fmt.Sprintf("\n=== DankInstall Log End ===\nCompleted: %s\n", time.Now().Format(time.RFC3339))
l.writer.WriteString(footer) l.writer.WriteString(footer) //nolint:errcheck
l.writer.Flush() l.writer.Flush()
if err := l.file.Sync(); err != nil { if err := l.file.Sync(); err != nil {

View File

@@ -22,6 +22,99 @@ func (_m *MockCUPSClientInterface) EXPECT() *MockCUPSClientInterface_Expecter {
return &MockCUPSClientInterface_Expecter{mock: &_m.Mock} return &MockCUPSClientInterface_Expecter{mock: &_m.Mock}
} }
// AcceptJobs provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) AcceptJobs(printer string) error {
ret := _m.Called(printer)
if len(ret) == 0 {
panic("no return value specified for AcceptJobs")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_AcceptJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AcceptJobs'
type MockCUPSClientInterface_AcceptJobs_Call struct {
*mock.Call
}
// AcceptJobs is a helper method to define mock.On call
// - printer string
func (_e *MockCUPSClientInterface_Expecter) AcceptJobs(printer interface{}) *MockCUPSClientInterface_AcceptJobs_Call {
return &MockCUPSClientInterface_AcceptJobs_Call{Call: _e.mock.On("AcceptJobs", printer)}
}
func (_c *MockCUPSClientInterface_AcceptJobs_Call) Run(run func(printer string)) *MockCUPSClientInterface_AcceptJobs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_AcceptJobs_Call) Return(_a0 error) *MockCUPSClientInterface_AcceptJobs_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_AcceptJobs_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_AcceptJobs_Call {
_c.Call.Return(run)
return _c
}
// AddPrinterToClass provides a mock function with given fields: class, printer
func (_m *MockCUPSClientInterface) AddPrinterToClass(class string, printer string) error {
ret := _m.Called(class, printer)
if len(ret) == 0 {
panic("no return value specified for AddPrinterToClass")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(class, printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_AddPrinterToClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddPrinterToClass'
type MockCUPSClientInterface_AddPrinterToClass_Call struct {
*mock.Call
}
// AddPrinterToClass is a helper method to define mock.On call
// - class string
// - printer string
func (_e *MockCUPSClientInterface_Expecter) AddPrinterToClass(class interface{}, printer interface{}) *MockCUPSClientInterface_AddPrinterToClass_Call {
return &MockCUPSClientInterface_AddPrinterToClass_Call{Call: _e.mock.On("AddPrinterToClass", class, printer)}
}
func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) Run(run func(class string, printer string)) *MockCUPSClientInterface_AddPrinterToClass_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) Return(_a0 error) *MockCUPSClientInterface_AddPrinterToClass_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_AddPrinterToClass_Call {
_c.Call.Return(run)
return _c
}
// CancelAllJob provides a mock function with given fields: printer, purge // CancelAllJob provides a mock function with given fields: printer, purge
func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error { func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error {
ret := _m.Called(printer, purge) ret := _m.Called(printer, purge)
@@ -116,6 +209,312 @@ func (_c *MockCUPSClientInterface_CancelJob_Call) RunAndReturn(run func(int, boo
return _c return _c
} }
// CreatePrinter provides a mock function with given fields: name, deviceURI, ppd, shared, errorPolicy, information, location
func (_m *MockCUPSClientInterface) CreatePrinter(name string, deviceURI string, ppd string, shared bool, errorPolicy string, information string, location string) error {
ret := _m.Called(name, deviceURI, ppd, shared, errorPolicy, information, location)
if len(ret) == 0 {
panic("no return value specified for CreatePrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, bool, string, string, string) error); ok {
r0 = rf(name, deviceURI, ppd, shared, errorPolicy, information, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_CreatePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrinter'
type MockCUPSClientInterface_CreatePrinter_Call struct {
*mock.Call
}
// CreatePrinter is a helper method to define mock.On call
// - name string
// - deviceURI string
// - ppd string
// - shared bool
// - errorPolicy string
// - information string
// - location string
func (_e *MockCUPSClientInterface_Expecter) CreatePrinter(name interface{}, deviceURI interface{}, ppd interface{}, shared interface{}, errorPolicy interface{}, information interface{}, location interface{}) *MockCUPSClientInterface_CreatePrinter_Call {
return &MockCUPSClientInterface_CreatePrinter_Call{Call: _e.mock.On("CreatePrinter", name, deviceURI, ppd, shared, errorPolicy, information, location)}
}
func (_c *MockCUPSClientInterface_CreatePrinter_Call) Run(run func(name string, deviceURI string, ppd string, shared bool, errorPolicy string, information string, location string)) *MockCUPSClientInterface_CreatePrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool), args[4].(string), args[5].(string), args[6].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_CreatePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_CreatePrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_CreatePrinter_Call) RunAndReturn(run func(string, string, string, bool, string, string, string) error) *MockCUPSClientInterface_CreatePrinter_Call {
_c.Call.Return(run)
return _c
}
// DeleteClass provides a mock function with given fields: class
func (_m *MockCUPSClientInterface) DeleteClass(class string) error {
ret := _m.Called(class)
if len(ret) == 0 {
panic("no return value specified for DeleteClass")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(class)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_DeleteClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteClass'
type MockCUPSClientInterface_DeleteClass_Call struct {
*mock.Call
}
// DeleteClass is a helper method to define mock.On call
// - class string
func (_e *MockCUPSClientInterface_Expecter) DeleteClass(class interface{}) *MockCUPSClientInterface_DeleteClass_Call {
return &MockCUPSClientInterface_DeleteClass_Call{Call: _e.mock.On("DeleteClass", class)}
}
func (_c *MockCUPSClientInterface_DeleteClass_Call) Run(run func(class string)) *MockCUPSClientInterface_DeleteClass_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_DeleteClass_Call) Return(_a0 error) *MockCUPSClientInterface_DeleteClass_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_DeleteClass_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_DeleteClass_Call {
_c.Call.Return(run)
return _c
}
// DeletePrinter provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) DeletePrinter(printer string) error {
ret := _m.Called(printer)
if len(ret) == 0 {
panic("no return value specified for DeletePrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_DeletePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePrinter'
type MockCUPSClientInterface_DeletePrinter_Call struct {
*mock.Call
}
// DeletePrinter is a helper method to define mock.On call
// - printer string
func (_e *MockCUPSClientInterface_Expecter) DeletePrinter(printer interface{}) *MockCUPSClientInterface_DeletePrinter_Call {
return &MockCUPSClientInterface_DeletePrinter_Call{Call: _e.mock.On("DeletePrinter", printer)}
}
func (_c *MockCUPSClientInterface_DeletePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_DeletePrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_DeletePrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_DeletePrinter_Call {
_c.Call.Return(run)
return _c
}
// DeletePrinterFromClass provides a mock function with given fields: class, printer
func (_m *MockCUPSClientInterface) DeletePrinterFromClass(class string, printer string) error {
ret := _m.Called(class, printer)
if len(ret) == 0 {
panic("no return value specified for DeletePrinterFromClass")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(class, printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_DeletePrinterFromClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePrinterFromClass'
type MockCUPSClientInterface_DeletePrinterFromClass_Call struct {
*mock.Call
}
// DeletePrinterFromClass is a helper method to define mock.On call
// - class string
// - printer string
func (_e *MockCUPSClientInterface_Expecter) DeletePrinterFromClass(class interface{}, printer interface{}) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
return &MockCUPSClientInterface_DeletePrinterFromClass_Call{Call: _e.mock.On("DeletePrinterFromClass", class, printer)}
}
func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) Run(run func(class string, printer string)) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) Return(_a0 error) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
_c.Call.Return(run)
return _c
}
// GetClasses provides a mock function with given fields: attributes
func (_m *MockCUPSClientInterface) GetClasses(attributes []string) (map[string]ipp.Attributes, error) {
ret := _m.Called(attributes)
if len(ret) == 0 {
panic("no return value specified for GetClasses")
}
var r0 map[string]ipp.Attributes
var r1 error
if rf, ok := ret.Get(0).(func([]string) (map[string]ipp.Attributes, error)); ok {
return rf(attributes)
}
if rf, ok := ret.Get(0).(func([]string) map[string]ipp.Attributes); ok {
r0 = rf(attributes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]ipp.Attributes)
}
}
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(attributes)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_GetClasses_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClasses'
type MockCUPSClientInterface_GetClasses_Call struct {
*mock.Call
}
// GetClasses is a helper method to define mock.On call
// - attributes []string
func (_e *MockCUPSClientInterface_Expecter) GetClasses(attributes interface{}) *MockCUPSClientInterface_GetClasses_Call {
return &MockCUPSClientInterface_GetClasses_Call{Call: _e.mock.On("GetClasses", attributes)}
}
func (_c *MockCUPSClientInterface_GetClasses_Call) Run(run func(attributes []string)) *MockCUPSClientInterface_GetClasses_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]string))
})
return _c
}
func (_c *MockCUPSClientInterface_GetClasses_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetClasses_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_GetClasses_Call) RunAndReturn(run func([]string) (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetClasses_Call {
_c.Call.Return(run)
return _c
}
// GetDevices provides a mock function with no fields
func (_m *MockCUPSClientInterface) GetDevices() (map[string]ipp.Attributes, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetDevices")
}
var r0 map[string]ipp.Attributes
var r1 error
if rf, ok := ret.Get(0).(func() (map[string]ipp.Attributes, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() map[string]ipp.Attributes); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]ipp.Attributes)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_GetDevices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDevices'
type MockCUPSClientInterface_GetDevices_Call struct {
*mock.Call
}
// GetDevices is a helper method to define mock.On call
func (_e *MockCUPSClientInterface_Expecter) GetDevices() *MockCUPSClientInterface_GetDevices_Call {
return &MockCUPSClientInterface_GetDevices_Call{Call: _e.mock.On("GetDevices")}
}
func (_c *MockCUPSClientInterface_GetDevices_Call) Run(run func()) *MockCUPSClientInterface_GetDevices_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCUPSClientInterface_GetDevices_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetDevices_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_GetDevices_Call) RunAndReturn(run func() (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetDevices_Call {
_c.Call.Return(run)
return _c
}
// GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes // GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes
func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) { func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) {
ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes) ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
@@ -180,6 +579,63 @@ func (_c *MockCUPSClientInterface_GetJobs_Call) RunAndReturn(run func(string, st
return _c return _c
} }
// GetPPDs provides a mock function with no fields
func (_m *MockCUPSClientInterface) GetPPDs() (map[string]ipp.Attributes, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetPPDs")
}
var r0 map[string]ipp.Attributes
var r1 error
if rf, ok := ret.Get(0).(func() (map[string]ipp.Attributes, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() map[string]ipp.Attributes); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]ipp.Attributes)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_GetPPDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPPDs'
type MockCUPSClientInterface_GetPPDs_Call struct {
*mock.Call
}
// GetPPDs is a helper method to define mock.On call
func (_e *MockCUPSClientInterface_Expecter) GetPPDs() *MockCUPSClientInterface_GetPPDs_Call {
return &MockCUPSClientInterface_GetPPDs_Call{Call: _e.mock.On("GetPPDs")}
}
func (_c *MockCUPSClientInterface_GetPPDs_Call) Run(run func()) *MockCUPSClientInterface_GetPPDs_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCUPSClientInterface_GetPPDs_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetPPDs_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_GetPPDs_Call) RunAndReturn(run func() (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetPPDs_Call {
_c.Call.Return(run)
return _c
}
// GetPrinters provides a mock function with given fields: attributes // GetPrinters provides a mock function with given fields: attributes
func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) { func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) {
ret := _m.Called(attributes) ret := _m.Called(attributes)
@@ -238,6 +694,100 @@ func (_c *MockCUPSClientInterface_GetPrinters_Call) RunAndReturn(run func([]stri
return _c return _c
} }
// HoldJobUntil provides a mock function with given fields: jobID, holdUntil
func (_m *MockCUPSClientInterface) HoldJobUntil(jobID int, holdUntil string) error {
ret := _m.Called(jobID, holdUntil)
if len(ret) == 0 {
panic("no return value specified for HoldJobUntil")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(jobID, holdUntil)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_HoldJobUntil_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HoldJobUntil'
type MockCUPSClientInterface_HoldJobUntil_Call struct {
*mock.Call
}
// HoldJobUntil is a helper method to define mock.On call
// - jobID int
// - holdUntil string
func (_e *MockCUPSClientInterface_Expecter) HoldJobUntil(jobID interface{}, holdUntil interface{}) *MockCUPSClientInterface_HoldJobUntil_Call {
return &MockCUPSClientInterface_HoldJobUntil_Call{Call: _e.mock.On("HoldJobUntil", jobID, holdUntil)}
}
func (_c *MockCUPSClientInterface_HoldJobUntil_Call) Run(run func(jobID int, holdUntil string)) *MockCUPSClientInterface_HoldJobUntil_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_HoldJobUntil_Call) Return(_a0 error) *MockCUPSClientInterface_HoldJobUntil_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_HoldJobUntil_Call) RunAndReturn(run func(int, string) error) *MockCUPSClientInterface_HoldJobUntil_Call {
_c.Call.Return(run)
return _c
}
// MoveJob provides a mock function with given fields: jobID, destPrinter
func (_m *MockCUPSClientInterface) MoveJob(jobID int, destPrinter string) error {
ret := _m.Called(jobID, destPrinter)
if len(ret) == 0 {
panic("no return value specified for MoveJob")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(jobID, destPrinter)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_MoveJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MoveJob'
type MockCUPSClientInterface_MoveJob_Call struct {
*mock.Call
}
// MoveJob is a helper method to define mock.On call
// - jobID int
// - destPrinter string
func (_e *MockCUPSClientInterface_Expecter) MoveJob(jobID interface{}, destPrinter interface{}) *MockCUPSClientInterface_MoveJob_Call {
return &MockCUPSClientInterface_MoveJob_Call{Call: _e.mock.On("MoveJob", jobID, destPrinter)}
}
func (_c *MockCUPSClientInterface_MoveJob_Call) Run(run func(jobID int, destPrinter string)) *MockCUPSClientInterface_MoveJob_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_MoveJob_Call) Return(_a0 error) *MockCUPSClientInterface_MoveJob_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_MoveJob_Call) RunAndReturn(run func(int, string) error) *MockCUPSClientInterface_MoveJob_Call {
_c.Call.Return(run)
return _c
}
// PausePrinter provides a mock function with given fields: printer // PausePrinter provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) PausePrinter(printer string) error { func (_m *MockCUPSClientInterface) PausePrinter(printer string) error {
ret := _m.Called(printer) ret := _m.Called(printer)
@@ -284,6 +834,156 @@ func (_c *MockCUPSClientInterface_PausePrinter_Call) RunAndReturn(run func(strin
return _c return _c
} }
// PrintTestPage provides a mock function with given fields: printer, testPageData, size
func (_m *MockCUPSClientInterface) PrintTestPage(printer string, testPageData io.Reader, size int) (int, error) {
ret := _m.Called(printer, testPageData, size)
if len(ret) == 0 {
panic("no return value specified for PrintTestPage")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func(string, io.Reader, int) (int, error)); ok {
return rf(printer, testPageData, size)
}
if rf, ok := ret.Get(0).(func(string, io.Reader, int) int); ok {
r0 = rf(printer, testPageData, size)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func(string, io.Reader, int) error); ok {
r1 = rf(printer, testPageData, size)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_PrintTestPage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrintTestPage'
type MockCUPSClientInterface_PrintTestPage_Call struct {
*mock.Call
}
// PrintTestPage is a helper method to define mock.On call
// - printer string
// - testPageData io.Reader
// - size int
func (_e *MockCUPSClientInterface_Expecter) PrintTestPage(printer interface{}, testPageData interface{}, size interface{}) *MockCUPSClientInterface_PrintTestPage_Call {
return &MockCUPSClientInterface_PrintTestPage_Call{Call: _e.mock.On("PrintTestPage", printer, testPageData, size)}
}
func (_c *MockCUPSClientInterface_PrintTestPage_Call) Run(run func(printer string, testPageData io.Reader, size int)) *MockCUPSClientInterface_PrintTestPage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(io.Reader), args[2].(int))
})
return _c
}
func (_c *MockCUPSClientInterface_PrintTestPage_Call) Return(_a0 int, _a1 error) *MockCUPSClientInterface_PrintTestPage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_PrintTestPage_Call) RunAndReturn(run func(string, io.Reader, int) (int, error)) *MockCUPSClientInterface_PrintTestPage_Call {
_c.Call.Return(run)
return _c
}
// RejectJobs provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) RejectJobs(printer string) error {
ret := _m.Called(printer)
if len(ret) == 0 {
panic("no return value specified for RejectJobs")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_RejectJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RejectJobs'
type MockCUPSClientInterface_RejectJobs_Call struct {
*mock.Call
}
// RejectJobs is a helper method to define mock.On call
// - printer string
func (_e *MockCUPSClientInterface_Expecter) RejectJobs(printer interface{}) *MockCUPSClientInterface_RejectJobs_Call {
return &MockCUPSClientInterface_RejectJobs_Call{Call: _e.mock.On("RejectJobs", printer)}
}
func (_c *MockCUPSClientInterface_RejectJobs_Call) Run(run func(printer string)) *MockCUPSClientInterface_RejectJobs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_RejectJobs_Call) Return(_a0 error) *MockCUPSClientInterface_RejectJobs_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_RejectJobs_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_RejectJobs_Call {
_c.Call.Return(run)
return _c
}
// RestartJob provides a mock function with given fields: jobID
func (_m *MockCUPSClientInterface) RestartJob(jobID int) error {
ret := _m.Called(jobID)
if len(ret) == 0 {
panic("no return value specified for RestartJob")
}
var r0 error
if rf, ok := ret.Get(0).(func(int) error); ok {
r0 = rf(jobID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_RestartJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestartJob'
type MockCUPSClientInterface_RestartJob_Call struct {
*mock.Call
}
// RestartJob is a helper method to define mock.On call
// - jobID int
func (_e *MockCUPSClientInterface_Expecter) RestartJob(jobID interface{}) *MockCUPSClientInterface_RestartJob_Call {
return &MockCUPSClientInterface_RestartJob_Call{Call: _e.mock.On("RestartJob", jobID)}
}
func (_c *MockCUPSClientInterface_RestartJob_Call) Run(run func(jobID int)) *MockCUPSClientInterface_RestartJob_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockCUPSClientInterface_RestartJob_Call) Return(_a0 error) *MockCUPSClientInterface_RestartJob_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_RestartJob_Call) RunAndReturn(run func(int) error) *MockCUPSClientInterface_RestartJob_Call {
_c.Call.Return(run)
return _c
}
// ResumePrinter provides a mock function with given fields: printer // ResumePrinter provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error { func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error {
ret := _m.Called(printer) ret := _m.Called(printer)
@@ -390,6 +1090,147 @@ func (_c *MockCUPSClientInterface_SendRequest_Call) RunAndReturn(run func(string
return _c return _c
} }
// SetPrinterInformation provides a mock function with given fields: printer, information
func (_m *MockCUPSClientInterface) SetPrinterInformation(printer string, information string) error {
ret := _m.Called(printer, information)
if len(ret) == 0 {
panic("no return value specified for SetPrinterInformation")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(printer, information)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_SetPrinterInformation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterInformation'
type MockCUPSClientInterface_SetPrinterInformation_Call struct {
*mock.Call
}
// SetPrinterInformation is a helper method to define mock.On call
// - printer string
// - information string
func (_e *MockCUPSClientInterface_Expecter) SetPrinterInformation(printer interface{}, information interface{}) *MockCUPSClientInterface_SetPrinterInformation_Call {
return &MockCUPSClientInterface_SetPrinterInformation_Call{Call: _e.mock.On("SetPrinterInformation", printer, information)}
}
func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) Run(run func(printer string, information string)) *MockCUPSClientInterface_SetPrinterInformation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterInformation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_SetPrinterInformation_Call {
_c.Call.Return(run)
return _c
}
// SetPrinterIsShared provides a mock function with given fields: printer, shared
func (_m *MockCUPSClientInterface) SetPrinterIsShared(printer string, shared bool) error {
ret := _m.Called(printer, shared)
if len(ret) == 0 {
panic("no return value specified for SetPrinterIsShared")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(printer, shared)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_SetPrinterIsShared_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterIsShared'
type MockCUPSClientInterface_SetPrinterIsShared_Call struct {
*mock.Call
}
// SetPrinterIsShared is a helper method to define mock.On call
// - printer string
// - shared bool
func (_e *MockCUPSClientInterface_Expecter) SetPrinterIsShared(printer interface{}, shared interface{}) *MockCUPSClientInterface_SetPrinterIsShared_Call {
return &MockCUPSClientInterface_SetPrinterIsShared_Call{Call: _e.mock.On("SetPrinterIsShared", printer, shared)}
}
func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) Run(run func(printer string, shared bool)) *MockCUPSClientInterface_SetPrinterIsShared_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterIsShared_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) RunAndReturn(run func(string, bool) error) *MockCUPSClientInterface_SetPrinterIsShared_Call {
_c.Call.Return(run)
return _c
}
// SetPrinterLocation provides a mock function with given fields: printer, location
func (_m *MockCUPSClientInterface) SetPrinterLocation(printer string, location string) error {
ret := _m.Called(printer, location)
if len(ret) == 0 {
panic("no return value specified for SetPrinterLocation")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(printer, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_SetPrinterLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterLocation'
type MockCUPSClientInterface_SetPrinterLocation_Call struct {
*mock.Call
}
// SetPrinterLocation is a helper method to define mock.On call
// - printer string
// - location string
func (_e *MockCUPSClientInterface_Expecter) SetPrinterLocation(printer interface{}, location interface{}) *MockCUPSClientInterface_SetPrinterLocation_Call {
return &MockCUPSClientInterface_SetPrinterLocation_Call{Call: _e.mock.On("SetPrinterLocation", printer, location)}
}
func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) Run(run func(printer string, location string)) *MockCUPSClientInterface_SetPrinterLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterLocation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_SetPrinterLocation_Call {
_c.Call.Return(run)
return _c
}
// NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value. // The first argument is typically a *testing.T value.
func NewMockCUPSClientInterface(t interface { func NewMockCUPSClientInterface(t interface {

View File

@@ -0,0 +1,708 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_cups_pkhelper
import (
cups "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
mock "github.com/stretchr/testify/mock"
)
// MockPkHelper is an autogenerated mock type for the PkHelper type
type MockPkHelper struct {
mock.Mock
}
type MockPkHelper_Expecter struct {
mock *mock.Mock
}
func (_m *MockPkHelper) EXPECT() *MockPkHelper_Expecter {
return &MockPkHelper_Expecter{mock: &_m.Mock}
}
// ClassAddPrinter provides a mock function with given fields: className, printerName
func (_m *MockPkHelper) ClassAddPrinter(className string, printerName string) error {
ret := _m.Called(className, printerName)
if len(ret) == 0 {
panic("no return value specified for ClassAddPrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(className, printerName)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_ClassAddPrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassAddPrinter'
type MockPkHelper_ClassAddPrinter_Call struct {
*mock.Call
}
// ClassAddPrinter is a helper method to define mock.On call
// - className string
// - printerName string
func (_e *MockPkHelper_Expecter) ClassAddPrinter(className interface{}, printerName interface{}) *MockPkHelper_ClassAddPrinter_Call {
return &MockPkHelper_ClassAddPrinter_Call{Call: _e.mock.On("ClassAddPrinter", className, printerName)}
}
func (_c *MockPkHelper_ClassAddPrinter_Call) Run(run func(className string, printerName string)) *MockPkHelper_ClassAddPrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_ClassAddPrinter_Call) Return(_a0 error) *MockPkHelper_ClassAddPrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_ClassAddPrinter_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_ClassAddPrinter_Call {
_c.Call.Return(run)
return _c
}
// ClassDelete provides a mock function with given fields: className
func (_m *MockPkHelper) ClassDelete(className string) error {
ret := _m.Called(className)
if len(ret) == 0 {
panic("no return value specified for ClassDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(className)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_ClassDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassDelete'
type MockPkHelper_ClassDelete_Call struct {
*mock.Call
}
// ClassDelete is a helper method to define mock.On call
// - className string
func (_e *MockPkHelper_Expecter) ClassDelete(className interface{}) *MockPkHelper_ClassDelete_Call {
return &MockPkHelper_ClassDelete_Call{Call: _e.mock.On("ClassDelete", className)}
}
func (_c *MockPkHelper_ClassDelete_Call) Run(run func(className string)) *MockPkHelper_ClassDelete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockPkHelper_ClassDelete_Call) Return(_a0 error) *MockPkHelper_ClassDelete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_ClassDelete_Call) RunAndReturn(run func(string) error) *MockPkHelper_ClassDelete_Call {
_c.Call.Return(run)
return _c
}
// ClassDeletePrinter provides a mock function with given fields: className, printerName
func (_m *MockPkHelper) ClassDeletePrinter(className string, printerName string) error {
ret := _m.Called(className, printerName)
if len(ret) == 0 {
panic("no return value specified for ClassDeletePrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(className, printerName)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_ClassDeletePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassDeletePrinter'
type MockPkHelper_ClassDeletePrinter_Call struct {
*mock.Call
}
// ClassDeletePrinter is a helper method to define mock.On call
// - className string
// - printerName string
func (_e *MockPkHelper_Expecter) ClassDeletePrinter(className interface{}, printerName interface{}) *MockPkHelper_ClassDeletePrinter_Call {
return &MockPkHelper_ClassDeletePrinter_Call{Call: _e.mock.On("ClassDeletePrinter", className, printerName)}
}
func (_c *MockPkHelper_ClassDeletePrinter_Call) Run(run func(className string, printerName string)) *MockPkHelper_ClassDeletePrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_ClassDeletePrinter_Call) Return(_a0 error) *MockPkHelper_ClassDeletePrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_ClassDeletePrinter_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_ClassDeletePrinter_Call {
_c.Call.Return(run)
return _c
}
// DevicesGet provides a mock function with given fields: timeout, limit, includeSchemes, excludeSchemes
func (_m *MockPkHelper) DevicesGet(timeout int, limit int, includeSchemes []string, excludeSchemes []string) ([]cups.Device, error) {
ret := _m.Called(timeout, limit, includeSchemes, excludeSchemes)
if len(ret) == 0 {
panic("no return value specified for DevicesGet")
}
var r0 []cups.Device
var r1 error
if rf, ok := ret.Get(0).(func(int, int, []string, []string) ([]cups.Device, error)); ok {
return rf(timeout, limit, includeSchemes, excludeSchemes)
}
if rf, ok := ret.Get(0).(func(int, int, []string, []string) []cups.Device); ok {
r0 = rf(timeout, limit, includeSchemes, excludeSchemes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]cups.Device)
}
}
if rf, ok := ret.Get(1).(func(int, int, []string, []string) error); ok {
r1 = rf(timeout, limit, includeSchemes, excludeSchemes)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockPkHelper_DevicesGet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DevicesGet'
type MockPkHelper_DevicesGet_Call struct {
*mock.Call
}
// DevicesGet is a helper method to define mock.On call
// - timeout int
// - limit int
// - includeSchemes []string
// - excludeSchemes []string
func (_e *MockPkHelper_Expecter) DevicesGet(timeout interface{}, limit interface{}, includeSchemes interface{}, excludeSchemes interface{}) *MockPkHelper_DevicesGet_Call {
return &MockPkHelper_DevicesGet_Call{Call: _e.mock.On("DevicesGet", timeout, limit, includeSchemes, excludeSchemes)}
}
func (_c *MockPkHelper_DevicesGet_Call) Run(run func(timeout int, limit int, includeSchemes []string, excludeSchemes []string)) *MockPkHelper_DevicesGet_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(int), args[2].([]string), args[3].([]string))
})
return _c
}
func (_c *MockPkHelper_DevicesGet_Call) Return(_a0 []cups.Device, _a1 error) *MockPkHelper_DevicesGet_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockPkHelper_DevicesGet_Call) RunAndReturn(run func(int, int, []string, []string) ([]cups.Device, error)) *MockPkHelper_DevicesGet_Call {
_c.Call.Return(run)
return _c
}
// JobCancelPurge provides a mock function with given fields: jobID, purge
func (_m *MockPkHelper) JobCancelPurge(jobID int, purge bool) error {
ret := _m.Called(jobID, purge)
if len(ret) == 0 {
panic("no return value specified for JobCancelPurge")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, bool) error); ok {
r0 = rf(jobID, purge)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_JobCancelPurge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobCancelPurge'
type MockPkHelper_JobCancelPurge_Call struct {
*mock.Call
}
// JobCancelPurge is a helper method to define mock.On call
// - jobID int
// - purge bool
func (_e *MockPkHelper_Expecter) JobCancelPurge(jobID interface{}, purge interface{}) *MockPkHelper_JobCancelPurge_Call {
return &MockPkHelper_JobCancelPurge_Call{Call: _e.mock.On("JobCancelPurge", jobID, purge)}
}
func (_c *MockPkHelper_JobCancelPurge_Call) Run(run func(jobID int, purge bool)) *MockPkHelper_JobCancelPurge_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(bool))
})
return _c
}
func (_c *MockPkHelper_JobCancelPurge_Call) Return(_a0 error) *MockPkHelper_JobCancelPurge_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_JobCancelPurge_Call) RunAndReturn(run func(int, bool) error) *MockPkHelper_JobCancelPurge_Call {
_c.Call.Return(run)
return _c
}
// JobRestart provides a mock function with given fields: jobID
func (_m *MockPkHelper) JobRestart(jobID int) error {
ret := _m.Called(jobID)
if len(ret) == 0 {
panic("no return value specified for JobRestart")
}
var r0 error
if rf, ok := ret.Get(0).(func(int) error); ok {
r0 = rf(jobID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_JobRestart_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobRestart'
type MockPkHelper_JobRestart_Call struct {
*mock.Call
}
// JobRestart is a helper method to define mock.On call
// - jobID int
func (_e *MockPkHelper_Expecter) JobRestart(jobID interface{}) *MockPkHelper_JobRestart_Call {
return &MockPkHelper_JobRestart_Call{Call: _e.mock.On("JobRestart", jobID)}
}
func (_c *MockPkHelper_JobRestart_Call) Run(run func(jobID int)) *MockPkHelper_JobRestart_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockPkHelper_JobRestart_Call) Return(_a0 error) *MockPkHelper_JobRestart_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_JobRestart_Call) RunAndReturn(run func(int) error) *MockPkHelper_JobRestart_Call {
_c.Call.Return(run)
return _c
}
// JobSetHoldUntil provides a mock function with given fields: jobID, holdUntil
func (_m *MockPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error {
ret := _m.Called(jobID, holdUntil)
if len(ret) == 0 {
panic("no return value specified for JobSetHoldUntil")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(jobID, holdUntil)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_JobSetHoldUntil_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSetHoldUntil'
type MockPkHelper_JobSetHoldUntil_Call struct {
*mock.Call
}
// JobSetHoldUntil is a helper method to define mock.On call
// - jobID int
// - holdUntil string
func (_e *MockPkHelper_Expecter) JobSetHoldUntil(jobID interface{}, holdUntil interface{}) *MockPkHelper_JobSetHoldUntil_Call {
return &MockPkHelper_JobSetHoldUntil_Call{Call: _e.mock.On("JobSetHoldUntil", jobID, holdUntil)}
}
func (_c *MockPkHelper_JobSetHoldUntil_Call) Run(run func(jobID int, holdUntil string)) *MockPkHelper_JobSetHoldUntil_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_JobSetHoldUntil_Call) Return(_a0 error) *MockPkHelper_JobSetHoldUntil_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_JobSetHoldUntil_Call) RunAndReturn(run func(int, string) error) *MockPkHelper_JobSetHoldUntil_Call {
_c.Call.Return(run)
return _c
}
// PrinterAdd provides a mock function with given fields: name, uri, ppd, info, location
func (_m *MockPkHelper) PrinterAdd(name string, uri string, ppd string, info string, location string) error {
ret := _m.Called(name, uri, ppd, info, location)
if len(ret) == 0 {
panic("no return value specified for PrinterAdd")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(name, uri, ppd, info, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterAdd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterAdd'
type MockPkHelper_PrinterAdd_Call struct {
*mock.Call
}
// PrinterAdd is a helper method to define mock.On call
// - name string
// - uri string
// - ppd string
// - info string
// - location string
func (_e *MockPkHelper_Expecter) PrinterAdd(name interface{}, uri interface{}, ppd interface{}, info interface{}, location interface{}) *MockPkHelper_PrinterAdd_Call {
return &MockPkHelper_PrinterAdd_Call{Call: _e.mock.On("PrinterAdd", name, uri, ppd, info, location)}
}
func (_c *MockPkHelper_PrinterAdd_Call) Run(run func(name string, uri string, ppd string, info string, location string)) *MockPkHelper_PrinterAdd_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterAdd_Call) Return(_a0 error) *MockPkHelper_PrinterAdd_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterAdd_Call) RunAndReturn(run func(string, string, string, string, string) error) *MockPkHelper_PrinterAdd_Call {
_c.Call.Return(run)
return _c
}
// PrinterDelete provides a mock function with given fields: name
func (_m *MockPkHelper) PrinterDelete(name string) error {
ret := _m.Called(name)
if len(ret) == 0 {
panic("no return value specified for PrinterDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(name)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterDelete'
type MockPkHelper_PrinterDelete_Call struct {
*mock.Call
}
// PrinterDelete is a helper method to define mock.On call
// - name string
func (_e *MockPkHelper_Expecter) PrinterDelete(name interface{}) *MockPkHelper_PrinterDelete_Call {
return &MockPkHelper_PrinterDelete_Call{Call: _e.mock.On("PrinterDelete", name)}
}
func (_c *MockPkHelper_PrinterDelete_Call) Run(run func(name string)) *MockPkHelper_PrinterDelete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterDelete_Call) Return(_a0 error) *MockPkHelper_PrinterDelete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterDelete_Call) RunAndReturn(run func(string) error) *MockPkHelper_PrinterDelete_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetAcceptJobs provides a mock function with given fields: name, enabled, reason
func (_m *MockPkHelper) PrinterSetAcceptJobs(name string, enabled bool, reason string) error {
ret := _m.Called(name, enabled, reason)
if len(ret) == 0 {
panic("no return value specified for PrinterSetAcceptJobs")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool, string) error); ok {
r0 = rf(name, enabled, reason)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetAcceptJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetAcceptJobs'
type MockPkHelper_PrinterSetAcceptJobs_Call struct {
*mock.Call
}
// PrinterSetAcceptJobs is a helper method to define mock.On call
// - name string
// - enabled bool
// - reason string
func (_e *MockPkHelper_Expecter) PrinterSetAcceptJobs(name interface{}, enabled interface{}, reason interface{}) *MockPkHelper_PrinterSetAcceptJobs_Call {
return &MockPkHelper_PrinterSetAcceptJobs_Call{Call: _e.mock.On("PrinterSetAcceptJobs", name, enabled, reason)}
}
func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) Run(run func(name string, enabled bool, reason string)) *MockPkHelper_PrinterSetAcceptJobs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool), args[2].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) Return(_a0 error) *MockPkHelper_PrinterSetAcceptJobs_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) RunAndReturn(run func(string, bool, string) error) *MockPkHelper_PrinterSetAcceptJobs_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetEnabled provides a mock function with given fields: name, enabled
func (_m *MockPkHelper) PrinterSetEnabled(name string, enabled bool) error {
ret := _m.Called(name, enabled)
if len(ret) == 0 {
panic("no return value specified for PrinterSetEnabled")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(name, enabled)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetEnabled'
type MockPkHelper_PrinterSetEnabled_Call struct {
*mock.Call
}
// PrinterSetEnabled is a helper method to define mock.On call
// - name string
// - enabled bool
func (_e *MockPkHelper_Expecter) PrinterSetEnabled(name interface{}, enabled interface{}) *MockPkHelper_PrinterSetEnabled_Call {
return &MockPkHelper_PrinterSetEnabled_Call{Call: _e.mock.On("PrinterSetEnabled", name, enabled)}
}
func (_c *MockPkHelper_PrinterSetEnabled_Call) Run(run func(name string, enabled bool)) *MockPkHelper_PrinterSetEnabled_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockPkHelper_PrinterSetEnabled_Call) Return(_a0 error) *MockPkHelper_PrinterSetEnabled_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetEnabled_Call) RunAndReturn(run func(string, bool) error) *MockPkHelper_PrinterSetEnabled_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetInfo provides a mock function with given fields: name, info
func (_m *MockPkHelper) PrinterSetInfo(name string, info string) error {
ret := _m.Called(name, info)
if len(ret) == 0 {
panic("no return value specified for PrinterSetInfo")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(name, info)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetInfo'
type MockPkHelper_PrinterSetInfo_Call struct {
*mock.Call
}
// PrinterSetInfo is a helper method to define mock.On call
// - name string
// - info string
func (_e *MockPkHelper_Expecter) PrinterSetInfo(name interface{}, info interface{}) *MockPkHelper_PrinterSetInfo_Call {
return &MockPkHelper_PrinterSetInfo_Call{Call: _e.mock.On("PrinterSetInfo", name, info)}
}
func (_c *MockPkHelper_PrinterSetInfo_Call) Run(run func(name string, info string)) *MockPkHelper_PrinterSetInfo_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterSetInfo_Call) Return(_a0 error) *MockPkHelper_PrinterSetInfo_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetInfo_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_PrinterSetInfo_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetLocation provides a mock function with given fields: name, location
func (_m *MockPkHelper) PrinterSetLocation(name string, location string) error {
ret := _m.Called(name, location)
if len(ret) == 0 {
panic("no return value specified for PrinterSetLocation")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(name, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetLocation'
type MockPkHelper_PrinterSetLocation_Call struct {
*mock.Call
}
// PrinterSetLocation is a helper method to define mock.On call
// - name string
// - location string
func (_e *MockPkHelper_Expecter) PrinterSetLocation(name interface{}, location interface{}) *MockPkHelper_PrinterSetLocation_Call {
return &MockPkHelper_PrinterSetLocation_Call{Call: _e.mock.On("PrinterSetLocation", name, location)}
}
func (_c *MockPkHelper_PrinterSetLocation_Call) Run(run func(name string, location string)) *MockPkHelper_PrinterSetLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterSetLocation_Call) Return(_a0 error) *MockPkHelper_PrinterSetLocation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetLocation_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_PrinterSetLocation_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetShared provides a mock function with given fields: name, shared
func (_m *MockPkHelper) PrinterSetShared(name string, shared bool) error {
ret := _m.Called(name, shared)
if len(ret) == 0 {
panic("no return value specified for PrinterSetShared")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(name, shared)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetShared_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetShared'
type MockPkHelper_PrinterSetShared_Call struct {
*mock.Call
}
// PrinterSetShared is a helper method to define mock.On call
// - name string
// - shared bool
func (_e *MockPkHelper_Expecter) PrinterSetShared(name interface{}, shared interface{}) *MockPkHelper_PrinterSetShared_Call {
return &MockPkHelper_PrinterSetShared_Call{Call: _e.mock.On("PrinterSetShared", name, shared)}
}
func (_c *MockPkHelper_PrinterSetShared_Call) Run(run func(name string, shared bool)) *MockPkHelper_PrinterSetShared_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockPkHelper_PrinterSetShared_Call) Return(_a0 error) *MockPkHelper_PrinterSetShared_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetShared_Call) RunAndReturn(run func(string, bool) error) *MockPkHelper_PrinterSetShared_Call {
_c.Call.Return(run)
return _c
}
// NewMockPkHelper creates a new instance of MockPkHelper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockPkHelper(t interface {
mock.TestingT
Cleanup(func())
}) *MockPkHelper {
mock := &MockPkHelper{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -0,0 +1,295 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_evdev
import (
go_evdev "github.com/holoplot/go-evdev"
mock "github.com/stretchr/testify/mock"
)
// MockEvdevDevice is an autogenerated mock type for the EvdevDevice type
type MockEvdevDevice struct {
mock.Mock
}
type MockEvdevDevice_Expecter struct {
mock *mock.Mock
}
func (_m *MockEvdevDevice) EXPECT() *MockEvdevDevice_Expecter {
return &MockEvdevDevice_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockEvdevDevice) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockEvdevDevice_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockEvdevDevice_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) Close() *MockEvdevDevice_Close_Call {
return &MockEvdevDevice_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockEvdevDevice_Close_Call) Run(run func()) *MockEvdevDevice_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_Close_Call) Return(_a0 error) *MockEvdevDevice_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockEvdevDevice_Close_Call) RunAndReturn(run func() error) *MockEvdevDevice_Close_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with no fields
func (_m *MockEvdevDevice) Name() (string, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func() (string, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockEvdevDevice_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockEvdevDevice_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) Name() *MockEvdevDevice_Name_Call {
return &MockEvdevDevice_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockEvdevDevice_Name_Call) Run(run func()) *MockEvdevDevice_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_Name_Call) Return(_a0 string, _a1 error) *MockEvdevDevice_Name_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockEvdevDevice_Name_Call) RunAndReturn(run func() (string, error)) *MockEvdevDevice_Name_Call {
_c.Call.Return(run)
return _c
}
// Path provides a mock function with no fields
func (_m *MockEvdevDevice) Path() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Path")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockEvdevDevice_Path_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Path'
type MockEvdevDevice_Path_Call struct {
*mock.Call
}
// Path is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) Path() *MockEvdevDevice_Path_Call {
return &MockEvdevDevice_Path_Call{Call: _e.mock.On("Path")}
}
func (_c *MockEvdevDevice_Path_Call) Run(run func()) *MockEvdevDevice_Path_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_Path_Call) Return(_a0 string) *MockEvdevDevice_Path_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockEvdevDevice_Path_Call) RunAndReturn(run func() string) *MockEvdevDevice_Path_Call {
_c.Call.Return(run)
return _c
}
// ReadOne provides a mock function with no fields
func (_m *MockEvdevDevice) ReadOne() (*go_evdev.InputEvent, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ReadOne")
}
var r0 *go_evdev.InputEvent
var r1 error
if rf, ok := ret.Get(0).(func() (*go_evdev.InputEvent, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *go_evdev.InputEvent); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*go_evdev.InputEvent)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockEvdevDevice_ReadOne_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadOne'
type MockEvdevDevice_ReadOne_Call struct {
*mock.Call
}
// ReadOne is a helper method to define mock.On call
func (_e *MockEvdevDevice_Expecter) ReadOne() *MockEvdevDevice_ReadOne_Call {
return &MockEvdevDevice_ReadOne_Call{Call: _e.mock.On("ReadOne")}
}
func (_c *MockEvdevDevice_ReadOne_Call) Run(run func()) *MockEvdevDevice_ReadOne_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockEvdevDevice_ReadOne_Call) Return(_a0 *go_evdev.InputEvent, _a1 error) *MockEvdevDevice_ReadOne_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockEvdevDevice_ReadOne_Call) RunAndReturn(run func() (*go_evdev.InputEvent, error)) *MockEvdevDevice_ReadOne_Call {
_c.Call.Return(run)
return _c
}
// State provides a mock function with given fields: t
func (_m *MockEvdevDevice) State(t go_evdev.EvType) (go_evdev.StateMap, error) {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for State")
}
var r0 go_evdev.StateMap
var r1 error
if rf, ok := ret.Get(0).(func(go_evdev.EvType) (go_evdev.StateMap, error)); ok {
return rf(t)
}
if rf, ok := ret.Get(0).(func(go_evdev.EvType) go_evdev.StateMap); ok {
r0 = rf(t)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(go_evdev.StateMap)
}
}
if rf, ok := ret.Get(1).(func(go_evdev.EvType) error); ok {
r1 = rf(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockEvdevDevice_State_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'State'
type MockEvdevDevice_State_Call struct {
*mock.Call
}
// State is a helper method to define mock.On call
// - t go_evdev.EvType
func (_e *MockEvdevDevice_Expecter) State(t interface{}) *MockEvdevDevice_State_Call {
return &MockEvdevDevice_State_Call{Call: _e.mock.On("State", t)}
}
func (_c *MockEvdevDevice_State_Call) Run(run func(t go_evdev.EvType)) *MockEvdevDevice_State_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(go_evdev.EvType))
})
return _c
}
func (_c *MockEvdevDevice_State_Call) Return(_a0 go_evdev.StateMap, _a1 error) *MockEvdevDevice_State_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockEvdevDevice_State_Call) RunAndReturn(run func(go_evdev.EvType) (go_evdev.StateMap, error)) *MockEvdevDevice_State_Call {
_c.Call.Return(run)
return _c
}
// NewMockEvdevDevice creates a new instance of MockEvdevDevice. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockEvdevDevice(t interface {
mock.TestingT
Cleanup(func())
}) *MockEvdevDevice {
mock := &MockEvdevDevice{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -328,6 +328,52 @@ func (_c *MockBackend_ConnectWiFi_Call) RunAndReturn(run func(network.Connection
return _c return _c
} }
// DeleteVPN provides a mock function with given fields: uuidOrName
func (_m *MockBackend) DeleteVPN(uuidOrName string) error {
ret := _m.Called(uuidOrName)
if len(ret) == 0 {
panic("no return value specified for DeleteVPN")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(uuidOrName)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_DeleteVPN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteVPN'
type MockBackend_DeleteVPN_Call struct {
*mock.Call
}
// DeleteVPN is a helper method to define mock.On call
// - uuidOrName string
func (_e *MockBackend_Expecter) DeleteVPN(uuidOrName interface{}) *MockBackend_DeleteVPN_Call {
return &MockBackend_DeleteVPN_Call{Call: _e.mock.On("DeleteVPN", uuidOrName)}
}
func (_c *MockBackend_DeleteVPN_Call) Run(run func(uuidOrName string)) *MockBackend_DeleteVPN_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_DeleteVPN_Call) Return(_a0 error) *MockBackend_DeleteVPN_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_DeleteVPN_Call) RunAndReturn(run func(string) error) *MockBackend_DeleteVPN_Call {
_c.Call.Return(run)
return _c
}
// DisconnectAllVPN provides a mock function with no fields // DisconnectAllVPN provides a mock function with no fields
func (_m *MockBackend) DisconnectAllVPN() error { func (_m *MockBackend) DisconnectAllVPN() error {
ret := _m.Called() ret := _m.Called()
@@ -418,6 +464,52 @@ func (_c *MockBackend_DisconnectEthernet_Call) RunAndReturn(run func() error) *M
return _c return _c
} }
// DisconnectEthernetDevice provides a mock function with given fields: device
func (_m *MockBackend) DisconnectEthernetDevice(device string) error {
ret := _m.Called(device)
if len(ret) == 0 {
panic("no return value specified for DisconnectEthernetDevice")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(device)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_DisconnectEthernetDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectEthernetDevice'
type MockBackend_DisconnectEthernetDevice_Call struct {
*mock.Call
}
// DisconnectEthernetDevice is a helper method to define mock.On call
// - device string
func (_e *MockBackend_Expecter) DisconnectEthernetDevice(device interface{}) *MockBackend_DisconnectEthernetDevice_Call {
return &MockBackend_DisconnectEthernetDevice_Call{Call: _e.mock.On("DisconnectEthernetDevice", device)}
}
func (_c *MockBackend_DisconnectEthernetDevice_Call) Run(run func(device string)) *MockBackend_DisconnectEthernetDevice_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_DisconnectEthernetDevice_Call) Return(_a0 error) *MockBackend_DisconnectEthernetDevice_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_DisconnectEthernetDevice_Call) RunAndReturn(run func(string) error) *MockBackend_DisconnectEthernetDevice_Call {
_c.Call.Return(run)
return _c
}
// DisconnectVPN provides a mock function with given fields: uuidOrName // DisconnectVPN provides a mock function with given fields: uuidOrName
func (_m *MockBackend) DisconnectVPN(uuidOrName string) error { func (_m *MockBackend) DisconnectVPN(uuidOrName string) error {
ret := _m.Called(uuidOrName) ret := _m.Called(uuidOrName)
@@ -509,6 +601,52 @@ func (_c *MockBackend_DisconnectWiFi_Call) RunAndReturn(run func() error) *MockB
return _c return _c
} }
// DisconnectWiFiDevice provides a mock function with given fields: device
func (_m *MockBackend) DisconnectWiFiDevice(device string) error {
ret := _m.Called(device)
if len(ret) == 0 {
panic("no return value specified for DisconnectWiFiDevice")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(device)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_DisconnectWiFiDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectWiFiDevice'
type MockBackend_DisconnectWiFiDevice_Call struct {
*mock.Call
}
// DisconnectWiFiDevice is a helper method to define mock.On call
// - device string
func (_e *MockBackend_Expecter) DisconnectWiFiDevice(device interface{}) *MockBackend_DisconnectWiFiDevice_Call {
return &MockBackend_DisconnectWiFiDevice_Call{Call: _e.mock.On("DisconnectWiFiDevice", device)}
}
func (_c *MockBackend_DisconnectWiFiDevice_Call) Run(run func(device string)) *MockBackend_DisconnectWiFiDevice_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_DisconnectWiFiDevice_Call) Return(_a0 error) *MockBackend_DisconnectWiFiDevice_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_DisconnectWiFiDevice_Call) RunAndReturn(run func(string) error) *MockBackend_DisconnectWiFiDevice_Call {
_c.Call.Return(run)
return _c
}
// ForgetWiFiNetwork provides a mock function with given fields: ssid // ForgetWiFiNetwork provides a mock function with given fields: ssid
func (_m *MockBackend) ForgetWiFiNetwork(ssid string) error { func (_m *MockBackend) ForgetWiFiNetwork(ssid string) error {
ret := _m.Called(ssid) ret := _m.Called(ssid)
@@ -612,6 +750,53 @@ func (_c *MockBackend_GetCurrentState_Call) RunAndReturn(run func() (*network.Ba
return _c return _c
} }
// GetEthernetDevices provides a mock function with no fields
func (_m *MockBackend) GetEthernetDevices() []network.EthernetDevice {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetEthernetDevices")
}
var r0 []network.EthernetDevice
if rf, ok := ret.Get(0).(func() []network.EthernetDevice); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]network.EthernetDevice)
}
}
return r0
}
// MockBackend_GetEthernetDevices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEthernetDevices'
type MockBackend_GetEthernetDevices_Call struct {
*mock.Call
}
// GetEthernetDevices is a helper method to define mock.On call
func (_e *MockBackend_Expecter) GetEthernetDevices() *MockBackend_GetEthernetDevices_Call {
return &MockBackend_GetEthernetDevices_Call{Call: _e.mock.On("GetEthernetDevices")}
}
func (_c *MockBackend_GetEthernetDevices_Call) Run(run func()) *MockBackend_GetEthernetDevices_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockBackend_GetEthernetDevices_Call) Return(_a0 []network.EthernetDevice) *MockBackend_GetEthernetDevices_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_GetEthernetDevices_Call) RunAndReturn(run func() []network.EthernetDevice) *MockBackend_GetEthernetDevices_Call {
_c.Call.Return(run)
return _c
}
// GetPromptBroker provides a mock function with no fields // GetPromptBroker provides a mock function with no fields
func (_m *MockBackend) GetPromptBroker() network.PromptBroker { func (_m *MockBackend) GetPromptBroker() network.PromptBroker {
ret := _m.Called() ret := _m.Called()
@@ -659,6 +844,111 @@ func (_c *MockBackend_GetPromptBroker_Call) RunAndReturn(run func() network.Prom
return _c return _c
} }
// GetVPNConfig provides a mock function with given fields: uuidOrName
func (_m *MockBackend) GetVPNConfig(uuidOrName string) (*network.VPNConfig, error) {
ret := _m.Called(uuidOrName)
if len(ret) == 0 {
panic("no return value specified for GetVPNConfig")
}
var r0 *network.VPNConfig
var r1 error
if rf, ok := ret.Get(0).(func(string) (*network.VPNConfig, error)); ok {
return rf(uuidOrName)
}
if rf, ok := ret.Get(0).(func(string) *network.VPNConfig); ok {
r0 = rf(uuidOrName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*network.VPNConfig)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(uuidOrName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockBackend_GetVPNConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetVPNConfig'
type MockBackend_GetVPNConfig_Call struct {
*mock.Call
}
// GetVPNConfig is a helper method to define mock.On call
// - uuidOrName string
func (_e *MockBackend_Expecter) GetVPNConfig(uuidOrName interface{}) *MockBackend_GetVPNConfig_Call {
return &MockBackend_GetVPNConfig_Call{Call: _e.mock.On("GetVPNConfig", uuidOrName)}
}
func (_c *MockBackend_GetVPNConfig_Call) Run(run func(uuidOrName string)) *MockBackend_GetVPNConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_GetVPNConfig_Call) Return(_a0 *network.VPNConfig, _a1 error) *MockBackend_GetVPNConfig_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockBackend_GetVPNConfig_Call) RunAndReturn(run func(string) (*network.VPNConfig, error)) *MockBackend_GetVPNConfig_Call {
_c.Call.Return(run)
return _c
}
// GetWiFiDevices provides a mock function with no fields
func (_m *MockBackend) GetWiFiDevices() []network.WiFiDevice {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetWiFiDevices")
}
var r0 []network.WiFiDevice
if rf, ok := ret.Get(0).(func() []network.WiFiDevice); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]network.WiFiDevice)
}
}
return r0
}
// MockBackend_GetWiFiDevices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiDevices'
type MockBackend_GetWiFiDevices_Call struct {
*mock.Call
}
// GetWiFiDevices is a helper method to define mock.On call
func (_e *MockBackend_Expecter) GetWiFiDevices() *MockBackend_GetWiFiDevices_Call {
return &MockBackend_GetWiFiDevices_Call{Call: _e.mock.On("GetWiFiDevices")}
}
func (_c *MockBackend_GetWiFiDevices_Call) Run(run func()) *MockBackend_GetWiFiDevices_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockBackend_GetWiFiDevices_Call) Return(_a0 []network.WiFiDevice) *MockBackend_GetWiFiDevices_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_GetWiFiDevices_Call) RunAndReturn(run func() []network.WiFiDevice) *MockBackend_GetWiFiDevices_Call {
_c.Call.Return(run)
return _c
}
// GetWiFiEnabled provides a mock function with no fields // GetWiFiEnabled provides a mock function with no fields
func (_m *MockBackend) GetWiFiEnabled() (bool, error) { func (_m *MockBackend) GetWiFiEnabled() (bool, error) {
ret := _m.Called() ret := _m.Called()
@@ -887,6 +1177,65 @@ func (_c *MockBackend_GetWiredNetworkDetails_Call) RunAndReturn(run func(string)
return _c return _c
} }
// ImportVPN provides a mock function with given fields: filePath, name
func (_m *MockBackend) ImportVPN(filePath string, name string) (*network.VPNImportResult, error) {
ret := _m.Called(filePath, name)
if len(ret) == 0 {
panic("no return value specified for ImportVPN")
}
var r0 *network.VPNImportResult
var r1 error
if rf, ok := ret.Get(0).(func(string, string) (*network.VPNImportResult, error)); ok {
return rf(filePath, name)
}
if rf, ok := ret.Get(0).(func(string, string) *network.VPNImportResult); ok {
r0 = rf(filePath, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*network.VPNImportResult)
}
}
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(filePath, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockBackend_ImportVPN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportVPN'
type MockBackend_ImportVPN_Call struct {
*mock.Call
}
// ImportVPN is a helper method to define mock.On call
// - filePath string
// - name string
func (_e *MockBackend_Expecter) ImportVPN(filePath interface{}, name interface{}) *MockBackend_ImportVPN_Call {
return &MockBackend_ImportVPN_Call{Call: _e.mock.On("ImportVPN", filePath, name)}
}
func (_c *MockBackend_ImportVPN_Call) Run(run func(filePath string, name string)) *MockBackend_ImportVPN_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockBackend_ImportVPN_Call) Return(_a0 *network.VPNImportResult, _a1 error) *MockBackend_ImportVPN_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockBackend_ImportVPN_Call) RunAndReturn(run func(string, string) (*network.VPNImportResult, error)) *MockBackend_ImportVPN_Call {
_c.Call.Return(run)
return _c
}
// Initialize provides a mock function with no fields // Initialize provides a mock function with no fields
func (_m *MockBackend) Initialize() error { func (_m *MockBackend) Initialize() error {
ret := _m.Called() ret := _m.Called()
@@ -989,6 +1338,63 @@ func (_c *MockBackend_ListActiveVPN_Call) RunAndReturn(run func() ([]network.VPN
return _c return _c
} }
// ListVPNPlugins provides a mock function with no fields
func (_m *MockBackend) ListVPNPlugins() ([]network.VPNPlugin, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ListVPNPlugins")
}
var r0 []network.VPNPlugin
var r1 error
if rf, ok := ret.Get(0).(func() ([]network.VPNPlugin, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() []network.VPNPlugin); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]network.VPNPlugin)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockBackend_ListVPNPlugins_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListVPNPlugins'
type MockBackend_ListVPNPlugins_Call struct {
*mock.Call
}
// ListVPNPlugins is a helper method to define mock.On call
func (_e *MockBackend_Expecter) ListVPNPlugins() *MockBackend_ListVPNPlugins_Call {
return &MockBackend_ListVPNPlugins_Call{Call: _e.mock.On("ListVPNPlugins")}
}
func (_c *MockBackend_ListVPNPlugins_Call) Run(run func()) *MockBackend_ListVPNPlugins_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockBackend_ListVPNPlugins_Call) Return(_a0 []network.VPNPlugin, _a1 error) *MockBackend_ListVPNPlugins_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockBackend_ListVPNPlugins_Call) RunAndReturn(run func() ([]network.VPNPlugin, error)) *MockBackend_ListVPNPlugins_Call {
_c.Call.Return(run)
return _c
}
// ListVPNProfiles provides a mock function with no fields // ListVPNProfiles provides a mock function with no fields
func (_m *MockBackend) ListVPNProfiles() ([]network.VPNProfile, error) { func (_m *MockBackend) ListVPNProfiles() ([]network.VPNProfile, error) {
ret := _m.Called() ret := _m.Called()
@@ -1091,6 +1497,52 @@ func (_c *MockBackend_ScanWiFi_Call) RunAndReturn(run func() error) *MockBackend
return _c return _c
} }
// ScanWiFiDevice provides a mock function with given fields: device
func (_m *MockBackend) ScanWiFiDevice(device string) error {
ret := _m.Called(device)
if len(ret) == 0 {
panic("no return value specified for ScanWiFiDevice")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(device)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_ScanWiFiDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScanWiFiDevice'
type MockBackend_ScanWiFiDevice_Call struct {
*mock.Call
}
// ScanWiFiDevice is a helper method to define mock.On call
// - device string
func (_e *MockBackend_Expecter) ScanWiFiDevice(device interface{}) *MockBackend_ScanWiFiDevice_Call {
return &MockBackend_ScanWiFiDevice_Call{Call: _e.mock.On("ScanWiFiDevice", device)}
}
func (_c *MockBackend_ScanWiFiDevice_Call) Run(run func(device string)) *MockBackend_ScanWiFiDevice_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_ScanWiFiDevice_Call) Return(_a0 error) *MockBackend_ScanWiFiDevice_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_ScanWiFiDevice_Call) RunAndReturn(run func(string) error) *MockBackend_ScanWiFiDevice_Call {
_c.Call.Return(run)
return _c
}
// SetPromptBroker provides a mock function with given fields: broker // SetPromptBroker provides a mock function with given fields: broker
func (_m *MockBackend) SetPromptBroker(broker network.PromptBroker) error { func (_m *MockBackend) SetPromptBroker(broker network.PromptBroker) error {
ret := _m.Called(broker) ret := _m.Called(broker)
@@ -1137,6 +1589,55 @@ func (_c *MockBackend_SetPromptBroker_Call) RunAndReturn(run func(network.Prompt
return _c return _c
} }
// SetVPNCredentials provides a mock function with given fields: uuid, username, password, save
func (_m *MockBackend) SetVPNCredentials(uuid string, username string, password string, save bool) error {
ret := _m.Called(uuid, username, password, save)
if len(ret) == 0 {
panic("no return value specified for SetVPNCredentials")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, bool) error); ok {
r0 = rf(uuid, username, password, save)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_SetVPNCredentials_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetVPNCredentials'
type MockBackend_SetVPNCredentials_Call struct {
*mock.Call
}
// SetVPNCredentials is a helper method to define mock.On call
// - uuid string
// - username string
// - password string
// - save bool
func (_e *MockBackend_Expecter) SetVPNCredentials(uuid interface{}, username interface{}, password interface{}, save interface{}) *MockBackend_SetVPNCredentials_Call {
return &MockBackend_SetVPNCredentials_Call{Call: _e.mock.On("SetVPNCredentials", uuid, username, password, save)}
}
func (_c *MockBackend_SetVPNCredentials_Call) Run(run func(uuid string, username string, password string, save bool)) *MockBackend_SetVPNCredentials_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool))
})
return _c
}
func (_c *MockBackend_SetVPNCredentials_Call) Return(_a0 error) *MockBackend_SetVPNCredentials_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_SetVPNCredentials_Call) RunAndReturn(run func(string, string, string, bool) error) *MockBackend_SetVPNCredentials_Call {
_c.Call.Return(run)
return _c
}
// SetWiFiAutoconnect provides a mock function with given fields: ssid, autoconnect // SetWiFiAutoconnect provides a mock function with given fields: ssid, autoconnect
func (_m *MockBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { func (_m *MockBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
ret := _m.Called(ssid, autoconnect) ret := _m.Called(ssid, autoconnect)
@@ -1356,6 +1857,53 @@ func (_c *MockBackend_SubmitCredentials_Call) RunAndReturn(run func(string, map[
return _c return _c
} }
// UpdateVPNConfig provides a mock function with given fields: uuid, updates
func (_m *MockBackend) UpdateVPNConfig(uuid string, updates map[string]interface{}) error {
ret := _m.Called(uuid, updates)
if len(ret) == 0 {
panic("no return value specified for UpdateVPNConfig")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, map[string]interface{}) error); ok {
r0 = rf(uuid, updates)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_UpdateVPNConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateVPNConfig'
type MockBackend_UpdateVPNConfig_Call struct {
*mock.Call
}
// UpdateVPNConfig is a helper method to define mock.On call
// - uuid string
// - updates map[string]interface{}
func (_e *MockBackend_Expecter) UpdateVPNConfig(uuid interface{}, updates interface{}) *MockBackend_UpdateVPNConfig_Call {
return &MockBackend_UpdateVPNConfig_Call{Call: _e.mock.On("UpdateVPNConfig", uuid, updates)}
}
func (_c *MockBackend_UpdateVPNConfig_Call) Run(run func(uuid string, updates map[string]interface{})) *MockBackend_UpdateVPNConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(map[string]interface{}))
})
return _c
}
func (_c *MockBackend_UpdateVPNConfig_Call) Return(_a0 error) *MockBackend_UpdateVPNConfig_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_UpdateVPNConfig_Call) RunAndReturn(run func(string, map[string]interface{}) error) *MockBackend_UpdateVPNConfig_Call {
_c.Call.Return(run)
return _c
}
// NewMockBackend creates a new instance of MockBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // NewMockBackend creates a new instance of MockBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value. // The first argument is typically a *testing.T value.
func NewMockBackend(t interface { func NewMockBackend(t interface {

View File

@@ -0,0 +1,144 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_version
import mock "github.com/stretchr/testify/mock"
// MockVersionFetcher is an autogenerated mock type for the VersionFetcher type
type MockVersionFetcher struct {
mock.Mock
}
type MockVersionFetcher_Expecter struct {
mock *mock.Mock
}
func (_m *MockVersionFetcher) EXPECT() *MockVersionFetcher_Expecter {
return &MockVersionFetcher_Expecter{mock: &_m.Mock}
}
// GetCurrentVersion provides a mock function with given fields: dmsPath
func (_m *MockVersionFetcher) GetCurrentVersion(dmsPath string) (string, error) {
ret := _m.Called(dmsPath)
if len(ret) == 0 {
panic("no return value specified for GetCurrentVersion")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
return rf(dmsPath)
}
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(dmsPath)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(dmsPath)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockVersionFetcher_GetCurrentVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCurrentVersion'
type MockVersionFetcher_GetCurrentVersion_Call struct {
*mock.Call
}
// GetCurrentVersion is a helper method to define mock.On call
// - dmsPath string
func (_e *MockVersionFetcher_Expecter) GetCurrentVersion(dmsPath interface{}) *MockVersionFetcher_GetCurrentVersion_Call {
return &MockVersionFetcher_GetCurrentVersion_Call{Call: _e.mock.On("GetCurrentVersion", dmsPath)}
}
func (_c *MockVersionFetcher_GetCurrentVersion_Call) Run(run func(dmsPath string)) *MockVersionFetcher_GetCurrentVersion_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockVersionFetcher_GetCurrentVersion_Call) Return(_a0 string, _a1 error) *MockVersionFetcher_GetCurrentVersion_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockVersionFetcher_GetCurrentVersion_Call) RunAndReturn(run func(string) (string, error)) *MockVersionFetcher_GetCurrentVersion_Call {
_c.Call.Return(run)
return _c
}
// GetLatestVersion provides a mock function with given fields: dmsPath
func (_m *MockVersionFetcher) GetLatestVersion(dmsPath string) (string, error) {
ret := _m.Called(dmsPath)
if len(ret) == 0 {
panic("no return value specified for GetLatestVersion")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
return rf(dmsPath)
}
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(dmsPath)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(dmsPath)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockVersionFetcher_GetLatestVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestVersion'
type MockVersionFetcher_GetLatestVersion_Call struct {
*mock.Call
}
// GetLatestVersion is a helper method to define mock.On call
// - dmsPath string
func (_e *MockVersionFetcher_Expecter) GetLatestVersion(dmsPath interface{}) *MockVersionFetcher_GetLatestVersion_Call {
return &MockVersionFetcher_GetLatestVersion_Call{Call: _e.mock.On("GetLatestVersion", dmsPath)}
}
func (_c *MockVersionFetcher_GetLatestVersion_Call) Run(run func(dmsPath string)) *MockVersionFetcher_GetLatestVersion_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockVersionFetcher_GetLatestVersion_Call) Return(_a0 string, _a1 error) *MockVersionFetcher_GetLatestVersion_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockVersionFetcher_GetLatestVersion_Call) RunAndReturn(run func(string) (string, error)) *MockVersionFetcher_GetLatestVersion_Call {
_c.Call.Return(run)
return _c
}
// NewMockVersionFetcher creates a new instance of MockVersionFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockVersionFetcher(t interface {
mock.TestingT
Cleanup(func())
}) *MockVersionFetcher {
mock := &MockVersionFetcher{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -93,7 +93,7 @@ func (m *Manager) Install(plugin Plugin) error {
if !repoExists { if !repoExists {
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil { if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
m.fs.RemoveAll(repoPath) m.fs.RemoveAll(repoPath) //nolint:errcheck
return fmt.Errorf("failed to clone repository: %w", err) return fmt.Errorf("failed to clone repository: %w", err)
} }
} else { } else {
@@ -130,7 +130,7 @@ func (m *Manager) Install(plugin Plugin) error {
} }
} else { } else {
if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil { if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil {
m.fs.RemoveAll(pluginPath) m.fs.RemoveAll(pluginPath) //nolint:errcheck
return fmt.Errorf("failed to clone plugin: %w", err) return fmt.Errorf("failed to clone plugin: %w", err)
} }
} }

View File

@@ -1,12 +1,12 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml // XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
// //
// dwl_ipc_unstable_v2 Protocol Copyright: // dwl_ipc_unstable_v2 Protocol Copyright:
package dwl_ipc package dwl_ipc
import "github.com/yaslama/go-wayland/wayland/client" import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the // It can be used to match the [client.RegistryGlobalEvent.Interface] in the
@@ -157,6 +157,16 @@ type ZdwlIpcOutputV2 struct {
appidHandler ZdwlIpcOutputV2AppidHandlerFunc appidHandler ZdwlIpcOutputV2AppidHandlerFunc
layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc
frameHandler ZdwlIpcOutputV2FrameHandlerFunc frameHandler ZdwlIpcOutputV2FrameHandlerFunc
fullscreenHandler ZdwlIpcOutputV2FullscreenHandlerFunc
floatingHandler ZdwlIpcOutputV2FloatingHandlerFunc
xHandler ZdwlIpcOutputV2XHandlerFunc
yHandler ZdwlIpcOutputV2YHandlerFunc
widthHandler ZdwlIpcOutputV2WidthHandlerFunc
heightHandler ZdwlIpcOutputV2HeightHandlerFunc
lastLayerHandler ZdwlIpcOutputV2LastLayerHandlerFunc
kbLayoutHandler ZdwlIpcOutputV2KbLayoutHandlerFunc
keymodeHandler ZdwlIpcOutputV2KeymodeHandlerFunc
scalefactorHandler ZdwlIpcOutputV2ScalefactorHandlerFunc
} }
// NewZdwlIpcOutputV2 : control dwl output // NewZdwlIpcOutputV2 : control dwl output
@@ -251,6 +261,60 @@ func (i *ZdwlIpcOutputV2) SetLayout(index uint32) error {
return err return err
} }
// Quit : Quit mango
// This request allows clients to instruct the compositor to quit mango.
func (i *ZdwlIpcOutputV2) Quit() error {
const opcode = 4
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SendDispatch : Set the active tags of this output
//
// dispatch: dispatch name.
// arg1: arg1.
// arg2: arg2.
// arg3: arg3.
// arg4: arg4.
// arg5: arg5.
func (i *ZdwlIpcOutputV2) SendDispatch(dispatch, arg1, arg2, arg3, arg4, arg5 string) error {
const opcode = 5
dispatchLen := client.PaddedLen(len(dispatch) + 1)
arg1Len := client.PaddedLen(len(arg1) + 1)
arg2Len := client.PaddedLen(len(arg2) + 1)
arg3Len := client.PaddedLen(len(arg3) + 1)
arg4Len := client.PaddedLen(len(arg4) + 1)
arg5Len := client.PaddedLen(len(arg5) + 1)
_reqBufLen := 8 + (4 + dispatchLen) + (4 + arg1Len) + (4 + arg2Len) + (4 + arg3Len) + (4 + arg4Len) + (4 + arg5Len)
_reqBuf := make([]byte, _reqBufLen)
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutString(_reqBuf[l:l+(4+dispatchLen)], dispatch)
l += (4 + dispatchLen)
client.PutString(_reqBuf[l:l+(4+arg1Len)], arg1)
l += (4 + arg1Len)
client.PutString(_reqBuf[l:l+(4+arg2Len)], arg2)
l += (4 + arg2Len)
client.PutString(_reqBuf[l:l+(4+arg3Len)], arg3)
l += (4 + arg3Len)
client.PutString(_reqBuf[l:l+(4+arg4Len)], arg4)
l += (4 + arg4Len)
client.PutString(_reqBuf[l:l+(4+arg5Len)], arg5)
l += (4 + arg5Len)
err := i.Context().WriteMsg(_reqBuf, nil)
return err
}
type ZdwlIpcOutputV2TagState uint32 type ZdwlIpcOutputV2TagState uint32
// ZdwlIpcOutputV2TagState : // ZdwlIpcOutputV2TagState :
@@ -399,6 +463,136 @@ func (i *ZdwlIpcOutputV2) SetFrameHandler(f ZdwlIpcOutputV2FrameHandlerFunc) {
i.frameHandler = f i.frameHandler = f
} }
// ZdwlIpcOutputV2FullscreenEvent : Update fullscreen status
//
// Indicates if the selected client on this output is fullscreen.
type ZdwlIpcOutputV2FullscreenEvent struct {
IsFullscreen uint32
}
type ZdwlIpcOutputV2FullscreenHandlerFunc func(ZdwlIpcOutputV2FullscreenEvent)
// SetFullscreenHandler : sets handler for ZdwlIpcOutputV2FullscreenEvent
func (i *ZdwlIpcOutputV2) SetFullscreenHandler(f ZdwlIpcOutputV2FullscreenHandlerFunc) {
i.fullscreenHandler = f
}
// ZdwlIpcOutputV2FloatingEvent : Update the floating status
//
// Indicates if the selected client on this output is floating.
type ZdwlIpcOutputV2FloatingEvent struct {
IsFloating uint32
}
type ZdwlIpcOutputV2FloatingHandlerFunc func(ZdwlIpcOutputV2FloatingEvent)
// SetFloatingHandler : sets handler for ZdwlIpcOutputV2FloatingEvent
func (i *ZdwlIpcOutputV2) SetFloatingHandler(f ZdwlIpcOutputV2FloatingHandlerFunc) {
i.floatingHandler = f
}
// ZdwlIpcOutputV2XEvent : Update the x coordinates
//
// Indicates if x coordinates of the selected client.
type ZdwlIpcOutputV2XEvent struct {
X int32
}
type ZdwlIpcOutputV2XHandlerFunc func(ZdwlIpcOutputV2XEvent)
// SetXHandler : sets handler for ZdwlIpcOutputV2XEvent
func (i *ZdwlIpcOutputV2) SetXHandler(f ZdwlIpcOutputV2XHandlerFunc) {
i.xHandler = f
}
// ZdwlIpcOutputV2YEvent : Update the y coordinates
//
// Indicates if y coordinates of the selected client.
type ZdwlIpcOutputV2YEvent struct {
Y int32
}
type ZdwlIpcOutputV2YHandlerFunc func(ZdwlIpcOutputV2YEvent)
// SetYHandler : sets handler for ZdwlIpcOutputV2YEvent
func (i *ZdwlIpcOutputV2) SetYHandler(f ZdwlIpcOutputV2YHandlerFunc) {
i.yHandler = f
}
// ZdwlIpcOutputV2WidthEvent : Update the width
//
// Indicates if width of the selected client.
type ZdwlIpcOutputV2WidthEvent struct {
Width int32
}
type ZdwlIpcOutputV2WidthHandlerFunc func(ZdwlIpcOutputV2WidthEvent)
// SetWidthHandler : sets handler for ZdwlIpcOutputV2WidthEvent
func (i *ZdwlIpcOutputV2) SetWidthHandler(f ZdwlIpcOutputV2WidthHandlerFunc) {
i.widthHandler = f
}
// ZdwlIpcOutputV2HeightEvent : Update the height
//
// Indicates if height of the selected client.
type ZdwlIpcOutputV2HeightEvent struct {
Height int32
}
type ZdwlIpcOutputV2HeightHandlerFunc func(ZdwlIpcOutputV2HeightEvent)
// SetHeightHandler : sets handler for ZdwlIpcOutputV2HeightEvent
func (i *ZdwlIpcOutputV2) SetHeightHandler(f ZdwlIpcOutputV2HeightHandlerFunc) {
i.heightHandler = f
}
// ZdwlIpcOutputV2LastLayerEvent : last map layer.
//
// last map layer.
type ZdwlIpcOutputV2LastLayerEvent struct {
LastLayer string
}
type ZdwlIpcOutputV2LastLayerHandlerFunc func(ZdwlIpcOutputV2LastLayerEvent)
// SetLastLayerHandler : sets handler for ZdwlIpcOutputV2LastLayerEvent
func (i *ZdwlIpcOutputV2) SetLastLayerHandler(f ZdwlIpcOutputV2LastLayerHandlerFunc) {
i.lastLayerHandler = f
}
// ZdwlIpcOutputV2KbLayoutEvent : current keyboard layout.
//
// current keyboard layout.
type ZdwlIpcOutputV2KbLayoutEvent struct {
KbLayout string
}
type ZdwlIpcOutputV2KbLayoutHandlerFunc func(ZdwlIpcOutputV2KbLayoutEvent)
// SetKbLayoutHandler : sets handler for ZdwlIpcOutputV2KbLayoutEvent
func (i *ZdwlIpcOutputV2) SetKbLayoutHandler(f ZdwlIpcOutputV2KbLayoutHandlerFunc) {
i.kbLayoutHandler = f
}
// ZdwlIpcOutputV2KeymodeEvent : current keybind mode.
//
// current keybind mode.
type ZdwlIpcOutputV2KeymodeEvent struct {
Keymode string
}
type ZdwlIpcOutputV2KeymodeHandlerFunc func(ZdwlIpcOutputV2KeymodeEvent)
// SetKeymodeHandler : sets handler for ZdwlIpcOutputV2KeymodeEvent
func (i *ZdwlIpcOutputV2) SetKeymodeHandler(f ZdwlIpcOutputV2KeymodeHandlerFunc) {
i.keymodeHandler = f
}
// ZdwlIpcOutputV2ScalefactorEvent : scale factor of monitor.
//
// scale factor of monitor.
type ZdwlIpcOutputV2ScalefactorEvent struct {
Scalefactor uint32
}
type ZdwlIpcOutputV2ScalefactorHandlerFunc func(ZdwlIpcOutputV2ScalefactorEvent)
// SetScalefactorHandler : sets handler for ZdwlIpcOutputV2ScalefactorEvent
func (i *ZdwlIpcOutputV2) SetScalefactorHandler(f ZdwlIpcOutputV2ScalefactorHandlerFunc) {
i.scalefactorHandler = f
}
func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) { func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode { switch opcode {
case 0: case 0:
@@ -487,5 +681,111 @@ func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
var e ZdwlIpcOutputV2FrameEvent var e ZdwlIpcOutputV2FrameEvent
i.frameHandler(e) i.frameHandler(e)
case 8:
if i.fullscreenHandler == nil {
return
}
var e ZdwlIpcOutputV2FullscreenEvent
l := 0
e.IsFullscreen = client.Uint32(data[l : l+4])
l += 4
i.fullscreenHandler(e)
case 9:
if i.floatingHandler == nil {
return
}
var e ZdwlIpcOutputV2FloatingEvent
l := 0
e.IsFloating = client.Uint32(data[l : l+4])
l += 4
i.floatingHandler(e)
case 10:
if i.xHandler == nil {
return
}
var e ZdwlIpcOutputV2XEvent
l := 0
e.X = int32(client.Uint32(data[l : l+4]))
l += 4
i.xHandler(e)
case 11:
if i.yHandler == nil {
return
}
var e ZdwlIpcOutputV2YEvent
l := 0
e.Y = int32(client.Uint32(data[l : l+4]))
l += 4
i.yHandler(e)
case 12:
if i.widthHandler == nil {
return
}
var e ZdwlIpcOutputV2WidthEvent
l := 0
e.Width = int32(client.Uint32(data[l : l+4]))
l += 4
i.widthHandler(e)
case 13:
if i.heightHandler == nil {
return
}
var e ZdwlIpcOutputV2HeightEvent
l := 0
e.Height = int32(client.Uint32(data[l : l+4]))
l += 4
i.heightHandler(e)
case 14:
if i.lastLayerHandler == nil {
return
}
var e ZdwlIpcOutputV2LastLayerEvent
l := 0
lastLayerLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.LastLayer = client.String(data[l : l+lastLayerLen])
l += lastLayerLen
i.lastLayerHandler(e)
case 15:
if i.kbLayoutHandler == nil {
return
}
var e ZdwlIpcOutputV2KbLayoutEvent
l := 0
kbLayoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.KbLayout = client.String(data[l : l+kbLayoutLen])
l += kbLayoutLen
i.kbLayoutHandler(e)
case 16:
if i.keymodeHandler == nil {
return
}
var e ZdwlIpcOutputV2KeymodeEvent
l := 0
keymodeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Keymode = client.String(data[l : l+keymodeLen])
l += keymodeLen
i.keymodeHandler(e)
case 17:
if i.scalefactorHandler == nil {
return
}
var e ZdwlIpcOutputV2ScalefactorEvent
l := 0
e.Scalefactor = client.Uint32(data[l : l+4])
l += 4
i.scalefactorHandler(e)
} }
} }

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : ext-workspace-v1.xml // XML file : ext-workspace-v1.xml
// //
// ext_workspace_v1 Protocol Copyright: // ext_workspace_v1 Protocol Copyright:
@@ -35,7 +35,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
// registerServerProxy registers a proxy with a server-assigned ID. // registerServerProxy registers a proxy with a server-assigned ID.
@@ -61,8 +62,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
return return
} }
objectsMap := reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap.SetMapIndex(reflect.ValueOf(serverID), reflect.ValueOf(proxy)) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap.Store(serverID, proxy)
} }
// ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml // XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml
// //
// wlr_gamma_control_unstable_v1 Protocol Copyright: // wlr_gamma_control_unstable_v1 Protocol Copyright:
@@ -31,7 +31,7 @@
package wlr_gamma_control package wlr_gamma_control
import ( import (
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml // XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml
// //
// wlr_output_management_unstable_v1 Protocol Copyright: // wlr_output_management_unstable_v1 Protocol Copyright:
@@ -33,7 +33,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) { func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
@@ -47,9 +48,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
if !objectsField.IsValid() { if !objectsField.IsValid() {
return return
} }
objectsField = reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap := objectsField.Interface().(map[uint32]client.Proxy) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap[serverID] = proxy objectsMap.Store(serverID, proxy)
} }
// ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].

View File

@@ -0,0 +1,283 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/wlr-output-power-management-unstable-v1.xml
//
// wlr_output_power_management_unstable_v1 Protocol Copyright:
//
// Copyright © 2019 Purism SPC
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package wlr_output_power
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZwlrOutputPowerManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrOutputPowerManagerV1InterfaceName = "zwlr_output_power_manager_v1"
// ZwlrOutputPowerManagerV1 : manager to create per-output power management
//
// This interface is a manager that allows creating per-output power
// management mode controls.
type ZwlrOutputPowerManagerV1 struct {
client.BaseProxy
}
// NewZwlrOutputPowerManagerV1 : manager to create per-output power management
//
// This interface is a manager that allows creating per-output power
// management mode controls.
func NewZwlrOutputPowerManagerV1(ctx *client.Context) *ZwlrOutputPowerManagerV1 {
zwlrOutputPowerManagerV1 := &ZwlrOutputPowerManagerV1{}
ctx.Register(zwlrOutputPowerManagerV1)
return zwlrOutputPowerManagerV1
}
// GetOutputPower : get a power management for an output
//
// Create an output power management mode control that can be used to
// adjust the power management mode for a given output.
func (i *ZwlrOutputPowerManagerV1) GetOutputPower(output *client.Output) (*ZwlrOutputPowerV1, error) {
id := NewZwlrOutputPowerV1(i.Context())
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
// Destroy : destroy the manager
//
// All objects created by the manager will still remain valid, until their
// appropriate destroy request has been called.
func (i *ZwlrOutputPowerManagerV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 1
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// ZwlrOutputPowerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrOutputPowerV1InterfaceName = "zwlr_output_power_v1"
// ZwlrOutputPowerV1 : adjust power management mode for an output
//
// This object offers requests to set the power management mode of
// an output.
type ZwlrOutputPowerV1 struct {
client.BaseProxy
modeHandler ZwlrOutputPowerV1ModeHandlerFunc
failedHandler ZwlrOutputPowerV1FailedHandlerFunc
}
// NewZwlrOutputPowerV1 : adjust power management mode for an output
//
// This object offers requests to set the power management mode of
// an output.
func NewZwlrOutputPowerV1(ctx *client.Context) *ZwlrOutputPowerV1 {
zwlrOutputPowerV1 := &ZwlrOutputPowerV1{}
ctx.Register(zwlrOutputPowerV1)
return zwlrOutputPowerV1
}
// SetMode : Set an outputs power save mode
//
// Set an output's power save mode to the given mode. The mode change
// is effective immediately. If the output does not support the given
// mode a failed event is sent.
//
// mode: the power save mode to set
func (i *ZwlrOutputPowerV1) SetMode(mode uint32) error {
const opcode = 0
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(mode))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Destroy : destroy this power management
//
// Destroys the output power management mode control object.
func (i *ZwlrOutputPowerV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 1
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwlrOutputPowerV1Mode uint32
// ZwlrOutputPowerV1Mode :
const (
// ZwlrOutputPowerV1ModeOff : Output is turned off.
ZwlrOutputPowerV1ModeOff ZwlrOutputPowerV1Mode = 0
// ZwlrOutputPowerV1ModeOn : Output is turned on, no power saving
ZwlrOutputPowerV1ModeOn ZwlrOutputPowerV1Mode = 1
)
func (e ZwlrOutputPowerV1Mode) Name() string {
switch e {
case ZwlrOutputPowerV1ModeOff:
return "off"
case ZwlrOutputPowerV1ModeOn:
return "on"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Mode) Value() string {
switch e {
case ZwlrOutputPowerV1ModeOff:
return "0"
case ZwlrOutputPowerV1ModeOn:
return "1"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Mode) String() string {
return e.Name() + "=" + e.Value()
}
type ZwlrOutputPowerV1Error uint32
// ZwlrOutputPowerV1Error :
const (
// ZwlrOutputPowerV1ErrorInvalidMode : nonexistent power save mode
ZwlrOutputPowerV1ErrorInvalidMode ZwlrOutputPowerV1Error = 1
)
func (e ZwlrOutputPowerV1Error) Name() string {
switch e {
case ZwlrOutputPowerV1ErrorInvalidMode:
return "invalid_mode"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Error) Value() string {
switch e {
case ZwlrOutputPowerV1ErrorInvalidMode:
return "1"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Error) String() string {
return e.Name() + "=" + e.Value()
}
// ZwlrOutputPowerV1ModeEvent : Report a power management mode change
//
// Report the power management mode change of an output.
//
// The mode event is sent after an output changed its power
// management mode. The reason can be a client using set_mode or the
// compositor deciding to change an output's mode.
// This event is also sent immediately when the object is created
// so the client is informed about the current power management mode.
type ZwlrOutputPowerV1ModeEvent struct {
Mode uint32
}
type ZwlrOutputPowerV1ModeHandlerFunc func(ZwlrOutputPowerV1ModeEvent)
// SetModeHandler : sets handler for ZwlrOutputPowerV1ModeEvent
func (i *ZwlrOutputPowerV1) SetModeHandler(f ZwlrOutputPowerV1ModeHandlerFunc) {
i.modeHandler = f
}
// ZwlrOutputPowerV1FailedEvent : object no longer valid
//
// This event indicates that the output power management mode control
// is no longer valid. This can happen for a number of reasons,
// including:
// - The output doesn't support power management
// - Another client already has exclusive power management mode control
// for this output
// - The output disappeared
//
// Upon receiving this event, the client should destroy this object.
type ZwlrOutputPowerV1FailedEvent struct{}
type ZwlrOutputPowerV1FailedHandlerFunc func(ZwlrOutputPowerV1FailedEvent)
// SetFailedHandler : sets handler for ZwlrOutputPowerV1FailedEvent
func (i *ZwlrOutputPowerV1) SetFailedHandler(f ZwlrOutputPowerV1FailedHandlerFunc) {
i.failedHandler = f
}
func (i *ZwlrOutputPowerV1) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.modeHandler == nil {
return
}
var e ZwlrOutputPowerV1ModeEvent
l := 0
e.Mode = client.Uint32(data[l : l+4])
l += 4
i.modeHandler(e)
case 1:
if i.failedHandler == nil {
return
}
var e ZwlrOutputPowerV1FailedEvent
i.failedHandler(e)
}
}

View File

@@ -19,7 +19,7 @@ I would probably just submit raphi's patchset but I don't think that would be po
reset. reset.
</description> </description>
<interface name="zdwl_ipc_manager_v2" version="1"> <interface name="zdwl_ipc_manager_v2" version="2">
<description summary="manage dwl state"> <description summary="manage dwl state">
This interface is exposed as a global in wl_registry. This interface is exposed as a global in wl_registry.
@@ -60,7 +60,7 @@ I would probably just submit raphi's patchset but I don't think that would be po
</event> </event>
</interface> </interface>
<interface name="zdwl_ipc_output_v2" version="1"> <interface name="zdwl_ipc_output_v2" version="2">
<description summary="control dwl output"> <description summary="control dwl output">
Observe and control a dwl output. Observe and control a dwl output.
@@ -162,5 +162,91 @@ I would probably just submit raphi's patchset but I don't think that would be po
<description summary="Set the layout of this output"/> <description summary="Set the layout of this output"/>
<arg name="index" type="uint" summary="index of a layout recieved by dwl_ipc_manager.layout"/> <arg name="index" type="uint" summary="index of a layout recieved by dwl_ipc_manager.layout"/>
</request> </request>
<request name="quit" since="2">
<description summary="Quit mango">This request allows clients to instruct the compositor to quit mango.</description>
</request>
<request name="dispatch" since="2">
<description summary="Set the active tags of this output"/>
<arg name="dispatch" type="string" summary="dispatch name."/>
<arg name="arg1" type="string" summary="arg1."/>
<arg name="arg2" type="string" summary="arg2."/>
<arg name="arg3" type="string" summary="arg3."/>
<arg name="arg4" type="string" summary="arg4."/>
<arg name="arg5" type="string" summary="arg5."/>
</request>
<!-- Version 2 -->
<event name="fullscreen" since="2">
<description summary="Update fullscreen status">
Indicates if the selected client on this output is fullscreen.
</description>
<arg name="is_fullscreen" type="uint" summary="If the selected client is fullscreen. Nonzero is valid, zero invalid"/>
</event>
<event name="floating" since="2">
<description summary="Update the floating status">
Indicates if the selected client on this output is floating.
</description>
<arg name="is_floating" type="uint" summary="If the selected client is floating. Nonzero is valid, zero invalid"/>
</event>
<event name="x" since="2">
<description summary="Update the x coordinates">
Indicates if x coordinates of the selected client.
</description>
<arg name="x" type="int" summary="x coordinate of the selected client"/>
</event>
<event name="y" since="2">
<description summary="Update the y coordinates">
Indicates if y coordinates of the selected client.
</description>
<arg name="y" type="int" summary="y coordinate of the selected client"/>
</event>
<event name="width" since="2">
<description summary="Update the width">
Indicates if width of the selected client.
</description>
<arg name="width" type="int" summary="width of the selected client"/>
</event>
<event name="height" since="2">
<description summary="Update the height">
Indicates if height of the selected client.
</description>
<arg name="height" type="int" summary="height of the selected client"/>
</event>
<event name="last_layer" since="2">
<description summary="last map layer.">
last map layer.
</description>
<arg name="last_layer" type="string" summary="last map layer."/>
</event>
<event name="kb_layout" since="2">
<description summary="current keyboard layout.">
current keyboard layout.
</description>
<arg name="kb_layout" type="string" summary="current keyboard layout."/>
</event>
<event name="keymode" since="2">
<description summary="current keybind mode.">
current keybind mode.
</description>
<arg name="keymode" type="string" summary="current keybind mode."/>
</event>
<event name="scalefactor" since="2">
<description summary="scale factor of monitor.">
scale factor of monitor.
</description>
<arg name="scalefactor" type="uint" summary="scale factor of monitor."/>
</event>
</interface> </interface>
</protocol> </protocol>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_output_power_management_unstable_v1">
<copyright>
Copyright © 2019 Purism SPC
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="Control power management modes of outputs">
This protocol allows clients to control power management modes
of outputs that are currently part of the compositor space. The
intent is to allow special clients like desktop shells to power
down outputs when the system is idle.
To modify outputs not currently part of the compositor space see
wlr-output-management.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible changes
may be added together with the corresponding interface version bump.
Backward incompatible changes are done by bumping the version number in
the protocol and interface names and resetting the interface version.
Once the protocol is to be declared stable, the 'z' prefix and the
version number in the protocol and interface names are removed and the
interface version number is reset.
</description>
<interface name="zwlr_output_power_manager_v1" version="1">
<description summary="manager to create per-output power management">
This interface is a manager that allows creating per-output power
management mode controls.
</description>
<request name="get_output_power">
<description summary="get a power management for an output">
Create an output power management mode control that can be used to
adjust the power management mode for a given output.
</description>
<arg name="id" type="new_id" interface="zwlr_output_power_v1"/>
<arg name="output" type="object" interface="wl_output"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
</interface>
<interface name="zwlr_output_power_v1" version="1">
<description summary="adjust power management mode for an output">
This object offers requests to set the power management mode of
an output.
</description>
<enum name="mode">
<entry name="off" value="0"
summary="Output is turned off."/>
<entry name="on" value="1"
summary="Output is turned on, no power saving"/>
</enum>
<enum name="error">
<entry name="invalid_mode" value="1" summary="nonexistent power save mode"/>
</enum>
<request name="set_mode">
<description summary="Set an outputs power save mode">
Set an output's power save mode to the given mode. The mode change
is effective immediately. If the output does not support the given
mode a failed event is sent.
</description>
<arg name="mode" type="uint" enum="mode" summary="the power save mode to set"/>
</request>
<event name="mode">
<description summary="Report a power management mode change">
Report the power management mode change of an output.
The mode event is sent after an output changed its power
management mode. The reason can be a client using set_mode or the
compositor deciding to change an output's mode.
This event is also sent immediately when the object is created
so the client is informed about the current power management mode.
</description>
<arg name="mode" type="uint" enum="mode"
summary="the output's new power management mode"/>
</event>
<event name="failed">
<description summary="object no longer valid">
This event indicates that the output power management mode control
is no longer valid. This can happen for a number of reasons,
including:
- The output doesn't support power management
- Another client already has exclusive power management mode control
for this output
- The output disappeared
Upon receiving this event, the client should destroy this object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy this power management">
Destroys the output power management mode control object.
</description>
</request>
</interface>
</protocol>

View File

@@ -165,12 +165,11 @@ func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, ente
log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered) log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered)
if entered == 0 { if entered == 0 {
pk := passkey passkeyStr := strconv.FormatUint(uint64(passkey), 10)
_, err := a.promptFor(device, "display-passkey", []string{}, nil) _, err := a.promptFor(device, "display-passkey", []string{}, &passkeyStr)
if err != nil { if err != nil {
log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err) log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err)
} }
_ = pk
} }
return nil return nil
@@ -179,7 +178,8 @@ func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, ente
func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error {
log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey) log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey)
secrets, err := a.promptFor(device, "confirm", []string{"decision"}, nil) passkeyStr := strconv.FormatUint(uint64(passkey), 10)
secrets, err := a.promptFor(device, "confirm", []string{"decision"}, &passkeyStr)
if err != nil { if err != nil {
log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err) log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err)
return a.errorFrom(err) return a.errorFrom(err)

View File

@@ -31,15 +31,11 @@ func NewManager() (*Manager, error) {
ConnectedDevices: []Device{}, ConnectedDevices: []Device{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan BluetoothState),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dbusConn: conn, dbusConn: conn,
signals: make(chan *dbus.Signal, 256), signals: make(chan *dbus.Signal, 256),
pairingSubscribers: make(map[string]chan PairingPrompt),
pairingSubMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
pendingPairings: make(map[string]bool),
eventQueue: make(chan func(), 32), eventQueue: make(chan func(), 32),
} }
@@ -358,14 +354,10 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
_, hasTrusted := changed["Trusted"] _, hasTrusted := changed["Trusted"]
if hasPaired { if hasPaired {
if paired, ok := pairedVar.Value().(bool); ok && paired {
devicePath := string(path) devicePath := string(path)
m.pendingPairingsMux.Lock() if paired, ok := pairedVar.Value().(bool); ok {
wasPending := m.pendingPairings[devicePath] if paired {
if wasPending { _, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
delete(m.pendingPairings, devicePath)
}
m.pendingPairingsMux.Unlock()
if wasPending { if wasPending {
select { select {
@@ -379,6 +371,9 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
default: default:
} }
} }
} else {
m.pendingPairings.Delete(devicePath)
}
} }
} }
@@ -430,28 +425,20 @@ func (m *Manager) notifier() {
} }
m.updateDevices() m.updateDevices()
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -484,48 +471,36 @@ func (m *Manager) snapshotState() BluetoothState {
func (m *Manager) Subscribe(id string) chan BluetoothState { func (m *Manager) Subscribe(id string) chan BluetoothState {
ch := make(chan BluetoothState, 64) ch := make(chan BluetoothState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribePairing(id string) chan PairingPrompt { func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
ch := make(chan PairingPrompt, 16) ch := make(chan PairingPrompt, 16)
m.pairingSubMutex.Lock() m.pairingSubscribers.Store(id, ch)
m.pairingSubscribers[id] = ch
m.pairingSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribePairing(id string) { func (m *Manager) UnsubscribePairing(id string) {
m.pairingSubMutex.Lock() if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.pairingSubscribers[id]; ok {
close(ch) close(ch)
delete(m.pairingSubscribers, id)
} }
m.pairingSubMutex.Unlock()
} }
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) { func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
m.pairingSubMutex.RLock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
defer m.pairingSubMutex.RUnlock()
for _, ch := range m.pairingSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error { func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error {
@@ -566,17 +541,13 @@ func (m *Manager) SetPowered(powered bool) error {
} }
func (m *Manager) PairDevice(devicePath string) error { func (m *Manager) PairDevice(devicePath string) error {
m.pendingPairingsMux.Lock() m.pendingPairings.Store(devicePath, true)
m.pendingPairings[devicePath] = true
m.pendingPairingsMux.Unlock()
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
err := obj.Call(device1Iface+".Pair", 0).Err err := obj.Call(device1Iface+".Pair", 0).Err
if err != nil { if err != nil {
m.pendingPairingsMux.Lock() m.pendingPairings.Delete(devicePath)
delete(m.pendingPairings, devicePath)
m.pendingPairingsMux.Unlock()
} }
return err return err
@@ -618,19 +589,17 @@ func (m *Manager) Close() {
m.agent.Close() m.agent.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan BluetoothState) return true
m.subMutex.Unlock() })
m.pairingSubMutex.Lock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
for _, ch := range m.pairingSubscribers {
close(ch) close(ch)
} m.pairingSubscribers.Delete(key)
m.pairingSubscribers = make(map[string]chan PairingPrompt) return true
m.pairingSubMutex.Unlock() })
if m.dbusConn != nil { if m.dbusConn != nil {
m.dbusConn.Close() m.dbusConn.Close()

View File

@@ -3,22 +3,19 @@ package bluez
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest
broadcastPrompt func(PairingPrompt) broadcastPrompt func(PairingPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply),
requests: make(map[string]PromptRequest),
broadcastPrompt: broadcastPrompt, broadcastPrompt: broadcastPrompt,
} }
} }
@@ -30,10 +27,8 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := PairingPrompt{ prompt := PairingPrompt{
@@ -53,10 +48,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -75,10 +67,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
} }
@@ -92,8 +81,6 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() b.pending.Delete(token)
delete(b.pending, token) b.requests.Delete(token)
delete(b.requests, token)
b.mu.Unlock()
} }

View File

@@ -3,6 +3,7 @@ package bluez
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -59,22 +60,19 @@ type PairingPrompt struct {
type Manager struct { type Manager struct {
state *BluetoothState state *BluetoothState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan BluetoothState subscribers syncmap.Map[string, chan BluetoothState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dbusConn *dbus.Conn dbusConn *dbus.Conn
signals chan *dbus.Signal signals chan *dbus.Signal
sigWG sync.WaitGroup sigWG sync.WaitGroup
agent *BluezAgent agent *BluezAgent
promptBroker PromptBroker promptBroker PromptBroker
pairingSubscribers map[string]chan PairingPrompt pairingSubscribers syncmap.Map[string, chan PairingPrompt]
pairingSubMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *BluetoothState lastNotifiedState *BluetoothState
adapterPath dbus.ObjectPath adapterPath dbus.ObjectPath
pendingPairings map[string]bool pendingPairings syncmap.Map[string, bool]
pendingPairingsMux sync.Mutex
eventQueue chan func() eventQueue chan func()
eventWg sync.WaitGroup eventWg sync.WaitGroup
} }

View File

@@ -24,7 +24,6 @@ const (
func NewDDCBackend() (*DDCBackend, error) { func NewDDCBackend() (*DDCBackend, error) {
b := &DDCBackend{ b := &DDCBackend{
devices: make(map[string]*ddcDevice),
scanInterval: 30 * time.Second, scanInterval: 30 * time.Second,
debounceTimers: make(map[string]*time.Timer), debounceTimers: make(map[string]*time.Timer),
debouncePending: make(map[string]ddcPendingSet), debouncePending: make(map[string]ddcPendingSet),
@@ -53,10 +52,10 @@ func (b *DDCBackend) scanI2CDevices() error {
return nil return nil
} }
b.devicesMutex.Lock() b.devices.Range(func(key string, value *ddcDevice) bool {
defer b.devicesMutex.Unlock() b.devices.Delete(key)
return true
b.devices = make(map[string]*ddcDevice) })
for i := 0; i < 32; i++ { for i := 0; i < 32; i++ {
busPath := fmt.Sprintf("/dev/i2c-%d", i) busPath := fmt.Sprintf("/dev/i2c-%d", i)
@@ -64,7 +63,6 @@ func (b *DDCBackend) scanI2CDevices() error {
continue continue
} }
// Skip SMBus, GPU internal buses (e.g. AMDGPU SMU) to prevent GPU hangs
if isIgnorableI2CBus(i) { if isIgnorableI2CBus(i) {
log.Debugf("Skipping ignorable i2c-%d", i) log.Debugf("Skipping ignorable i2c-%d", i)
continue continue
@@ -77,7 +75,7 @@ func (b *DDCBackend) scanI2CDevices() error {
id := fmt.Sprintf("ddc:i2c-%d", i) id := fmt.Sprintf("ddc:i2c-%d", i)
dev.id = id dev.id = id
b.devices[id] = dev b.devices.Store(id, dev)
log.Debugf("found DDC device on i2c-%d", i) log.Debugf("found DDC device on i2c-%d", i)
} }
@@ -100,7 +98,7 @@ func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) {
} }
dummy := make([]byte, 32) dummy := make([]byte, 32)
syscall.Read(fd, dummy) syscall.Read(fd, dummy) //nolint:errcheck
writebuf := []byte{0x00} writebuf := []byte{0x00}
n, err := syscall.Write(fd, writebuf) n, err := syscall.Write(fd, writebuf)
@@ -164,12 +162,9 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
log.Debugf("DDC scan error: %v", err) log.Debugf("DDC scan error: %v", err)
} }
b.devicesMutex.Lock() devices := make([]Device, 0)
defer b.devicesMutex.Unlock()
devices := make([]Device, 0, len(b.devices)) b.devices.Range(func(id string, dev *ddcDevice) bool {
for id, dev := range b.devices {
devices = append(devices, Device{ devices = append(devices, Device{
Class: ClassDDC, Class: ClassDDC,
ID: id, ID: id,
@@ -179,7 +174,8 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
CurrentPercent: dev.lastBrightness, CurrentPercent: dev.lastBrightness,
Backend: "ddc", Backend: "ddc",
}) })
} return true
})
return devices, nil return devices, nil
} }
@@ -189,9 +185,7 @@ func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callb
} }
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error { func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
b.devicesMutex.RLock() _, ok := b.devices.Load(id)
_, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -202,8 +196,6 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
b.debounceMutex.Lock() b.debounceMutex.Lock()
defer b.debounceMutex.Unlock()
b.debouncePending[id] = ddcPendingSet{ b.debouncePending[id] = ddcPendingSet{
percent: value, percent: value,
callback: callback, callback: callback,
@@ -234,14 +226,13 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
}) })
} }
b.debounceMutex.Unlock()
return nil return nil
} }
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error { func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error {
b.devicesMutex.RLock() dev, ok := b.devices.Load(id)
dev, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -266,9 +257,8 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
return fmt.Errorf("get current capability: %w", err) return fmt.Errorf("get current capability: %w", err)
} }
max = cap.max max = cap.max
b.devicesMutex.Lock()
dev.max = max dev.max = max
b.devicesMutex.Unlock() b.devices.Store(id, dev)
} }
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil { if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil {
@@ -277,10 +267,9 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
log.Debugf("set %s to %d/%d", id, value, max) log.Debugf("set %s to %d/%d", id, value, max)
b.devicesMutex.Lock()
dev.max = max dev.max = max
dev.lastBrightness = value dev.lastBrightness = value
b.devicesMutex.Unlock() b.devices.Store(id, dev)
return nil return nil
} }

View File

@@ -14,7 +14,6 @@ type DBusConn interface {
type LogindBackend struct { type LogindBackend struct {
conn DBusConn conn DBusConn
connOnce bool
} }
func NewLogindBackend() (*LogindBackend, error) { func NewLogindBackend() (*LogindBackend, error) {

View File

@@ -15,8 +15,6 @@ func NewManager() (*Manager, error) {
func NewManagerWithOptions(exponential bool) (*Manager, error) { func NewManagerWithOptions(exponential bool) (*Manager, error) {
m := &Manager{ m := &Manager{
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
exponential: exponential, exponential: exponential,
} }
@@ -360,20 +358,13 @@ func (m *Manager) broadcastDeviceUpdate(deviceID string) {
update := DeviceUpdate{Device: *targetDevice} update := DeviceUpdate{Device: *targetDevice}
m.subMutex.RLock()
defer m.subMutex.RUnlock()
if len(m.updateSubscribers) == 0 {
log.Debugf("No update subscribers for device: %s", deviceID)
return
}
log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent) log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent)
for _, ch := range m.updateSubscribers { m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
select { select {
case ch <- update: case ch <- update:
default: default:
} }
} return true
})
} }

View File

@@ -15,7 +15,6 @@ func NewSysfsBackend() (*SysfsBackend, error) {
b := &SysfsBackend{ b := &SysfsBackend{
basePath: "/sys/class", basePath: "/sys/class",
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
@@ -26,9 +25,6 @@ func NewSysfsBackend() (*SysfsBackend, error) {
} }
func (b *SysfsBackend) scanDevices() error { func (b *SysfsBackend) scanDevices() error {
b.deviceCacheMutex.Lock()
defer b.deviceCacheMutex.Unlock()
for _, class := range b.classes { for _, class := range b.classes {
classPath := filepath.Join(b.basePath, class) classPath := filepath.Join(b.basePath, class)
entries, err := os.ReadDir(classPath) entries, err := os.ReadDir(classPath)
@@ -68,13 +64,13 @@ func (b *SysfsBackend) scanDevices() error {
} }
deviceID := fmt.Sprintf("%s:%s", class, entry.Name()) deviceID := fmt.Sprintf("%s:%s", class, entry.Name())
b.deviceCache[deviceID] = &sysfsDevice{ b.deviceCache.Store(deviceID, &sysfsDevice{
class: deviceClass, class: deviceClass,
id: deviceID, id: deviceID,
name: entry.Name(), name: entry.Name(),
maxBrightness: maxBrightness, maxBrightness: maxBrightness,
minValue: minValue, minValue: minValue,
} })
log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness) log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness)
} }
@@ -106,19 +102,16 @@ func shouldSuppressDevice(name string) bool {
} }
func (b *SysfsBackend) GetDevices() ([]Device, error) { func (b *SysfsBackend) GetDevices() ([]Device, error) {
b.deviceCacheMutex.RLock() devices := make([]Device, 0)
defer b.deviceCacheMutex.RUnlock()
devices := make([]Device, 0, len(b.deviceCache)) b.deviceCache.Range(func(key string, dev *sysfsDevice) bool {
for _, dev := range b.deviceCache {
if shouldSuppressDevice(dev.name) { if shouldSuppressDevice(dev.name) {
continue return true
} }
parts := strings.SplitN(dev.id, ":", 2) parts := strings.SplitN(dev.id, ":", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue return true
} }
class := parts[0] class := parts[0]
@@ -130,13 +123,13 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
brightnessData, err := os.ReadFile(brightnessPath) brightnessData, err := os.ReadFile(brightnessPath)
if err != nil { if err != nil {
log.Debugf("failed to read brightness for %s: %v", dev.id, err) log.Debugf("failed to read brightness for %s: %v", dev.id, err)
continue return true
} }
current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData)))
if err != nil { if err != nil {
log.Debugf("failed to parse brightness for %s: %v", dev.id, err) log.Debugf("failed to parse brightness for %s: %v", dev.id, err)
continue return true
} }
percent := b.ValueToPercent(current, dev, false) percent := b.ValueToPercent(current, dev, false)
@@ -150,16 +143,14 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
CurrentPercent: percent, CurrentPercent: percent,
Backend: "sysfs", Backend: "sysfs",
}) })
} return true
})
return devices, nil return devices, nil
} }
func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) {
b.deviceCacheMutex.RLock() dev, ok := b.deviceCache.Load(id)
defer b.deviceCacheMutex.RUnlock()
dev, ok := b.deviceCache[id]
if !ok { if !ok {
return nil, fmt.Errorf("device not found: %s", id) return nil, fmt.Errorf("device not found: %s", id)
} }

View File

@@ -33,7 +33,6 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -45,8 +44,6 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
@@ -107,7 +104,6 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -119,8 +115,6 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
@@ -177,7 +171,6 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -189,8 +182,6 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: false, logindReady: false,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
@@ -242,7 +233,6 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"leds"}, classes: []string{"leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -254,8 +244,6 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }

View File

@@ -162,24 +162,19 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
b := &SysfsBackend{ b := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
t.Fatalf("scanDevices() error = %v", err) t.Fatalf("scanDevices() error = %v", err)
} }
if len(b.deviceCache) != 2 {
t.Errorf("expected 2 devices, got %d", len(b.deviceCache))
}
backlightID := "backlight:test_backlight" backlightID := "backlight:test_backlight"
if _, ok := b.deviceCache[backlightID]; !ok { if _, ok := b.deviceCache.Load(backlightID); !ok {
t.Errorf("backlight device not found") t.Errorf("backlight device not found")
} }
ledID := "leds:test_led" ledID := "leds:test_led"
if _, ok := b.deviceCache[ledID]; !ok { if _, ok := b.deviceCache.Load(ledID); !ok {
t.Errorf("LED device not found") t.Errorf("LED device not found")
} }
} }

View File

@@ -3,6 +3,8 @@ package brightness
import ( import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type DeviceClass string type DeviceClass string
@@ -51,9 +53,8 @@ type Manager struct {
stateMutex sync.RWMutex stateMutex sync.RWMutex
state State state State
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
updateSubscribers map[string]chan DeviceUpdate updateSubscribers syncmap.Map[string, chan DeviceUpdate]
subMutex sync.RWMutex
broadcastMutex sync.Mutex broadcastMutex sync.Mutex
broadcastTimer *time.Timer broadcastTimer *time.Timer
@@ -67,8 +68,7 @@ type SysfsBackend struct {
basePath string basePath string
classes []string classes []string
deviceCache map[string]*sysfsDevice deviceCache syncmap.Map[string, *sysfsDevice]
deviceCacheMutex sync.RWMutex
} }
type sysfsDevice struct { type sysfsDevice struct {
@@ -80,8 +80,7 @@ type sysfsDevice struct {
} }
type DDCBackend struct { type DDCBackend struct {
devices map[string]*ddcDevice devices syncmap.Map[string, *ddcDevice]
devicesMutex sync.RWMutex
scanMutex sync.Mutex scanMutex sync.Mutex
lastScan time.Time lastScan time.Time
@@ -121,36 +120,31 @@ type SetBrightnessParams struct {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16) ch := make(chan State, 16)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate { func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate {
ch := make(chan DeviceUpdate, 16) ch := make(chan DeviceUpdate, 16)
m.subMutex.Lock() m.updateSubscribers.Store(id, ch)
m.updateSubscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeUpdates(id string) { func (m *Manager) UnsubscribeUpdates(id string) {
m.subMutex.Lock() if val, ok := m.updateSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.updateSubscribers[id]; ok { close(val)
close(ch)
delete(m.updateSubscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
@@ -158,15 +152,13 @@ func (m *Manager) NotifySubscribers() {
state := m.state state := m.state
m.stateMutex.RUnlock() m.stateMutex.RUnlock()
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
defer m.subMutex.RUnlock()
for _, ch := range m.subscribers {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
@@ -178,16 +170,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
for _, ch := range m.updateSubscribers { })
m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
close(ch) close(ch)
} m.updateSubscribers.Delete(key)
m.updateSubscribers = make(map[string]chan DeviceUpdate) return true
m.subMutex.Unlock() })
if m.logindBackend != nil { if m.logindBackend != nil {
m.logindBackend.Close() m.logindBackend.Close()

View File

@@ -1,12 +1,34 @@
package cups package cups
import ( import (
"errors"
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
) )
func isAuthError(err error) bool {
if err == nil {
return false
}
var httpErr ipp.HTTPError
if errors.As(err, &httpErr) {
return httpErr.Code == 401 || httpErr.Code == 403
}
var ippErr ipp.IPPError
if errors.As(err, &ippErr) {
return ippErr.Status == ipp.StatusErrorForbidden ||
ippErr.Status == ipp.StatusErrorNotAuthenticated ||
ippErr.Status == ipp.StatusErrorNotAuthorized
}
return false
}
func (m *Manager) GetPrinters() ([]Printer, error) { func (m *Manager) GetPrinters() ([]Printer, error) {
attributes := []string{ attributes := []string{
ipp.AttributePrinterName, ipp.AttributePrinterName,
@@ -21,6 +43,9 @@ func (m *Manager) GetPrinters() ([]Printer, error) {
printerAttrs, err := m.client.GetPrinters(attributes) printerAttrs, err := m.client.GetPrinters(attributes)
if err != nil { if err != nil {
if isNoPrintersError(err) {
return []Printer{}, nil
}
return nil, err return nil, err
} }
@@ -91,17 +116,289 @@ func (m *Manager) GetJobs(printerName string, whichJobs string) ([]Job, error) {
} }
func (m *Manager) CancelJob(jobID int) error { func (m *Manager) CancelJob(jobID int) error {
return m.client.CancelJob(jobID, false) err := m.client.CancelJob(jobID, false)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.JobCancelPurge(jobID, false)
}
if err == nil {
m.RefreshState()
}
return err
} }
func (m *Manager) PausePrinter(printerName string) error { func (m *Manager) PausePrinter(printerName string) error {
return m.client.PausePrinter(printerName) err := m.client.PausePrinter(printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetEnabled(printerName, false)
}
if err == nil {
m.RefreshState()
}
return err
} }
func (m *Manager) ResumePrinter(printerName string) error { func (m *Manager) ResumePrinter(printerName string) error {
return m.client.ResumePrinter(printerName) err := m.client.ResumePrinter(printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetEnabled(printerName, true)
}
if err == nil {
m.RefreshState()
}
return err
} }
func (m *Manager) PurgeJobs(printerName string) error { func (m *Manager) PurgeJobs(printerName string) error {
return m.client.CancelAllJob(printerName, true) err := m.client.CancelAllJob(printerName, true)
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) GetDevices() ([]Device, error) {
if m.pkHelper != nil {
return m.pkHelper.DevicesGet(10, 0, nil, nil)
}
deviceAttrs, err := m.client.GetDevices()
if err != nil {
return nil, err
}
devices := make([]Device, 0, len(deviceAttrs))
for uri, attrs := range deviceAttrs {
device := Device{
URI: uri,
Class: getStringAttr(attrs, "device-class"),
Info: getStringAttr(attrs, "device-info"),
MakeModel: getStringAttr(attrs, "device-make-and-model"),
ID: getStringAttr(attrs, "device-id"),
Location: getStringAttr(attrs, "device-location"),
}
devices = append(devices, device)
}
return devices, nil
}
func (m *Manager) GetPPDs() ([]PPD, error) {
ppdAttrs, err := m.client.GetPPDs()
if err != nil {
return nil, err
}
ppds := make([]PPD, 0, len(ppdAttrs))
for name, attrs := range ppdAttrs {
ppd := PPD{
Name: name,
NaturalLanguage: getStringAttr(attrs, "ppd-natural-language"),
MakeModel: getStringAttr(attrs, ipp.AttributePPDMakeAndModel),
DeviceID: getStringAttr(attrs, "ppd-device-id"),
Product: getStringAttr(attrs, "ppd-product"),
PSVersion: getStringAttr(attrs, "ppd-psversion"),
Type: getStringAttr(attrs, "ppd-type"),
}
ppds = append(ppds, ppd)
}
return ppds, nil
}
func (m *Manager) GetClasses() ([]PrinterClass, error) {
attributes := []string{
ipp.AttributePrinterName,
ipp.AttributePrinterUriSupported,
ipp.AttributePrinterState,
ipp.AttributeMemberURIs,
ipp.AttributeMemberNames,
ipp.AttributePrinterLocation,
ipp.AttributePrinterInfo,
}
classAttrs, err := m.client.GetClasses(attributes)
if err != nil {
return nil, err
}
classes := make([]PrinterClass, 0, len(classAttrs))
for _, attrs := range classAttrs {
class := PrinterClass{
Name: getStringAttr(attrs, ipp.AttributePrinterName),
URI: getStringAttr(attrs, ipp.AttributePrinterUriSupported),
State: parsePrinterState(attrs),
Location: getStringAttr(attrs, ipp.AttributePrinterLocation),
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
Members: getStringSliceAttr(attrs, ipp.AttributeMemberNames),
}
classes = append(classes, class)
}
return classes, nil
}
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
usedPkHelper := false
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
if isAuthError(err) && m.pkHelper != nil {
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
return err
}
usedPkHelper = true
} else if err != nil {
return err
}
if usedPkHelper {
m.pkHelper.PrinterSetEnabled(name, true) //nolint:errcheck
m.pkHelper.PrinterSetAcceptJobs(name, true, "") //nolint:errcheck
} else {
if err := m.client.ResumePrinter(name); isAuthError(err) && m.pkHelper != nil {
m.pkHelper.PrinterSetEnabled(name, true) //nolint:errcheck
}
if err := m.client.AcceptJobs(name); isAuthError(err) && m.pkHelper != nil {
m.pkHelper.PrinterSetAcceptJobs(name, true, "") //nolint:errcheck
}
}
m.RefreshState()
return nil
}
func (m *Manager) DeletePrinter(printerName string) error {
err := m.client.DeletePrinter(printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterDelete(printerName)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) AcceptJobs(printerName string) error {
err := m.client.AcceptJobs(printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetAcceptJobs(printerName, true, "")
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) RejectJobs(printerName string) error {
err := m.client.RejectJobs(printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetAcceptJobs(printerName, false, "")
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) SetPrinterShared(printerName string, shared bool) error {
err := m.client.SetPrinterIsShared(printerName, shared)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetShared(printerName, shared)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) SetPrinterLocation(printerName, location string) error {
err := m.client.SetPrinterLocation(printerName, location)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetLocation(printerName, location)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) SetPrinterInfo(printerName, info string) error {
err := m.client.SetPrinterInformation(printerName, info)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterSetInfo(printerName, info)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) MoveJob(jobID int, destPrinter string) error {
err := m.client.MoveJob(jobID, destPrinter)
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) PrintTestPage(printerName string) (int, error) {
jobID, err := m.client.PrintTestPage(printerName, strings.NewReader(config.TestPage), len(config.TestPage))
if err == nil {
m.RefreshState()
}
return jobID, err
}
func (m *Manager) AddPrinterToClass(className, printerName string) error {
err := m.client.AddPrinterToClass(className, printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.ClassAddPrinter(className, printerName)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) RemovePrinterFromClass(className, printerName string) error {
err := m.client.DeletePrinterFromClass(className, printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.ClassDeletePrinter(className, printerName)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) DeleteClass(className string) error {
err := m.client.DeleteClass(className)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.ClassDelete(className)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) RestartJob(jobID int) error {
err := m.client.RestartJob(jobID)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.JobRestart(jobID)
}
if err == nil {
m.RefreshState()
}
return err
}
func (m *Manager) HoldJob(jobID int, holdUntil string) error {
err := m.client.HoldJobUntil(jobID, holdUntil)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.JobSetHoldUntil(jobID, holdUntil)
}
if err == nil {
m.RefreshState()
}
return err
} }

View File

@@ -0,0 +1,235 @@
package cups_test
import (
"testing"
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
mocks_pkhelper "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups_pkhelper"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func authErr() error {
return ipp.IPPError{Status: ipp.StatusErrorForbidden}
}
func TestManager_CancelJob_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().JobCancelPurge(1, false).Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.CancelJob(1))
}
func TestManager_CancelJob_PkHelperError(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(authErr())
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().JobCancelPurge(1, false).Return(assert.AnError)
m := cups.NewTestManager(mockClient, mockPk)
assert.Error(t, m.CancelJob(1))
}
func TestManager_PausePrinter_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetEnabled("printer1", false).Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.PausePrinter("printer1"))
}
func TestManager_ResumePrinter_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetEnabled("printer1", true).Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.ResumePrinter("printer1"))
}
func TestManager_GetDevices_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().DevicesGet(10, 0, []string(nil), []string(nil)).Return([]cups.Device{
{URI: "usb://HP/LaserJet", Class: "direct"},
}, nil)
m := cups.NewTestManager(mockClient, mockPk)
got, err := m.GetDevices()
assert.NoError(t, err)
assert.Len(t, got, 1)
assert.Equal(t, "usb://HP/LaserJet", got[0].URI)
}
func TestManager_GetDevices_PkHelperError(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().DevicesGet(10, 0, []string(nil), []string(nil)).Return(nil, assert.AnError)
m := cups.NewTestManager(mockClient, mockPk)
_, err := m.GetDevices()
assert.Error(t, err)
}
func TestManager_CreatePrinter_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterAdd("newprinter", "usb://HP", "generic.ppd", "info", "location").Return(nil)
mockPk.EXPECT().PrinterSetEnabled("newprinter", true).Return(nil)
mockPk.EXPECT().PrinterSetAcceptJobs("newprinter", true, "").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location"))
}
func TestManager_DeletePrinter_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeletePrinter("printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterDelete("printer1").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.DeletePrinter("printer1"))
}
func TestManager_AcceptJobs_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().AcceptJobs("printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetAcceptJobs("printer1", true, "").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.AcceptJobs("printer1"))
}
func TestManager_RejectJobs_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().RejectJobs("printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetAcceptJobs("printer1", false, "").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.RejectJobs("printer1"))
}
func TestManager_SetPrinterShared_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetShared("printer1", true).Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.SetPrinterShared("printer1", true))
}
func TestManager_SetPrinterLocation_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetLocation("printer1", "Office").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.SetPrinterLocation("printer1", "Office"))
}
func TestManager_SetPrinterInfo_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().PrinterSetInfo("printer1", "Main Printer").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.SetPrinterInfo("printer1", "Main Printer"))
}
func TestManager_AddPrinterToClass_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().ClassAddPrinter("office", "printer1").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.AddPrinterToClass("office", "printer1"))
}
func TestManager_RemovePrinterFromClass_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().ClassDeletePrinter("office", "printer1").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.RemovePrinterFromClass("office", "printer1"))
}
func TestManager_DeleteClass_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeleteClass("office").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().ClassDelete("office").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.DeleteClass("office"))
}
func TestManager_RestartJob_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().RestartJob(1).Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().JobRestart(1).Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.RestartJob(1))
}
func TestManager_HoldJob_WithPkHelper(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(authErr())
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
mockPk := mocks_pkhelper.NewMockPkHelper(t)
mockPk.EXPECT().JobSetHoldUntil(1, "indefinite").Return(nil)
m := cups.NewTestManager(mockClient, mockPk)
assert.NoError(t, m.HoldJob(1, "indefinite"))
}

View File

@@ -137,114 +137,30 @@ func TestManager_GetJobs(t *testing.T) {
} }
func TestManager_CancelJob(t *testing.T) { func TestManager_CancelJob(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(tt.mockErr) mockClient.EXPECT().CancelJob(1, false).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient, assert.NoError(t, m.CancelJob(1))
}
err := m.CancelJob(1)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
} }
func TestManager_PausePrinter(t *testing.T) { func TestManager_PausePrinter(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(tt.mockErr) mockClient.EXPECT().PausePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient, assert.NoError(t, m.PausePrinter("printer1"))
}
err := m.PausePrinter("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
} }
func TestManager_ResumePrinter(t *testing.T) { func TestManager_ResumePrinter(t *testing.T) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(tt.mockErr) mockClient.EXPECT().ResumePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient, assert.NoError(t, m.ResumePrinter("printer1"))
}
err := m.ResumePrinter("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
} }
func TestManager_PurgeJobs(t *testing.T) { func TestManager_PurgeJobs(t *testing.T) {
@@ -269,11 +185,12 @@ func TestManager_PurgeJobs(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr) mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr)
if !tt.wantErr {
m := &Manager{ mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
client: mockClient,
} }
m := NewTestManager(mockClient, nil)
err := m.PurgeJobs("printer1") err := m.PurgeJobs("printer1")
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
@@ -283,3 +200,251 @@ func TestManager_PurgeJobs(t *testing.T) {
}) })
} }
} }
func TestManager_GetDevices(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetDevices().Return(map[string]ipp.Attributes{
"usb://HP/LaserJet": {
"device-class": []ipp.Attribute{{Value: "direct"}},
"device-info": []ipp.Attribute{{Value: "HP LaserJet"}},
"device-make-and-model": []ipp.Attribute{{Value: "HP LaserJet 1020"}},
},
}, nil)
m := &Manager{client: mockClient}
got, err := m.GetDevices()
assert.NoError(t, err)
assert.Len(t, got, 1)
assert.Equal(t, "usb://HP/LaserJet", got[0].URI)
assert.Equal(t, "direct", got[0].Class)
}
func TestManager_GetPPDs(t *testing.T) {
tests := []struct {
name string
mockRet map[string]ipp.Attributes
mockErr error
want int
wantErr bool
}{
{
name: "success",
mockRet: map[string]ipp.Attributes{
"drv:///sample.drv/generic.ppd": {
"ppd-make-and-model": []ipp.Attribute{{Value: "Generic PostScript"}},
"ppd-type": []ipp.Attribute{{Value: "ppd"}},
},
},
mockErr: nil,
want: 1,
wantErr: false,
},
{
name: "error",
mockRet: nil,
mockErr: errors.New("test error"),
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetPPDs().Return(tt.mockRet, tt.mockErr)
m := &Manager{client: mockClient}
got, err := m.GetPPDs()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, len(got))
})
}
}
func TestManager_GetClasses(t *testing.T) {
tests := []struct {
name string
mockRet map[string]ipp.Attributes
mockErr error
want int
wantErr bool
}{
{
name: "success",
mockRet: map[string]ipp.Attributes{
"office": {
ipp.AttributePrinterName: []ipp.Attribute{{Value: "office"}},
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
ipp.AttributeMemberNames: []ipp.Attribute{{Value: "printer1"}, {Value: "printer2"}},
},
},
mockErr: nil,
want: 1,
wantErr: false,
},
{
name: "error",
mockRet: nil,
mockErr: errors.New("test error"),
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetClasses(mock.Anything).Return(tt.mockRet, tt.mockErr)
m := &Manager{client: mockClient}
got, err := m.GetClasses()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, len(got))
if len(got) > 0 {
assert.Equal(t, "office", got[0].Name)
assert.Equal(t, 2, len(got[0].Members))
}
})
}
}
func TestManager_CreatePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location").Return(nil)
mockClient.EXPECT().ResumePrinter("newprinter").Return(nil)
mockClient.EXPECT().AcceptJobs("newprinter").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location"))
}
func TestManager_DeletePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeletePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.DeletePrinter("printer1"))
}
func TestManager_AcceptJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().AcceptJobs("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.AcceptJobs("printer1"))
}
func TestManager_RejectJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().RejectJobs("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.RejectJobs("printer1"))
}
func TestManager_SetPrinterShared(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.SetPrinterShared("printer1", true))
}
func TestManager_SetPrinterLocation(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.SetPrinterLocation("printer1", "Office"))
}
func TestManager_SetPrinterInfo(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.SetPrinterInfo("printer1", "Main Printer"))
}
func TestManager_MoveJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().MoveJob(1, "printer2").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
err := m.MoveJob(1, "printer2")
assert.NoError(t, err)
}
func TestManager_PrintTestPage(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PrintTestPage("printer1", mock.Anything, mock.Anything).Return(42, nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
jobID, err := m.PrintTestPage("printer1")
assert.NoError(t, err)
assert.Equal(t, 42, jobID)
}
func TestManager_AddPrinterToClass(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.AddPrinterToClass("office", "printer1"))
}
func TestManager_RemovePrinterFromClass(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.RemovePrinterFromClass("office", "printer1"))
}
func TestManager_DeleteClass(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeleteClass("office").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.DeleteClass("office"))
}
func TestManager_RestartJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().RestartJob(1).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.RestartJob(1))
}
func TestManager_HoldJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.HoldJob(1, "indefinite"))
}

View File

@@ -40,6 +40,40 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) {
handleCancelJob(conn, req, manager) handleCancelJob(conn, req, manager)
case "cups.purgeJobs": case "cups.purgeJobs":
handlePurgeJobs(conn, req, manager) handlePurgeJobs(conn, req, manager)
case "cups.getDevices":
handleGetDevices(conn, req, manager)
case "cups.getPPDs":
handleGetPPDs(conn, req, manager)
case "cups.getClasses":
handleGetClasses(conn, req, manager)
case "cups.createPrinter":
handleCreatePrinter(conn, req, manager)
case "cups.deletePrinter":
handleDeletePrinter(conn, req, manager)
case "cups.acceptJobs":
handleAcceptJobs(conn, req, manager)
case "cups.rejectJobs":
handleRejectJobs(conn, req, manager)
case "cups.setPrinterShared":
handleSetPrinterShared(conn, req, manager)
case "cups.setPrinterLocation":
handleSetPrinterLocation(conn, req, manager)
case "cups.setPrinterInfo":
handleSetPrinterInfo(conn, req, manager)
case "cups.moveJob":
handleMoveJob(conn, req, manager)
case "cups.printTestPage":
handlePrintTestPage(conn, req, manager)
case "cups.addPrinterToClass":
handleAddPrinterToClass(conn, req, manager)
case "cups.removePrinterFromClass":
handleRemovePrinterFromClass(conn, req, manager)
case "cups.deleteClass":
handleDeleteClass(conn, req, manager)
case "cups.restartJob":
handleRestartJob(conn, req, manager)
case "cups.holdJob":
handleHoldJob(conn, req, manager)
default: default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
} }
@@ -158,3 +192,291 @@ func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
} }
} }
} }
func handleGetDevices(conn net.Conn, req Request, manager *Manager) {
devices, err := manager.GetDevices()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, devices)
}
func handleGetPPDs(conn net.Conn, req Request, manager *Manager) {
ppds, err := manager.GetPPDs()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, ppds)
}
func handleGetClasses(conn net.Conn, req Request, manager *Manager) {
classes, err := manager.GetClasses()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, classes)
}
func handleCreatePrinter(conn net.Conn, req Request, manager *Manager) {
name, ok := req.Params["name"].(string)
if !ok || name == "" {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
deviceURI, ok := req.Params["deviceURI"].(string)
if !ok || deviceURI == "" {
models.RespondError(conn, req.ID, "missing or invalid 'deviceURI' parameter")
return
}
ppd, ok := req.Params["ppd"].(string)
if !ok || ppd == "" {
models.RespondError(conn, req.ID, "missing or invalid 'ppd' parameter")
return
}
shared, _ := req.Params["shared"].(bool)
errorPolicy, _ := req.Params["errorPolicy"].(string)
information, _ := req.Params["information"].(string)
location, _ := req.Params["location"].(string)
if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer created"})
}
func handleDeletePrinter(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.DeletePrinter(printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer deleted"})
}
func handleAcceptJobs(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.AcceptJobs(printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "accepting jobs"})
}
func handleRejectJobs(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.RejectJobs(printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "rejecting jobs"})
}
func handleSetPrinterShared(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
shared, ok := req.Params["shared"].(bool)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'shared' parameter")
return
}
if err := manager.SetPrinterShared(printerName, shared); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sharing updated"})
}
func handleSetPrinterLocation(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
location, ok := req.Params["location"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter")
return
}
if err := manager.SetPrinterLocation(printerName, location); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location updated"})
}
func handleSetPrinterInfo(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
info, ok := req.Params["info"].(string)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'info' parameter")
return
}
if err := manager.SetPrinterInfo(printerName, info); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "info updated"})
}
func handleMoveJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
destPrinter, ok := req.Params["destPrinter"].(string)
if !ok || destPrinter == "" {
models.RespondError(conn, req.ID, "missing or invalid 'destPrinter' parameter")
return
}
if err := manager.MoveJob(int(jobIDFloat), destPrinter); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job moved"})
}
type TestPageResult struct {
Success bool `json:"success"`
JobID int `json:"jobId"`
Message string `json:"message"`
}
func handlePrintTestPage(conn net.Conn, req Request, manager *Manager) {
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
jobID, err := manager.PrintTestPage(printerName)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
}
func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) {
className, ok := req.Params["className"].(string)
if !ok || className == "" {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter")
return
}
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.AddPrinterToClass(className, printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer added to class"})
}
func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager) {
className, ok := req.Params["className"].(string)
if !ok || className == "" {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter")
return
}
printerName, ok := req.Params["printerName"].(string)
if !ok || printerName == "" {
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
return
}
if err := manager.RemovePrinterFromClass(className, printerName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer removed from class"})
}
func handleDeleteClass(conn net.Conn, req Request, manager *Manager) {
className, ok := req.Params["className"].(string)
if !ok || className == "" {
models.RespondError(conn, req.ID, "missing or invalid 'className' parameter")
return
}
if err := manager.DeleteClass(className); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "class deleted"})
}
func handleRestartJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
if err := manager.RestartJob(int(jobIDFloat)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn net.Conn, req Request, manager *Manager) {
jobIDFloat, ok := req.Params["jobID"].(float64)
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter")
return
}
holdUntil, _ := req.Params["holdUntil"].(string)
if holdUntil == "" {
holdUntil = "indefinite"
}
if err := manager.HoldJob(int(jobIDFloat), holdUntil); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job held"})
}

View File

@@ -145,10 +145,9 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
func TestHandlePausePrinter(t *testing.T) { func TestHandlePausePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(nil) mockClient.EXPECT().PausePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient,
}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
@@ -173,10 +172,9 @@ func TestHandlePausePrinter(t *testing.T) {
func TestHandleResumePrinter(t *testing.T) { func TestHandleResumePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(nil) mockClient.EXPECT().ResumePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient,
}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
@@ -201,10 +199,9 @@ func TestHandleResumePrinter(t *testing.T) {
func TestHandleCancelJob(t *testing.T) { func TestHandleCancelJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(nil) mockClient.EXPECT().CancelJob(1, false).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient,
}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
@@ -229,10 +226,9 @@ func TestHandleCancelJob(t *testing.T) {
func TestHandlePurgeJobs(t *testing.T) { func TestHandlePurgeJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil) mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{ m := NewTestManager(mockClient, nil)
client: mockClient,
}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf} conn := &mockConn{Buffer: buf}
@@ -277,3 +273,439 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
assert.Nil(t, resp.Result) assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error) assert.NotNil(t, resp.Error)
} }
func TestHandleGetDevices(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetDevices().Return(map[string]ipp.Attributes{
"usb://HP/LaserJet": {
"device-class": []ipp.Attribute{{Value: "direct"}},
"device-info": []ipp.Attribute{{Value: "HP LaserJet"}},
},
}, nil)
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m)
var resp models.Response[[]Device]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.Equal(t, 1, len(*resp.Result))
}
func TestHandleGetPPDs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetPPDs().Return(map[string]ipp.Attributes{
"generic.ppd": {
"ppd-make-and-model": []ipp.Attribute{{Value: "Generic"}},
},
}, nil)
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m)
var resp models.Response[[]PPD]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.Equal(t, 1, len(*resp.Result))
}
func TestHandleGetClasses(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().GetClasses(mock.Anything).Return(map[string]ipp.Attributes{
"office": {
ipp.AttributePrinterName: []ipp.Attribute{{Value: "office"}},
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
},
}, nil)
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m)
var resp models.Response[[]PrinterClass]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.Equal(t, 1, len(*resp.Result))
}
func TestHandleCreatePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", false, "", "", "").Return(nil)
mockClient.EXPECT().ResumePrinter("newprinter").Return(nil)
mockClient.EXPECT().AcceptJobs("newprinter").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.createPrinter",
Params: map[string]interface{}{
"name": "newprinter",
"deviceURI": "usb://HP",
"ppd": "generic.ppd",
},
}
handleCreatePrinter(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleCreatePrinter_MissingParams(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]interface{}{}}
handleCreatePrinter(conn, req, m)
var resp models.Response[interface{}]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error)
}
func TestHandleDeletePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeletePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.deletePrinter",
Params: map[string]interface{}{"printerName": "printer1"},
}
handleDeletePrinter(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleAcceptJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().AcceptJobs("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.acceptJobs",
Params: map[string]interface{}{"printerName": "printer1"},
}
handleAcceptJobs(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleRejectJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().RejectJobs("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.rejectJobs",
Params: map[string]interface{}{"printerName": "printer1"},
}
handleRejectJobs(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleSetPrinterShared(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.setPrinterShared",
Params: map[string]interface{}{"printerName": "printer1", "shared": true},
}
handleSetPrinterShared(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleSetPrinterLocation(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.setPrinterLocation",
Params: map[string]interface{}{"printerName": "printer1", "location": "Office"},
}
handleSetPrinterLocation(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleSetPrinterInfo(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.setPrinterInfo",
Params: map[string]interface{}{"printerName": "printer1", "info": "Main Printer"},
}
handleSetPrinterInfo(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleMoveJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().MoveJob(1, "printer2").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.moveJob",
Params: map[string]interface{}{"jobID": float64(1), "destPrinter": "printer2"},
}
handleMoveJob(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandlePrintTestPage(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PrintTestPage("printer1", mock.Anything, mock.Anything).Return(42, nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.printTestPage",
Params: map[string]interface{}{"printerName": "printer1"},
}
handlePrintTestPage(conn, req, m)
var resp models.Response[TestPageResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
assert.Equal(t, 42, resp.Result.JobID)
}
func TestHandleAddPrinterToClass(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.addPrinterToClass",
Params: map[string]interface{}{"className": "office", "printerName": "printer1"},
}
handleAddPrinterToClass(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleRemovePrinterFromClass(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.removePrinterFromClass",
Params: map[string]interface{}{"className": "office", "printerName": "printer1"},
}
handleRemovePrinterFromClass(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleDeleteClass(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().DeleteClass("office").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.deleteClass",
Params: map[string]interface{}{"className": "office"},
}
handleDeleteClass(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleRestartJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().RestartJob(1).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.restartJob",
Params: map[string]interface{}{"jobID": float64(1)},
}
handleRestartJob(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleHoldJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.holdJob",
Params: map[string]interface{}{"jobID": float64(1)},
}
handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}
func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().HoldJobUntil(1, "no-hold").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := Request{
ID: 1,
Method: "cups.holdJob",
Params: map[string]interface{}{"jobID": float64(1), "holdUntil": "no-hold"},
}
handleHoldJob(conn, req, m)
var resp models.Response[SuccessResult]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
}

View File

@@ -1,6 +1,7 @@
package cups package cups
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
@@ -31,17 +32,25 @@ func NewManager() (*Manager, error) {
client := ipp.NewCUPSClient(host, port, username, password, false) client := ipp.NewCUPSClient(host, port, username, password, false)
baseURL := fmt.Sprintf("http://%s:%d", host, port) baseURL := fmt.Sprintf("http://%s:%d", host, port)
var pkHelper PkHelper
if isLocalCUPS(host) {
var err error
pkHelper, err = NewPkHelper()
if err != nil {
log.Warnf("[CUPS] Failed to initialize pkhelper: %v", err)
}
}
m := &Manager{ m := &Manager{
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: client, client: client,
pkHelper: pkHelper,
baseURL: baseURL, baseURL: baseURL,
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
subMutex: sync.RWMutex{},
} }
if err := m.updateState(); err != nil { if err := m.updateState(); err != nil {
@@ -100,6 +109,12 @@ func (m *Manager) eventHandler() {
func (m *Manager) updateState() error { func (m *Manager) updateState() error {
printers, err := m.GetPrinters() printers, err := m.GetPrinters()
if err != nil { if err != nil {
if isNoPrintersError(err) {
m.stateMutex.Lock()
m.state.Printers = make(map[string]*Printer)
m.stateMutex.Unlock()
return nil
}
return err return err
} }
@@ -121,6 +136,19 @@ func (m *Manager) updateState() error {
return nil return nil
} }
func isNoPrintersError(err error) bool {
if err == nil {
return false
}
var ippErr ipp.IPPError
if errors.As(err, &ippErr) {
return ippErr.Status == 1030
}
return false
}
func (m *Manager) notifier() { func (m *Manager) notifier() {
defer m.notifierWg.Done() defer m.notifierWg.Done()
const minGap = 100 * time.Millisecond const minGap = 100 * time.Millisecond
@@ -142,28 +170,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -179,6 +200,14 @@ func (m *Manager) notifySubscribers() {
} }
} }
func (m *Manager) RefreshState() {
if err := m.updateState(); err != nil {
log.Warnf("[CUPS] Failed to refresh state: %v", err)
return
}
m.notifySubscribers()
}
func (m *Manager) GetState() CUPSState { func (m *Manager) GetState() CUPSState {
return m.snapshotState() return m.snapshotState()
} }
@@ -199,10 +228,14 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subMutex.Lock()
wasEmpty := len(m.subscribers) == 0 wasEmpty := true
m.subscribers[id] = ch m.subscribers.Range(func(key string, ch chan CUPSState) bool {
m.subMutex.Unlock() wasEmpty = false
return false
})
m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if wasEmpty && m.subscription != nil {
if err := m.subscription.Start(); err != nil { if err := m.subscription.Start(); err != nil {
@@ -217,13 +250,15 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
isEmpty := len(m.subscribers) == 0
m.subMutex.Unlock() isEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
isEmpty = false
return false
})
if isEmpty && m.subscription != nil { if isEmpty && m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
@@ -241,12 +276,11 @@ func (m *Manager) Close() {
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan CUPSState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan CUPSState) return true
m.subMutex.Unlock() })
} }
func stateChanged(old, new *CUPSState) bool { func stateChanged(old, new *CUPSState) bool {
@@ -260,6 +294,7 @@ func stateChanged(old, new *CUPSState) bool {
} }
if oldPrinter.State != newPrinter.State || if oldPrinter.State != newPrinter.State ||
oldPrinter.StateReason != newPrinter.StateReason || oldPrinter.StateReason != newPrinter.StateReason ||
oldPrinter.Accepting != newPrinter.Accepting ||
len(oldPrinter.Jobs) != len(newPrinter.Jobs) { len(oldPrinter.Jobs) != len(newPrinter.Jobs) {
return true return true
} }
@@ -338,3 +373,18 @@ func getBoolAttr(attrs ipp.Attributes, key string) bool {
} }
return false return false
} }
func getStringSliceAttr(attrs ipp.Attributes, key string) []string {
attr, ok := attrs[key]
if !ok {
return nil
}
result := make([]string, 0, len(attr))
for _, a := range attr {
if val, ok := a.Value.(string); ok {
result = append(result, val)
}
}
return result
}

View File

@@ -16,7 +16,6 @@ func TestNewManager(t *testing.T) {
client: nil, client: nil,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -38,7 +37,6 @@ func TestManager_GetState(t *testing.T) {
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
state := m.GetState() state := m.GetState()
@@ -56,15 +54,25 @@ func TestManager_Subscribe(t *testing.T) {
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 1, len(m.subscribers))
count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client") m.Unsubscribe("test-client")
assert.Equal(t, 0, len(m.subscribers)) count = 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
@@ -77,7 +85,6 @@ func TestManager_Close(t *testing.T) {
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
m.eventWG.Add(1) m.eventWG.Add(1)
@@ -93,7 +100,12 @@ func TestManager_Close(t *testing.T) {
}() }()
m.Close() m.Close()
assert.Equal(t, 0, len(m.subscribers)) count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestStateChanged(t *testing.T) { func TestStateChanged(t *testing.T) {

View File

@@ -0,0 +1,184 @@
package cups
import (
"fmt"
"strings"
"github.com/godbus/dbus/v5"
)
const (
pkHelperDest = "org.opensuse.CupsPkHelper.Mechanism"
pkHelperPath = "/"
pkHelperInterface = "org.opensuse.CupsPkHelper.Mechanism"
)
type PkHelper interface {
DevicesGet(timeout, limit int, includeSchemes, excludeSchemes []string) ([]Device, error)
PrinterAdd(name, uri, ppd, info, location string) error
PrinterDelete(name string) error
PrinterSetEnabled(name string, enabled bool) error
PrinterSetAcceptJobs(name string, enabled bool, reason string) error
PrinterSetInfo(name, info string) error
PrinterSetLocation(name, location string) error
PrinterSetShared(name string, shared bool) error
ClassAddPrinter(className, printerName string) error
ClassDeletePrinter(className, printerName string) error
ClassDelete(className string) error
JobCancelPurge(jobID int, purge bool) error
JobRestart(jobID int) error
JobSetHoldUntil(jobID int, holdUntil string) error
}
type DBusPkHelper struct {
conn *dbus.Conn
obj dbus.BusObject
}
func NewPkHelper() (*DBusPkHelper, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
}
return &DBusPkHelper{
conn: conn,
obj: conn.Object(pkHelperDest, pkHelperPath),
}, nil
}
func (p *DBusPkHelper) DevicesGet(timeout, limit int, includeSchemes, excludeSchemes []string) ([]Device, error) {
if includeSchemes == nil {
includeSchemes = []string{}
}
if excludeSchemes == nil {
excludeSchemes = []string{}
}
var errStr string
var devicesMap map[string]string
call := p.obj.Call(pkHelperInterface+".DevicesGet", 0, int32(timeout), int32(limit), includeSchemes, excludeSchemes)
if call.Err != nil {
return nil, call.Err
}
if err := call.Store(&errStr, &devicesMap); err != nil {
return nil, err
}
if errStr != "" {
return nil, fmt.Errorf("%s", errStr)
}
return parseDevicesMap(devicesMap), nil
}
func parseDevicesMap(devicesMap map[string]string) []Device {
devicesByIndex := make(map[string]*Device)
for key, value := range devicesMap {
idx := strings.LastIndex(key, ":")
if idx == -1 {
continue
}
attr := key[:idx]
index := key[idx+1:]
dev, ok := devicesByIndex[index]
if !ok {
dev = &Device{}
devicesByIndex[index] = dev
}
switch attr {
case "device-uri":
dev.URI = value
case "device-class":
dev.Class = value
case "device-info":
dev.Info = value
case "device-make-and-model":
dev.MakeModel = value
case "device-id":
dev.ID = value
case "device-location":
dev.Location = value
}
}
devices := make([]Device, 0, len(devicesByIndex))
for _, dev := range devicesByIndex {
if dev.URI != "" {
devices = append(devices, *dev)
}
}
return devices
}
func (p *DBusPkHelper) PrinterAdd(name, uri, ppd, info, location string) error {
return p.callSimple("PrinterAdd", name, uri, ppd, info, location)
}
func (p *DBusPkHelper) PrinterDelete(name string) error {
return p.callSimple("PrinterDelete", name)
}
func (p *DBusPkHelper) PrinterSetEnabled(name string, enabled bool) error {
return p.callSimple("PrinterSetEnabled", name, enabled)
}
func (p *DBusPkHelper) PrinterSetAcceptJobs(name string, enabled bool, reason string) error {
return p.callSimple("PrinterSetAcceptJobs", name, enabled, reason)
}
func (p *DBusPkHelper) PrinterSetInfo(name, info string) error {
return p.callSimple("PrinterSetInfo", name, info)
}
func (p *DBusPkHelper) PrinterSetLocation(name, location string) error {
return p.callSimple("PrinterSetLocation", name, location)
}
func (p *DBusPkHelper) PrinterSetShared(name string, shared bool) error {
return p.callSimple("PrinterSetShared", name, shared)
}
func (p *DBusPkHelper) ClassAddPrinter(className, printerName string) error {
return p.callSimple("ClassAddPrinter", className, printerName)
}
func (p *DBusPkHelper) ClassDeletePrinter(className, printerName string) error {
return p.callSimple("ClassDeletePrinter", className, printerName)
}
func (p *DBusPkHelper) ClassDelete(className string) error {
return p.callSimple("ClassDelete", className)
}
func (p *DBusPkHelper) JobCancelPurge(jobID int, purge bool) error {
return p.callSimple("JobCancelPurge", int32(jobID), purge)
}
func (p *DBusPkHelper) JobRestart(jobID int) error {
return p.callSimple("JobRestart", int32(jobID))
}
func (p *DBusPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error {
return p.callSimple("JobSetHoldUntil", int32(jobID), holdUntil)
}
func (p *DBusPkHelper) callSimple(method string, args ...interface{}) error {
var errStr string
call := p.obj.Call(pkHelperInterface+"."+method, 0, args...)
if call.Err != nil {
return call.Err
}
if err := call.Store(&errStr); err != nil {
return err
}
if errStr != "" {
return fmt.Errorf("%s", errStr)
}
return nil
}

View File

@@ -0,0 +1,95 @@
package cups
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseDevicesMap(t *testing.T) {
tests := []struct {
name string
input map[string]string
wantLen int
wantURIs []string
}{
{
name: "empty",
input: map[string]string{},
wantLen: 0,
wantURIs: nil,
},
{
name: "single_device",
input: map[string]string{
"device-uri:0": "usb://HP/LaserJet",
"device-class:0": "direct",
"device-info:0": "HP LaserJet",
"device-make-and-model:0": "HP LaserJet 1020",
"device-id:0": "MFG:HP;MDL:LaserJet",
},
wantLen: 1,
wantURIs: []string{"usb://HP/LaserJet"},
},
{
name: "multiple_devices",
input: map[string]string{
"device-uri:0": "usb://HP/LaserJet",
"device-class:0": "direct",
"device-info:0": "HP LaserJet",
"device-uri:1": "socket://192.168.1.100",
"device-class:1": "network",
"device-info:1": "Network Printer",
},
wantLen: 2,
wantURIs: []string{"usb://HP/LaserJet", "socket://192.168.1.100"},
},
{
name: "malformed_key",
input: map[string]string{
"no-colon-here": "value",
},
wantLen: 0,
wantURIs: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseDevicesMap(tt.input)
assert.Len(t, got, tt.wantLen)
if tt.wantURIs != nil {
gotURIs := make(map[string]bool)
for _, d := range got {
gotURIs[d.URI] = true
}
for _, uri := range tt.wantURIs {
assert.True(t, gotURIs[uri], "expected URI %s not found", uri)
}
}
})
}
}
func TestParseDevicesMap_Attributes(t *testing.T) {
input := map[string]string{
"device-uri:0": "usb://HP/LaserJet",
"device-class:0": "direct",
"device-info:0": "HP LaserJet",
"device-make-and-model:0": "HP LaserJet 1020",
"device-id:0": "MFG:HP;MDL:LaserJet",
"device-location:0": "USB Port",
}
got := parseDevicesMap(input)
assert.Len(t, got, 1)
dev := got[0]
assert.Equal(t, "usb://HP/LaserJet", dev.URI)
assert.Equal(t, "direct", dev.Class)
assert.Equal(t, "HP LaserJet", dev.Info)
assert.Equal(t, "HP LaserJet 1020", dev.MakeModel)
assert.Equal(t, "MFG:HP;MDL:LaserJet", dev.ID)
assert.Equal(t, "USB Port", dev.Location)
}

View File

@@ -0,0 +1,13 @@
package cups
func NewTestManager(client CUPSClientInterface, pkHelper PkHelper) *Manager {
return &Manager{
client: client,
pkHelper: pkHelper,
state: &CUPSState{
Printers: make(map[string]*Printer),
},
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
}

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type CUPSState struct { type CUPSState struct {
@@ -34,13 +35,41 @@ type Job struct {
TimeCreated time.Time `json:"timeCreated"` TimeCreated time.Time `json:"timeCreated"`
} }
type Device struct {
URI string `json:"uri"`
Class string `json:"class"`
Info string `json:"info"`
MakeModel string `json:"makeModel"`
ID string `json:"id"`
Location string `json:"location"`
}
type PPD struct {
Name string `json:"name"`
NaturalLanguage string `json:"naturalLanguage"`
MakeModel string `json:"makeModel"`
DeviceID string `json:"deviceId"`
Product string `json:"product"`
PSVersion string `json:"psVersion"`
Type string `json:"type"`
}
type PrinterClass struct {
Name string `json:"name"`
URI string `json:"uri"`
State string `json:"state"`
Members []string `json:"members"`
Location string `json:"location"`
Info string `json:"info"`
}
type Manager struct { type Manager struct {
state *CUPSState state *CUPSState
client CUPSClientInterface client CUPSClientInterface
pkHelper PkHelper
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan CUPSState subscribers syncmap.Map[string, chan CUPSState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
eventWG sync.WaitGroup eventWG sync.WaitGroup
dirty chan struct{} dirty chan struct{}
@@ -63,6 +92,24 @@ type CUPSClientInterface interface {
ResumePrinter(printer string) error ResumePrinter(printer string) error
CancelAllJob(printer string, purge bool) error CancelAllJob(printer string, purge bool) error
SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error) SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error)
GetDevices() (map[string]ipp.Attributes, error)
GetPPDs() (map[string]ipp.Attributes, error)
GetClasses(attributes []string) (map[string]ipp.Attributes, error)
CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error
DeletePrinter(printer string) error
AcceptJobs(printer string) error
RejectJobs(printer string) error
SetPrinterIsShared(printer string, shared bool) error
SetPrinterLocation(printer, location string) error
SetPrinterInformation(printer, information string) error
MoveJob(jobID int, destPrinter string) error
PrintTestPage(printer string, testPageData io.Reader, size int) (int, error)
AddPrinterToClass(class, printer string) error
DeletePrinterFromClass(class, printer string) error
DeleteClass(class string) error
RestartJob(jobID int) error
HoldJobUntil(jobID int, holdUntil string) error
} }
type SubscriptionEvent struct { type SubscriptionEvent struct {

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
@@ -13,11 +13,11 @@ import (
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
outputs: make(map[uint32]*outputState), ctx: display.Context(),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 128),
outputSetupReq: make(chan uint32, 16), outputSetupReq: make(chan uint32, 16),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
layouts: make([]string, 0), layouts: make([]string, 0),
} }
@@ -55,10 +55,7 @@ func (m *Manager) waylandActor() {
case c := <-m.cmdq: case c := <-m.cmdq:
c.fn() c.fn()
case outputID := <-m.outputSetupReq: case outputID := <-m.outputSetupReq:
m.outputsMutex.RLock() out, exists := m.outputs.Load(outputID)
out, exists := m.outputs[outputID]
m.outputsMutex.RUnlock()
if !exists { if !exists {
log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID) log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
continue continue
@@ -86,7 +83,6 @@ func (m *Manager) waylandActor() {
func (m *Manager) setupRegistry() error { func (m *Manager) setupRegistry() error {
log.Info("DWL: starting registry setup") log.Info("DWL: starting registry setup")
ctx := m.display.Context()
registry, err := m.display.GetRegistry() registry, err := m.display.GetRegistry()
if err != nil { if err != nil {
@@ -102,10 +98,10 @@ func (m *Manager) setupRegistry() error {
switch e.Interface { switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName: case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName) log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName)
manager := dwl_ipc.NewZdwlIpcManagerV2(ctx) manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx)
version := e.Version version := e.Version
if version > 1 { if version > 2 {
version = 1 version = 2
} }
if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
dwlMgr = manager dwlMgr = manager
@@ -128,7 +124,7 @@ func (m *Manager) setupRegistry() error {
} }
case "wl_output": case "wl_output":
log.Debugf("DWL: found wl_output (name=%d)", e.Name) log.Debugf("DWL: found wl_output (name=%d)", e.Name)
output := wlclient.NewOutput(ctx) output := wlclient.NewOutput(m.ctx)
outState := &outputState{ outState := &outputState{
registryName: e.Name, registryName: e.Name,
@@ -156,9 +152,7 @@ func (m *Manager) setupRegistry() error {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name outputRegNames[outputID] = e.Name
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[outputID] = outState
m.outputsMutex.Unlock()
if m.manager != nil { if m.manager != nil {
select { select {
@@ -176,17 +170,16 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock()
var outToRelease *outputState var outToRelease *outputState
for id, out := range m.outputs { m.outputs.Range(func(id uint32, out *outputState) bool {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("DWL: Output %d removed", id) log.Infof("DWL: Output %d removed", id)
outToRelease = out outToRelease = out
delete(m.outputs, id) m.outputs.Delete(id)
break return false
} }
} return true
m.outputsMutex.Unlock() })
if outToRelease != nil { if outToRelease != nil {
if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil { if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
@@ -236,14 +229,11 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
return fmt.Errorf("failed to get dwl output: %w", err) return fmt.Errorf("failed to get dwl output: %w", err)
} }
m.outputsMutex.Lock() outState, exists := m.outputs.Load(output.ID())
outState, exists := m.outputs[output.ID()]
if !exists { if !exists {
m.outputsMutex.Unlock()
return fmt.Errorf("output state not found for id %d", output.ID()) return fmt.Errorf("output state not found for id %d", output.ID())
} }
outState.ipcOutput = ipcOutput outState.ipcOutput = ipcOutput
m.outputsMutex.Unlock()
ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
outState.active = e.Active outState.active = e.Active
@@ -292,6 +282,14 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
outState.layoutSymbol = e.Layout outState.layoutSymbol = e.Layout
}) })
ipcOutput.SetKbLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2KbLayoutEvent) {
outState.kbLayout = e.KbLayout
})
ipcOutput.SetKeymodeHandler(func(e dwl_ipc.ZdwlIpcOutputV2KeymodeEvent) {
outState.keymode = e.Keymode
})
ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) { ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
m.updateState() m.updateState()
}) })
@@ -300,11 +298,10 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.outputsMutex.RLock()
outputs := make(map[string]*OutputState) outputs := make(map[string]*OutputState)
activeOutput := "" activeOutput := ""
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -321,13 +318,15 @@ func (m *Manager) updateState() {
LayoutSymbol: out.layoutSymbol, LayoutSymbol: out.layoutSymbol,
Title: out.title, Title: out.title,
AppID: out.appID, AppID: out.appID,
KbLayout: out.kbLayout,
Keymode: out.keymode,
} }
if out.active != 0 { if out.active != 0 {
activeOutput = name activeOutput = name
} }
} return true
m.outputsMutex.RUnlock() })
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -365,14 +364,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -381,15 +372,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("DWL: subscriber channel full, dropping update") log.Warn("DWL: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -407,11 +397,9 @@ func (m *Manager) ensureOutputSetup(out *outputState) error {
} }
func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error { func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
m.outputsMutex.RLock() availableOutputs := make([]string, 0)
availableOutputs := make([]string, 0, len(m.outputs))
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -419,10 +407,10 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
availableOutputs = append(availableOutputs, name) availableOutputs = append(availableOutputs, name)
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs) return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
@@ -444,20 +432,18 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
} }
func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error { func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -479,20 +465,18 @@ func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint3
} }
func (m *Manager) SetLayout(outputName string, index uint32) error { func (m *Manager) SetLayout(outputName string, index uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -518,21 +502,19 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok { if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
ipcOut.Release() ipcOut.Release()
} }
} m.outputs.Delete(key)
m.outputs = make(map[uint32]*outputState) return true
m.outputsMutex.Unlock() })
if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok { if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
mgr.Release() mgr.Release()

View File

@@ -3,7 +3,8 @@ package dwl
import ( import (
"sync" "sync"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type TagState struct { type TagState struct {
@@ -21,6 +22,8 @@ type OutputState struct {
LayoutSymbol string `json:"layoutSymbol"` LayoutSymbol string `json:"layoutSymbol"`
Title string `json:"title"` Title string `json:"title"`
AppID string `json:"appId"` AppID string `json:"appId"`
KbLayout string `json:"kbLayout"`
Keymode string `json:"keymode"`
} }
type State struct { type State struct {
@@ -36,11 +39,11 @@ type cmd struct {
type Manager struct { type Manager struct {
display *wlclient.Display display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry registry *wlclient.Registry
manager interface{} manager interface{}
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
tagCount uint32 tagCount uint32
layouts []string layouts []string
@@ -51,8 +54,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -73,6 +75,8 @@ type outputState struct {
layoutSymbol string layoutSymbol string
title string title string
appID string appID string
kbLayout string
keymode string
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
@@ -91,19 +95,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {
@@ -150,6 +151,12 @@ func stateChanged(old, new *State) bool {
if oldOut.AppID != newOut.AppID { if oldOut.AppID != newOut.AppID {
return true return true
} }
if oldOut.KbLayout != newOut.KbLayout {
return true
}
if oldOut.Keymode != newOut.Keymode {
return true
}
if len(oldOut.Tags) != len(newOut.Tags) { if len(oldOut.Tags) != len(newOut.Tags) {
return true return true
} }

View File

@@ -0,0 +1,27 @@
package evdev
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type Request struct {
ID interface{} `json:"id"`
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, m *Manager) {
switch req.Method {
case "evdev.getState":
handleGetState(conn, req, m)
default:
models.RespondError(conn, req.ID.(int), "unknown method: "+req.Method)
}
}
func handleGetState(conn net.Conn, req Request, m *Manager) {
state := m.GetState()
models.Respond(conn, req.ID.(int), state)
}

View File

@@ -0,0 +1,130 @@
package evdev
import (
"bytes"
"encoding/json"
"errors"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type mockNetConn struct {
net.Conn
readBuf *bytes.Buffer
writeBuf *bytes.Buffer
closed bool
}
func newMockNetConn() *mockNetConn {
return &mockNetConn{
readBuf: &bytes.Buffer{},
writeBuf: &bytes.Buffer{},
}
}
func (m *mockNetConn) Read(b []byte) (n int, err error) {
return m.readBuf.Read(b)
}
func (m *mockNetConn) Write(b []byte) (n int, err error) {
return m.writeBuf.Write(b)
}
func (m *mockNetConn) Close() error {
m.closed = true
return nil
}
func TestHandleRequest(t *testing.T) {
t.Run("getState request", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true},
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
req := Request{
ID: 123,
Method: "evdev.getState",
Params: map[string]interface{}{},
}
HandleRequest(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Available)
assert.True(t, resp.Result.CapsLock)
})
t.Run("unknown method", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
req := Request{
ID: 456,
Method: "evdev.unknownMethod",
Params: map[string]interface{}{},
}
HandleRequest(conn, req, m)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 456, resp.ID)
assert.NotEmpty(t, resp.Error)
assert.Contains(t, resp.Error, "unknown method")
})
}
func TestHandleGetState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
req := Request{
ID: 789,
Method: "evdev.getState",
Params: map[string]interface{}{},
}
handleGetState(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 789, resp.ID)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Available)
assert.False(t, resp.Result.CapsLock)
}

View File

@@ -0,0 +1,404 @@
package evdev
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev"
)
const (
evKeyType = 0x01
evLedType = 0x11
keyCapslockKey = 58
ledCapslockKey = 1
keyStateOn = 1
)
type EvdevDevice interface {
Name() (string, error)
Path() string
Close() error
ReadOne() (*evdev.InputEvent, error)
State(t evdev.EvType) (evdev.StateMap, error)
}
type Manager struct {
devices []EvdevDevice
devicesMutex sync.RWMutex
monitoredPaths map[string]bool
state State
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
closeChan chan struct{}
closeOnce sync.Once
watcher *fsnotify.Watcher
}
func NewManager() (*Manager, error) {
devices, err := findKeyboards()
if err != nil {
return nil, fmt.Errorf("failed to find keyboards: %w", err)
}
initialCapsLock := readInitialCapsLockState(devices[0])
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Warnf("Failed to create fsnotify watcher, hotplug detection disabled: %v", err)
watcher = nil
} else if err := watcher.Add("/dev/input"); err != nil {
log.Warnf("Failed to watch /dev/input, hotplug detection disabled: %v", err)
watcher.Close()
watcher = nil
}
monitoredPaths := make(map[string]bool)
for _, device := range devices {
monitoredPaths[device.Path()] = true
}
m := &Manager{
devices: devices,
monitoredPaths: monitoredPaths,
state: State{Available: true, CapsLock: initialCapsLock},
closeChan: make(chan struct{}),
watcher: watcher,
}
for i, device := range devices {
go m.monitorDevice(device, i)
}
if watcher != nil {
go m.watchForNewKeyboards()
}
return m, nil
}
func readInitialCapsLockState(device EvdevDevice) bool {
ledStates, err := device.State(evLedType)
if err != nil {
log.Debugf("Could not read LED state: %v", err)
return false
}
return ledStates[ledCapslockKey]
}
func findKeyboards() ([]EvdevDevice, error) {
pattern := "/dev/input/event*"
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("failed to glob input devices: %w", err)
}
if len(matches) == 0 {
return nil, fmt.Errorf("no input devices found")
}
var keyboards []EvdevDevice
for _, path := range matches {
device, err := evdev.Open(path)
if err != nil {
continue
}
if !isKeyboard(device) {
device.Close()
continue
}
deviceName, _ := device.Name()
log.Debugf("Found keyboard: %s at %s", deviceName, path)
keyboards = append(keyboards, device)
}
if len(keyboards) == 0 {
return nil, fmt.Errorf("no keyboard device found")
}
return keyboards, nil
}
func isKeyboard(device EvdevDevice) bool {
deviceName, err := device.Name()
if err != nil {
return false
}
name := strings.ToLower(deviceName)
switch {
case strings.Contains(name, "keyboard"):
return true
case strings.Contains(name, "kbd"):
return true
case strings.Contains(name, "input") && strings.Contains(name, "key"):
return true
}
keyStates, err := device.State(evKeyType)
if err != nil {
return false
}
hasKeyA := len(keyStates) > 30
hasKeyZ := len(keyStates) > 44
hasEnter := len(keyStates) > 28
return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100
}
func (m *Manager) watchForNewKeyboards() {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic in keyboard hotplug monitor: %v", r)
}
}()
for {
select {
case <-m.closeChan:
return
case event, ok := <-m.watcher.Events:
if !ok {
return
}
if !strings.HasPrefix(filepath.Base(event.Name), "event") {
continue
}
if event.Op&fsnotify.Create == fsnotify.Create {
time.Sleep(100 * time.Millisecond)
m.devicesMutex.Lock()
if m.monitoredPaths[event.Name] {
m.devicesMutex.Unlock()
continue
}
device, err := evdev.Open(event.Name)
if err != nil {
m.devicesMutex.Unlock()
continue
}
if !isKeyboard(device) {
device.Close()
m.devicesMutex.Unlock()
continue
}
deviceName, _ := device.Name()
log.Debugf("Hotplugged keyboard: %s at %s", deviceName, event.Name)
m.devices = append(m.devices, device)
m.monitoredPaths[event.Name] = true
deviceIndex := len(m.devices) - 1
m.devicesMutex.Unlock()
go m.monitorDevice(device, deviceIndex)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
m.devicesMutex.Lock()
if !m.monitoredPaths[event.Name] {
m.devicesMutex.Unlock()
continue
}
delete(m.monitoredPaths, event.Name)
for i, device := range m.devices {
if device != nil && device.Path() == event.Name {
log.Debugf("Keyboard removed: %s", event.Name)
device.Close()
m.devices[i] = nil
break
}
}
m.devicesMutex.Unlock()
}
case err, ok := <-m.watcher.Errors:
if !ok {
return
}
log.Warnf("Keyboard hotplug watcher error: %v", err)
}
}
}
func (m *Manager) monitorDevice(device EvdevDevice, deviceIndex int) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic in evdev monitor: %v", r)
}
}()
for {
select {
case <-m.closeChan:
return
default:
}
event, err := device.ReadOne()
if err != nil {
if isClosedError(err) {
return
}
log.Warnf("Failed to read evdev event: %v", err)
time.Sleep(100 * time.Millisecond)
continue
}
if event == nil {
continue
}
if event.Type == evKeyType && event.Code == keyCapslockKey && event.Value == keyStateOn {
time.Sleep(50 * time.Millisecond)
m.readAndUpdateCapsLockState(deviceIndex)
} else if event.Type == evLedType && event.Code == ledCapslockKey {
capsLockState := event.Value == keyStateOn
m.updateCapsLockStateDirect(capsLockState)
}
}
}
func isClosedError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
switch {
case strings.Contains(errStr, "closed"):
return true
case strings.Contains(errStr, "bad file descriptor"):
return true
default:
return false
}
}
func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
m.devicesMutex.RLock()
if deviceIndex >= len(m.devices) {
m.devicesMutex.RUnlock()
return
}
device := m.devices[deviceIndex]
m.devicesMutex.RUnlock()
ledStates, err := device.State(evLedType)
if err != nil {
log.Warnf("Failed to read LED state: %v", err)
return
}
capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState)
}
func (m *Manager) updateCapsLockStateDirect(capsLockState bool) {
m.stateMutex.Lock()
if m.state.CapsLock == capsLockState {
m.stateMutex.Unlock()
return
}
m.state.CapsLock = capsLockState
newState := m.state
m.stateMutex.Unlock()
log.Debugf("Caps lock state: %v", newState.CapsLock)
m.notifySubscribers(newState)
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
return m.state
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) notifySubscribers(state State) {
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- state:
default:
}
return true
})
}
func (m *Manager) Close() {
m.closeOnce.Do(func() {
close(m.closeChan)
if m.watcher != nil {
m.watcher.Close()
}
m.devicesMutex.Lock()
for _, device := range m.devices {
if device == nil {
continue
}
if err := device.Close(); err != nil && !isClosedError(err) {
log.Warnf("Error closing evdev device: %v", err)
}
}
m.devicesMutex.Unlock()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
})
}
func InitializeManager() (*Manager, error) {
if os.Getuid() != 0 && !hasInputGroupAccess() {
return nil, fmt.Errorf("insufficient permissions to access input devices")
}
return NewManager()
}
func hasInputGroupAccess() bool {
pattern := "/dev/input/event*"
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
return false
}
testFile, err := os.Open(matches[0])
if err != nil {
return false
}
testFile.Close()
return true
}

View File

@@ -0,0 +1,344 @@
package evdev
import (
"errors"
"testing"
evdev "github.com/holoplot/go-evdev"
"github.com/stretchr/testify/assert"
mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev"
)
func TestManager_Creation(t *testing.T) {
t.Run("manager created successfully with caps lock off", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
assert.NotNil(t, m)
assert.True(t, m.state.Available)
assert.False(t, m.state.CapsLock)
})
t.Run("manager created successfully with caps lock on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true},
closeChan: make(chan struct{}),
}
assert.NotNil(t, m)
assert.True(t, m.state.Available)
assert.True(t, m.state.CapsLock)
})
}
func TestManager_GetState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
state := m.GetState()
assert.True(t, state.Available)
assert.False(t, state.CapsLock)
}
func TestManager_Subscribe(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
assert.NotNil(t, ch)
count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
}
func TestManager_Unsubscribe(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client")
count = 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
select {
case _, ok := <-ch:
assert.False(t, ok, "channel should be closed")
default:
t.Error("channel should be closed")
}
}
func TestManager_UpdateCapsLock(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
ledStateOn := evdev.StateMap{ledCapslockKey: true}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOn, nil).Once()
go func() {
m.readAndUpdateCapsLockState(0)
}()
newState := <-ch
assert.True(t, newState.CapsLock)
ledStateOff := evdev.StateMap{ledCapslockKey: false}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOff, nil).Once()
go func() {
m.readAndUpdateCapsLockState(0)
}()
newState = <-ch
assert.False(t, newState.CapsLock)
}
func TestManager_Close(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Close().Return(nil).Once()
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch1 := m.Subscribe("client1")
ch2 := m.Subscribe("client2")
m.Close()
select {
case _, ok := <-ch1:
assert.False(t, ok, "channel 1 should be closed")
default:
t.Error("channel 1 should be closed")
}
select {
case _, ok := <-ch2:
assert.False(t, ok, "channel 2 should be closed")
default:
t.Error("channel 2 should be closed")
}
count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
m.Close()
}
func TestIsKeyboard(t *testing.T) {
tests := []struct {
name string
devName string
expected bool
}{
{"keyboard in name", "AT Translated Set 2 keyboard", true},
{"kbd in name", "USB kbd", true},
{"input and key", "input key device", true},
{"random device", "Mouse", false},
{"empty name", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Name().Return(tt.devName, nil).Once()
if !tt.expected {
mockDevice.EXPECT().State(evdev.EvType(evKeyType)).Return(evdev.StateMap{}, nil).Maybe()
}
result := isKeyboard(mockDevice)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsKeyboard_ErrorHandling(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Name().Return("", errors.New("device error")).Once()
result := isKeyboard(mockDevice)
assert.False(t, result)
}
func TestManager_MonitorDevice(t *testing.T) {
t.Run("caps lock key press updates state", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
capsLockEvent := &evdev.InputEvent{
Type: evKeyType,
Code: keyCapslockKey,
Value: keyStateOn,
}
ledState := evdev.StateMap{ledCapslockKey: true}
mockDevice.EXPECT().ReadOne().Return(capsLockEvent, nil).Once()
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("stop")).Maybe()
mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test")
go m.monitorDevice(mockDevice, 0)
state := <-ch
assert.True(t, state.CapsLock)
m.Close()
})
}
func TestIsClosedError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{"nil error", nil, false},
{"closed error", errors.New("device closed"), true},
{"bad file descriptor", errors.New("bad file descriptor"), true},
{"other error", errors.New("some other error"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isClosedError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestNotifySubscribers(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch1 := m.Subscribe("client1")
ch2 := m.Subscribe("client2")
newState := State{Available: true, CapsLock: true}
go m.notifySubscribers(newState)
state1 := <-ch1
state2 := <-ch2
assert.Equal(t, newState, state1)
assert.Equal(t, newState, state2)
m.Close()
}
func TestReadInitialCapsLockState(t *testing.T) {
t.Run("caps lock is on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: true,
}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice)
assert.True(t, result)
})
t.Run("caps lock is off", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: false,
}
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice)
assert.False(t, result)
})
t.Run("error reading LED state", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
result := readInitialCapsLockState(mockDevice)
assert.False(t, result)
})
}
func TestHasInputGroupAccess(t *testing.T) {
result := hasInputGroupAccess()
t.Logf("hasInputGroupAccess: %v", result)
}

View File

@@ -0,0 +1,6 @@
package evdev
type State struct {
Available bool `json:"available"`
CapsLock bool `json:"capsLock"`
}

View File

@@ -6,19 +6,45 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func CheckCapability() bool {
display, err := wlclient.Connect("")
if err != nil {
return false
}
defer display.Destroy()
registry, err := display.GetRegistry()
if err != nil {
return false
}
defer registry.Destroy()
found := false
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
found = true
}
})
// Roundtrip to ensure all registry events are processed
if err := display.Roundtrip(); err != nil {
return false
}
return found
}
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
outputs: make(map[uint32]*wlclient.Output), ctx: display.Context(),
outputNames: make(map[uint32]string),
groups: make(map[uint32]*workspaceGroupState),
workspaces: make(map[uint32]*workspaceState),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
} }
@@ -62,7 +88,6 @@ func (m *Manager) waylandActor() {
func (m *Manager) setupRegistry() error { func (m *Manager) setupRegistry() error {
log.Info("ExtWorkspace: starting registry setup") log.Info("ExtWorkspace: starting registry setup")
ctx := m.display.Context()
registry, err := m.display.GetRegistry() registry, err := m.display.GetRegistry()
if err != nil { if err != nil {
@@ -72,15 +97,16 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == "wl_output" { if e.Interface == "wl_output" {
output := wlclient.NewOutput(ctx) output := wlclient.NewOutput(m.ctx)
if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil { if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil {
outputID := output.ID() outputID := output.ID()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
m.outputsMutex.Lock() m.outputNames.Store(outputID, ev.Name)
m.outputNames[outputID] = ev.Name
m.outputsMutex.Unlock()
log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name) log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name)
m.post(func() {
m.updateState()
})
}) })
} }
return return
@@ -88,7 +114,7 @@ func (m *Manager) setupRegistry() error {
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName { if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName) log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName)
manager := ext_workspace.NewExtWorkspaceManagerV1(ctx) manager := ext_workspace.NewExtWorkspaceManagerV1(m.ctx)
version := e.Version version := e.Version
if version > 1 { if version > 1 {
version = 1 version = 1
@@ -139,9 +165,7 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceIDs: make([]uint32, 0), workspaceIDs: make([]uint32, 0),
} }
m.groupsMutex.Lock() m.groups.Store(groupID, group)
m.groups[groupID] = group
m.groupsMutex.Unlock()
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) { handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) {
log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities) log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities)
@@ -151,9 +175,8 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
outputID := e.Output.ID() outputID := e.Output.ID()
log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID) log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID)
group.outputIDs[outputID] = true
m.post(func() { m.post(func() {
group.outputIDs[outputID] = true
m.updateState() m.updateState()
}) })
}) })
@@ -161,8 +184,8 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) { handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) {
outputID := e.Output.ID() outputID := e.Output.ID()
log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID) log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID)
delete(group.outputIDs, outputID)
m.post(func() { m.post(func() {
delete(group.outputIDs, outputID)
m.updateState() m.updateState()
}) })
}) })
@@ -171,14 +194,12 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceID := e.Workspace.ID() workspaceID := e.Workspace.ID()
log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID)
m.workspacesMutex.Lock() m.post(func() {
if ws, exists := m.workspaces[workspaceID]; exists { if ws, ok := m.workspaces.Load(workspaceID); ok {
ws.groupID = groupID ws.groupID = groupID
} }
m.workspacesMutex.Unlock()
group.workspaceIDs = append(group.workspaceIDs, workspaceID) group.workspaceIDs = append(group.workspaceIDs, workspaceID)
m.post(func() {
m.updateState() m.updateState()
}) })
}) })
@@ -187,11 +208,10 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceID := e.Workspace.ID() workspaceID := e.Workspace.ID()
log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID)
m.workspacesMutex.Lock() m.post(func() {
if ws, exists := m.workspaces[workspaceID]; exists { if ws, ok := m.workspaces.Load(workspaceID); ok {
ws.groupID = 0 ws.groupID = 0
} }
m.workspacesMutex.Unlock()
for i, id := range group.workspaceIDs { for i, id := range group.workspaceIDs {
if id == workspaceID { if id == workspaceID {
@@ -199,20 +219,18 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
break break
} }
} }
m.post(func() {
m.updateState() m.updateState()
}) })
}) })
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) { handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) {
log.Debugf("ExtWorkspace: Group %d removed", groupID) log.Debugf("ExtWorkspace: Group %d removed", groupID)
group.removed = true
m.groupsMutex.Lock()
delete(m.groups, groupID)
m.groupsMutex.Unlock()
m.post(func() { m.post(func() {
group.removed = true
m.groups.Delete(groupID)
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
m.wlMutex.Unlock() m.wlMutex.Unlock()
@@ -234,22 +252,20 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
coordinates: make([]uint32, 0), coordinates: make([]uint32, 0),
} }
m.workspacesMutex.Lock() m.workspaces.Store(workspaceID, ws)
m.workspaces[workspaceID] = ws
m.workspacesMutex.Unlock()
handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) { handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) {
log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id) log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id)
ws.workspaceID = e.Id
m.post(func() { m.post(func() {
ws.workspaceID = e.Id
m.updateState() m.updateState()
}) })
}) })
handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) { handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) {
log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name) log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name)
ws.name = e.Name
m.post(func() { m.post(func() {
ws.name = e.Name
m.updateState() m.updateState()
}) })
}) })
@@ -266,16 +282,16 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
} }
} }
log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords) log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords)
ws.coordinates = coords
m.post(func() { m.post(func() {
ws.coordinates = coords
m.updateState() m.updateState()
}) })
}) })
handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) { handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) {
log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State) log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State)
ws.state = e.State
m.post(func() { m.post(func() {
ws.state = e.State
m.updateState() m.updateState()
}) })
}) })
@@ -286,13 +302,12 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) { handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) {
log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID) log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID)
ws.removed = true
m.workspacesMutex.Lock()
delete(m.workspaces, workspaceID)
m.workspacesMutex.Unlock()
m.post(func() { m.post(func() {
ws.removed = true
m.workspaces.Delete(workspaceID)
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
m.wlMutex.Unlock() m.wlMutex.Unlock()
@@ -303,32 +318,27 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.groupsMutex.RLock()
m.workspacesMutex.RLock()
groups := make([]*WorkspaceGroup, 0) groups := make([]*WorkspaceGroup, 0)
for _, group := range m.groups { m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if group.removed { if group.removed {
continue return true
} }
outputs := make([]string, 0) outputs := make([]string, 0)
for outputID := range group.outputIDs { for outputID := range group.outputIDs {
m.outputsMutex.RLock() if name, ok := m.outputNames.Load(outputID); ok && name != "" {
name := m.outputNames[outputID]
m.outputsMutex.RUnlock()
if name != "" {
outputs = append(outputs, name) outputs = append(outputs, name)
} else {
outputs = append(outputs, fmt.Sprintf("output-%d", outputID))
} }
} }
workspaces := make([]*Workspace, 0) workspaces := make([]*Workspace, 0)
for _, wsID := range group.workspaceIDs { for _, wsID := range group.workspaceIDs {
ws, exists := m.workspaces[wsID] ws, exists := m.workspaces.Load(wsID)
if !exists || ws.removed { if !exists {
continue
}
if ws.removed {
continue continue
} }
@@ -350,10 +360,8 @@ func (m *Manager) updateState() {
Workspaces: workspaces, Workspaces: workspaces,
} }
groups = append(groups, groupState) groups = append(groups, groupState)
} return true
})
m.workspacesMutex.RUnlock()
m.groupsMutex.RUnlock()
newState := State{ newState := State{
Groups: groups, Groups: groups,
@@ -388,14 +396,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -404,15 +404,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("ExtWorkspace: subscriber channel full, dropping update") log.Warn("ExtWorkspace: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -422,9 +421,9 @@ func (m *Manager) notifier() {
} }
func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error { func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
m.workspacesMutex.RLock() errChan := make(chan error, 1)
defer m.workspacesMutex.RUnlock()
m.post(func() {
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -433,9 +432,10 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -444,17 +444,25 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
err = m.manager.Commit() err = m.manager.Commit()
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
return err errChan <- err
} found = true
return false
} }
return true
})
return fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
} }
func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error { func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
m.workspacesMutex.RLock() errChan := make(chan error, 1)
defer m.workspacesMutex.RUnlock()
m.post(func() {
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -463,9 +471,10 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -474,17 +483,25 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
err = m.manager.Commit() err = m.manager.Commit()
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
return err errChan <- err
} found = true
return false
} }
return true
})
return fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
} }
func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error { func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
m.workspacesMutex.RLock() errChan := make(chan error, 1)
defer m.workspacesMutex.RUnlock()
m.post(func() {
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -493,9 +510,10 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -504,18 +522,27 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
err = m.manager.Commit() err = m.manager.Commit()
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
return err errChan <- err
} found = true
return false
} }
return true
})
return fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
} }
func (m *Manager) CreateWorkspace(groupID, workspaceName string) error { func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
m.groupsMutex.RLock() errChan := make(chan error, 1)
defer m.groupsMutex.RUnlock()
for _, group := range m.groups { m.post(func() {
var found bool
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if fmt.Sprintf("group-%d", group.id) == groupID { if fmt.Sprintf("group-%d", group.id) == groupID {
m.wlMutex.Lock() m.wlMutex.Lock()
err := group.handle.CreateWorkspace(workspaceName) err := group.handle.CreateWorkspace(workspaceName)
@@ -523,11 +550,19 @@ func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
err = m.manager.Commit() err = m.manager.Commit()
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
return err errChan <- err
} found = true
return false
} }
return true
})
return fmt.Errorf("workspace group not found: %s", groupID) if !found {
errChan <- fmt.Errorf("workspace group not found: %s", groupID)
}
})
return <-errChan
} }
func (m *Manager) Close() { func (m *Manager) Close() {
@@ -535,30 +570,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.workspacesMutex.Lock() m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
for _, ws := range m.workspaces {
if ws.handle != nil { if ws.handle != nil {
ws.handle.Destroy() ws.handle.Destroy()
} }
} m.workspaces.Delete(key)
m.workspaces = make(map[uint32]*workspaceState) return true
m.workspacesMutex.Unlock() })
m.groupsMutex.Lock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
for _, group := range m.groups {
if group.handle != nil { if group.handle != nil {
group.handle.Destroy() group.handle.Destroy()
} }
} m.groups.Delete(key)
m.groups = make(map[uint32]*workspaceGroupState) return true
m.groupsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()

View File

@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type Workspace struct { type Workspace struct {
@@ -33,26 +34,22 @@ type cmd struct {
type Manager struct { type Manager struct {
display *wlclient.Display display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry registry *wlclient.Registry
manager *ext_workspace.ExtWorkspaceManagerV1 manager *ext_workspace.ExtWorkspaceManagerV1
outputsMutex sync.RWMutex outputNames syncmap.Map[uint32, string]
outputs map[uint32]*wlclient.Output
outputNames map[uint32]string
groupsMutex sync.RWMutex groups syncmap.Map[uint32, *workspaceGroupState]
groups map[uint32]*workspaceGroupState
workspacesMutex sync.RWMutex workspaces syncmap.Map[uint32, *workspaceState]
workspaces map[uint32]*workspaceState
wlMutex sync.Mutex wlMutex sync.Mutex
cmdq chan cmd cmdq chan cmd
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -94,19 +91,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -29,8 +29,6 @@ func NewManager() (*Manager, error) {
systemConn: systemConn, systemConn: systemConn,
sessionConn: sessionConn, sessionConn: sessionConn,
currentUID: uint64(os.Getuid()), currentUID: uint64(os.Getuid()),
subscribers: make(map[string]chan FreedeskState),
subMutex: sync.RWMutex{},
} }
m.initializeAccounts() m.initializeAccounts()
@@ -206,41 +204,33 @@ func (m *Manager) GetState() FreedeskState {
func (m *Manager) Subscribe(id string) chan FreedeskState { func (m *Manager) Subscribe(id string) chan FreedeskState {
ch := make(chan FreedeskState, 64) ch := make(chan FreedeskState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
m.subMutex.RLock()
defer m.subMutex.RUnlock()
state := m.GetState() state := m.GetState()
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) Close() { func (m *Manager) Close() {
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
for id, ch := range m.subscribers {
close(ch) close(ch)
delete(m.subscribers, id) m.subscribers.Delete(key)
} return true
m.subMutex.Unlock() })
if m.systemConn != nil { if m.systemConn != nil {
m.systemConn.Close() m.systemConn.Close()

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