1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

Compare commits

...

259 Commits

Author SHA1 Message Date
bbedward
4e66d3532e appdrawer: fix context menu
fixes #859
2025-11-30 23:02:00 -05:00
Jon Rogers
1b6d567451 feat: Add browser picker modal for URL handling (#815)
* feat: add browser picker for opening URLs

- Introduce a QML modal allowing users to select a web browser to open a given URL.
- Add a CLI command `dms open <url>` that sends a `browser.open` request to the DMS server.
- Implement server‑side Browser manager, request handling, and subscription handling to propagate open events to clients.
- Extend router and server initialization to register the new “browser” capability and include it in advertised capabilities.
- Expose `openUrlRequested` signal in DMSService.qml and connect it to the modal for seamless UI activation.
- Add a desktop entry for the Browser Picker and update the active subscriptions list to include the browser service.

* fix(browser-picker): resolve QML errors in BrowserPickerModal and DMSShell

* fix(browser-picker): fix socket discovery in dms open command

* feat: add keyboard navigation and dynamic model to browser picker

- Replace the static browsers array with a ListModel built from AppSearchService, ensuring robust iteration and future‑proofing of the browser list.
- Introduce keyboard navigation (arrow keys and Enter) using selectedIndex and gridColumns, allowing users to select a browser without a mouse.
- Reset URL, selected index, and navigation flag when the modal closes to avoid stale state.
- Redesign the grid layout to compute cell width from columns, improve focus handling, and use AppLauncherGridDelegate for a consistent UI.
- Enhance delegate behavior to update selection on hover and reset keyboard navigation state appropriately.

* feat: add searchable list/grid view to browser picker

- Introduce view mode setting (list or grid) saved in SettingsData for persistent user preference
- Add search field with real‑time filtering to quickly locate a browser by name
- Sort browsers by usage frequency from AppUsageHistoryData, falling back to alphabetical order
- Provide UI toggle buttons to switch between list and grid layouts, updating the stored setting
- Adjust keyboard navigation logic to support both layouts and improve focus handling
- Refine modal dimensions and header layout for better visual consistency
- Record launched browser usage to keep usage rankings up‑to‑date.

* feat(browser-picker): improve UX with search, view persistence, and usage tracking

Enhance BrowserPickerModal to match AppLauncher design and functionality:

UI/UX Improvements:
- Add search bar with DankTextField for filtering browsers
- Move view mode switcher (list/grid) to header next to title
- Persist view mode preference to SettingsData.browserPickerViewMode
- Match AppLauncher dimensions (520x500)
- Add proper spacing between list items
- Improve URL display with truncation (single line, elide middle)
- Remove redundant close button

Functionality:
- Implement separate browser usage tracking in SettingsData.browserUsageHistory
- Sort browsers by most recently used (independent from app launcher stats)
- Add keyboard navigation auto-scrolling for list and grid views
- Track usage count, last used timestamp, and browser name
- Filter browsers by search query

Technical:
- Add ensureVisible() functions to DankListView and DankGridView
- Store browser usage with count, lastUsed, and name fields
- Update browser list reactively on search query changes

* feat(browser-picker): use appLauncherGridColumns setting for grid layout

Make browser picker grid view respect the same column setting as the app launcher
for consistent UI across both components.

* refactor: make browser picker extensible for any MIME type/category

Refactor browser picker into a generic, reusable application picker
system that can handle any MIME type or application category, similar
to Junction. This addresses the maintainer feedback about making the
functionality "as re-usable as possible."

Frontend (QML):
- Create generic AppPickerModal component (~450 lines)
  - Configurable filtering by application categories
  - Customizable title, view modes, and usage tracking
  - Emits applicationSelected signal for flexibility
- Refactor BrowserPickerModal as thin wrapper (473 → 46 lines)
  - Demonstrates how to create specialized pickers
  - Maintains all existing browser picker functionality

Backend (Go):
- Rename browser package to apppicker for clarity
- Enhance event model to support:
  - MIME types (for future file associations)
  - Application categories (WebBrowser, Office, Graphics, etc.)
  - Request types (url, file, custom)
- Maintain backward compatibility with browser.open method
- Add new apppicker.open method for generic usage

CLI:
- Rename commands_browser.go to commands_open.go
- Add extensibility flags:
  --mime/-m: Filter by MIME type
  --category/-c: Filter by category (repeatable)
  --type/-t: Specify request type
- Examples:
  dms open file.pdf --category Office
  dms open image.png --category Graphics

DMSService:
- Add appPickerRequested signal for generic events
- Smart routing between URL and generic app picker events
- Fully backward compatible

Benefits:
- Easy to create new pickers (~15 lines of wrapper code)
- Foundation for universal file handling system
- Consistent UX across all picker types
- Ready for MIME type associations

Future extensions:
- PDF picker, image viewer picker, text editor picker
- Default application management
- File association UI in settings
- Multi-MIME type desktop file integration

* fix(cli): remove all shorthands from open command flags for consistency

Remove shorthands from --mime, --category, and --type flags to maintain
consistency and avoid conflicts with global flags.

Flags now (all long-form only):
- --category: Application categories
- --mime: MIME type
- --type: Request type

Global flags still available:
- --config, -c: Config directory path

* style: apply gofmt formatting to apppicker files

Fix formatting issues caught by CI:
- Align struct field spacing in OpenEvent
- Align variable declaration spacing
- Fix Args field alignment in cobra.Command

* feat(apppicker): add generic file opener with auto MIME detection

Implements Junction-style generic file opening capabilities:

**Backend (Go):**
- Enhanced CLI to parse file:// URIs and extract file paths
- Auto-detect MIME types from file extensions using Go's mime package
- Auto-map MIME types to desktop categories:
  - Images → Graphics, Viewer
  - Videos → Video, AudioVideo
  - Audio → Audio, AudioVideo
  - Text → TextEditor, Office (or WebBrowser for HTML)
  - PDFs → Office, Viewer
  - Office docs → Office
  - Archives → Archiving, Utility
- Added debug logging to CLI and server handler for troubleshooting

**Frontend (QML):**
- Added generic AppPickerModal (filePickerModal) for file selection
- Connected to DMSService.appPickerRequested signal
- Implemented onApplicationSelected handler with desktop entry field code support:
  - %f/%F for file paths
  - %u/%U for file:// URIs
  - Fallback to appending path if no field codes
- Separate usage tracking: filePickerUsageHistory

**Desktop Integration:**
- Updated dms-open.desktop to handle x-scheme-handler/file
- Changed category from Network;WebBrowser to Utility (more generic)
- Added text/html to MIME types

**Usage:**
Set DMS as default for specific MIME types in ~/.config/mimeapps.list:
  text/plain=dms-open.desktop
  image/png=dms-open.desktop
  application/pdf=dms-open.desktop

Then use:
  xdg-open file.txt
  xdg-open image.png
  dms open document.pdf

The picker will show appropriate apps based on auto-detected categories.

Related to #815

* fix: resolve relative path handling by converting to absolute paths

- Convert file:// URIs to absolute filesystem paths for reliable file resolution
- Convert plain local file arguments to absolute paths to ensure consistent processing
- Update log messages to display absolute paths, improving traceability
- Retain request type detection while using absolute path extensions for MIME type inference

* feat(app-picker): add Tab key view toggle and fix targetData binding

- Add Tab key to toggle between grid and list views for better keyboard UX
- Fix bug where targetData binding broke after first modal close
  - Removed targetData reset from onDialogClosed
  - Parent components (BrowserPickerModal, filePickerModal) now manage targetData
  - Fixes issue where URL/file path disappeared on subsequent opens

* fix(app-picker): properly escape URLs and file paths for shell execution

- Add shellEscape() function to wrap arguments in single quotes
- Prevents shell interpretation of special characters (&, ?, =, spaces, etc.)
- Fixes bug where URLs with query parameters were truncated at first &
- Example: http://localhost:36275/vnc.html?autoconnect=true&reconnect=true
  now properly passes the full URL instead of cutting at first &
- Applied to both BrowserPickerModal (URLs) and filePickerModal (file paths)

* fix: check error return from InitializeAppPickerManager
2025-11-30 22:41:37 -05:00
mbpowers
7959a79575 feat: add autohide and settings ipc functions (#786)
* feat: bar visibility and autoHide IPC

also changed reveal to show

* feat: settings get/set IPC

* fix: show -> reveal, show is reserved keyword

* move IpcHandlers from SettingsData to DMSShellIPC
2025-11-30 20:50:00 -05:00
dms-ci[bot]
abf3249b67 nix: update vendorHash for go.mod changes 2025-12-01 00:27:18 +00:00
bbedward
35e0dc84e8 keybinds: add niri provider 2025-11-30 19:25:48 -05:00
mbpowers
17639e8729 feat: add sun and moon view to WeatherTab (#787)
* feat: add sun and moon view to WeatherTab

* feat: hourly forecast and scrollable date

* fix: put listviews in loaders to prevent ui blocking

* dankdash/weather: wrap all tab content in loaders, weather updates
- remove a bunch of transitions that make things feel glitchy
- use animation durations from Theme
- configurable detailed/compact hourly view

* weather: fix scroll and some display issues

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-11-30 18:47:27 -05:00
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
506 changed files with 72807 additions and 27958 deletions

View File

@@ -1,8 +1,4 @@
#!/bin/bash
# DISABLED for now
exit 0
set -euo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -10,18 +6,61 @@ REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
cd "$REPO_ROOT"
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
exit 0
# =============================================================================
# Go CI checks (when core/ files are staged)
# =============================================================================
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
if [[ -n "$STAGED_CORE_FILES" ]]; then
echo "Go files staged in core/, running CI checks..."
cd "$REPO_ROOT/core"
# Format check
echo " Checking gofmt..."
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
if [[ -n "$UNFORMATTED" ]]; then
echo "The following files are not formatted:"
echo "$UNFORMATTED"
echo ""
echo "Run: cd core && gofmt -s -w ."
exit 1
fi
# golangci-lint
if command -v golangci-lint &>/dev/null; then
echo " Running golangci-lint..."
golangci-lint run ./...
else
echo " Warning: golangci-lint not installed, skipping lint"
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
# Tests
echo " Running tests..."
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
if ! command -v python3 &>/dev/null; then
exit 0
fi
if ! python3 scripts/i18nsync.py check &>/dev/null; then
echo "Translations out of sync"
echo "run python3 scripts/i18nsync.py sync"
exit 1
fi
# =============================================================================
# i18n sync check (DISABLED for now)
# =============================================================================
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
# if command -v python3 &>/dev/null; then
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
# echo "Translations out of sync"
# echo "Run: python3 scripts/i18nsync.py sync"
# exit 1
# fi
# fi
# fi
exit 0

View File

@@ -7,9 +7,18 @@ on:
paths:
- 'core/**'
- '.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:
test:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
@@ -32,11 +41,20 @@ jobs:
exit 1
fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
working-directory: core
- name: Test
run: go test -v ./...
- name: Build dms
run: go build -v ./cmd/dms
- name: Build dms (distropkg)
run: go build -v -tags distro_binary ./cmd/dms
- name: Build dankinstall
run: go build -v ./cmd/dankinstall

View File

@@ -35,6 +35,14 @@ jobs:
with:
go-version-file: ./core/go.mod
- name: Format check
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
- name: Run tests
run: go test -v ./...
@@ -124,38 +132,40 @@ jobs:
runs-on: ubuntu-latest
needs: build-core
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app_token.outputs.token }}
fetch-depth: 0
- name: Update VERSION
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
version="${GITHUB_REF#refs/tags/}"
version_no_v="${version#v}"
echo "Updating to version: $version"
# Update VERSION file in quickshell/
echo "${version}" > quickshell/VERSION
git add quickshell/VERSION
if ! git diff --cached --quiet; then
git commit -m "chore: bump version to $version"
git push origin HEAD:master || git push origin HEAD:main
echo "Pushed version updates to master"
else
echo "No version changes needed"
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
fi
# Force-push the tag to point to the commit with updated 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:
runs-on: ubuntu-24.04
@@ -168,6 +178,11 @@ jobs:
with:
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
uses: actions/download-artifact@v4
with:
@@ -255,6 +270,9 @@ jobs:
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# 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
(cd quickshell && tar --exclude='.git' \
--exclude='.github' \
@@ -291,6 +309,11 @@ jobs:
fi
done
# Copy docs directory
if [ -d "docs" ]; then
cp -r docs _temp_full/
fi
# Create installation guide
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
# DankMaterialShell Installation
@@ -365,6 +388,68 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trigger-obs-update:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Update OBS packages
run: |
VERSION="${{ github.ref_name }}"
cd distro
bash scripts/obs-upload.sh dms "Update to $VERSION"
trigger-ppa-update:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Upload to PPA
run: |
VERSION="${{ github.ref_name }}"
cd distro/ubuntu/ppa
bash create-and-upload.sh ../dms dms questing
copr-build:
runs-on: ubuntu-latest
needs: release

View File

@@ -1,4 +1,4 @@
name: DMS Copr Stable Release (Manual)
name: DMS Copr Stable Release
on:
workflow_dispatch:
@@ -7,6 +7,10 @@ on:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false
default: ''
release:
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
required: false
default: '1'
jobs:
build-and-upload:
@@ -19,6 +23,7 @@ jobs:
- name: Determine version
id: version
run: |
# Get version from manual input or latest release
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION"
@@ -27,8 +32,14 @@ jobs:
echo "Using latest release version: $VERSION"
fi
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$RELEASE" ]; then
RELEASE="1"
fi
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
run: |
@@ -57,6 +68,7 @@ jobs:
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
@@ -68,7 +80,7 @@ jobs:
Name: dms
Version: %{version}
Release: 1%{?dist}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
@@ -212,16 +224,17 @@ jobs:
%{_bindir}/dgop
%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
- Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}"
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
echo ""
echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec
@@ -295,7 +308,7 @@ jobs:
run: |
echo "### 🎉 DMS Stable Build Summary" >> $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 "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $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
on:
workflow_dispatch:
push:
paths:
- "core/go.mod"
@@ -8,14 +9,25 @@ on:
branches:
- master
permissions:
contents: write
jobs:
update-vendor-hash:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
@@ -23,68 +35,32 @@ jobs:
- name: Update vendorHash in flake.nix
run: |
set -euo pipefail
# Try to build and capture the expected hash from error message
echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dmsCli 2>&1); then
echo "Build succeeded, no hash update needed"
exit 0
fi
# Extract the expected hash from the error message
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
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
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
echo "Current vendorHash: $current_hash"
if [ "$current_hash" = "$new_hash" ]; then
echo "vendorHash is already up to date"
exit 0
fi
# Update the hash in flake.nix
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
# Verify the build works with the new hash
echo "Verifying build with new vendorHash..."
nix build .#dmsCli
echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if ! git diff --quiet flake.nix; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes"
for attempt in 1 2 3; do
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
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else
echo "No changes to flake.nix"
fi

6
.gitignore vendored
View File

@@ -136,3 +136,9 @@ go.work.sum
# .vscode/
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.
## 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.
```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
},
```bash
git config core.hooksPath .githooks
```
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

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)
[![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)
[![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>
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
@@ -36,8 +36,10 @@ DankMaterialShell/
│ ├── cmd/ # dms CLI and dankinstall binaries
│ ├── internal/ # System integration, IPC, distro support
│ └── pkg/ # Shared packages
├── distro/ # Distribution packaging (Fedora RPM specs)
├── nix/ # NixOS/home-manager modules
├── distro/ # Distribution packaging
│ ├── fedora/ # Fedora RPM specs
│ ├── debian/ # Debian packaging
│ └── nix/ # NixOS/home-manager modules
└── flake.nix # Nix flake for declarative installation
```
@@ -103,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), 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)
@@ -136,8 +138,7 @@ See component-specific documentation:
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
- **[core/](core/)** - Go backend, CLI tools, and system integration
- **[distro/](distro/)** - Distribution packaging
- **[nix/](nix/)** - NixOS and home-manager modules
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
### 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
- [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
MIT License - See [LICENSE](LICENSE) for details.

10
assets/dms-open.desktop Normal file
View File

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

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`)
- Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
**Plugin System**
- Plugin registry integration
@@ -71,6 +72,13 @@ sudo make install # Install to /usr/local/bin/dms
## 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:**
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ package main
import (
"fmt"
"os"
"strings"
"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("ghostty", false, "Output in Ghostty 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("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")
isGhostty, _ := cmd.Flags().GetBool("ghostty")
isWezterm, _ := cmd.Flags().GetBool("wezterm")
vscodeEnrich, _ := cmd.Flags().GetString("vscode-enrich")
background, _ := cmd.Flags().GetString("background")
contrastAlgo, _ := cmd.Flags().GetString("contrast")
@@ -65,18 +62,7 @@ func runDank16(cmd *cobra.Command, args []string) {
colors := dank16.GeneratePalette(primaryColor, opts)
if vscodeEnrich != "" {
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 {
if isJson {
fmt.Print(dank16.GenerateJSON(colors))
} else if isKitty {
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 {
case distros.FamilyArch:
updateErr = updateArchLinux()
case distros.FamilyNix:
updateErr = updateNixOS()
case distros.FamilySUSE:
updateErr = updateOtherDistros()
default:
@@ -152,27 +150,6 @@ func updateArchLinux() error {
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 {
homeDir, err := os.UserHomeDir()
if err != nil {

View File

@@ -11,6 +11,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var greeterCmd = &cobra.Command{
@@ -217,6 +219,191 @@ func checkGroupExists(groupName string) bool {
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 {
fmt.Println("=== DMS Greeter Enable ===")
fmt.Println()
@@ -232,8 +419,29 @@ func enableGreeter() error {
}
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")
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
}
@@ -322,11 +530,23 @@ func enableGreeter() error {
}
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("\nTo start the greeter, run:")
fmt.Println("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nTo enable on boot, run:")
fmt.Println(" sudo systemctl enable --now greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil
}

View File

@@ -34,9 +34,7 @@ var keybindsShowCmd = &cobra.Command{
}
func init() {
keybindsShowCmd.Flags().String("hyprland-path", "$HOME/.config/hypr", "Path to Hyprland config directory")
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")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd)
@@ -66,6 +64,11 @@ func initializeProviders() {
log.Warnf("Failed to register Sway provider: %v", err)
}
niriProvider := providers.NewNiriProvider("")
if err := registry.Register(niriProvider); err != nil {
log.Warnf("Failed to register Niri provider: %v", err)
}
config := keybinds.DefaultDiscoveryConfig()
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
log.Warnf("Failed to auto-discover providers: %v", err)
@@ -89,25 +92,36 @@ func runKeybindsList(cmd *cobra.Command, args []string) {
func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0]
registry := keybinds.GetDefaultRegistry()
if providerName == "hyprland" {
hyprlandPath, _ := cmd.Flags().GetString("hyprland-path")
hyprlandProvider := providers.NewHyprlandProvider(hyprlandPath)
registry.Register(hyprlandProvider)
}
customPath, _ := cmd.Flags().GetString("path")
if customPath != "" {
var provider keybinds.Provider
switch providerName {
case "hyprland":
provider = providers.NewHyprlandProvider(customPath)
case "mangowc":
provider = providers.NewMangoWCProvider(customPath)
case "sway":
provider = providers.NewSwayProvider(customPath)
case "niri":
provider = providers.NewNiriProvider(customPath)
default:
log.Fatalf("Provider %s does not support custom path", providerName)
}
if providerName == "mangowc" {
mangowcPath, _ := cmd.Flags().GetString("mangowc-path")
mangowcProvider := providers.NewMangoWCProvider(mangowcPath)
registry.Register(mangowcProvider)
}
sheet, err := provider.GetCheatSheet()
if err != nil {
log.Fatalf("Error getting cheatsheet: %v", err)
}
if providerName == "sway" {
swayPath, _ := cmd.Flags().GetString("sway-path")
swayProvider := providers.NewSwayProvider(swayPath)
registry.Register(swayProvider)
output, err := json.MarshalIndent(sheet, "", " ")
if err != nil {
log.Fatalf("Error generating JSON: %v", err)
}
fmt.Fprintln(os.Stdout, string(output))
return
}
provider, err := registry.Get(providerName)

View File

@@ -0,0 +1,224 @@
package main
import (
"encoding/json"
"fmt"
"mime"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
var (
openMimeType string
openCategories []string
openRequestType string
)
var openCmd = &cobra.Command{
Use: "open [target]",
Short: "Open a file, URL, or resource with an application picker",
Long: `Open a target (URL, file, or other resource) using the DMS application picker.
By default, this opens URLs with the browser picker. You can customize the behavior
with flags to handle different MIME types or application categories.
Examples:
dms open https://example.com # Open URL with browser picker
dms open file.pdf --mime application/pdf # Open PDF with compatible apps
dms open document.odt --category Office # Open with office applications
dms open --mime image/png image.png # Open image with image viewers`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runOpen(args[0])
},
}
func init() {
rootCmd.AddCommand(openCmd)
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)")
openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)")
}
// mimeTypeToCategories maps MIME types to desktop file categories
func mimeTypeToCategories(mimeType string) []string {
// Split MIME type to get the main type
parts := strings.Split(mimeType, "/")
if len(parts) < 1 {
return nil
}
mainType := parts[0]
switch mainType {
case "image":
return []string{"Graphics", "Viewer"}
case "video":
return []string{"Video", "AudioVideo"}
case "audio":
return []string{"Audio", "AudioVideo"}
case "text":
if strings.Contains(mimeType, "html") {
return []string{"WebBrowser"}
}
return []string{"TextEditor", "Office"}
case "application":
if strings.Contains(mimeType, "pdf") {
return []string{"Office", "Viewer"}
}
if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") ||
strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") ||
strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") ||
strings.Contains(mimeType, "opendocument") {
return []string{"Office"}
}
if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") ||
strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") {
return []string{"Archiving", "Utility"}
}
return []string{"Office", "Viewer"}
}
return nil
}
func runOpen(target string) {
socketPath, err := server.FindSocket()
if err != nil {
log.Warnf("DMS socket not found: %v", err)
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Warnf("DMS socket connection failed: %v", err)
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
defer conn.Close()
buf := make([]byte, 1)
for {
_, err := conn.Read(buf)
if err != nil {
return
}
if buf[0] == '\n' {
break
}
}
// Parse file:// URIs to extract the actual file path
actualTarget := target
detectedMimeType := openMimeType
detectedCategories := openCategories
detectedRequestType := openRequestType
log.Infof("Processing target: %s", target)
if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" {
// Extract file path from file:// URI and convert to absolute path
actualTarget = parsedURL.Path
if absPath, err := filepath.Abs(actualTarget); err == nil {
actualTarget = absPath
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
} else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
// Handle HTTP(S) URLs
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected HTTP(S) URL")
} else if _, err := os.Stat(target); err == nil {
// Handle local file paths directly (not file:// URIs)
// Convert to absolute path
if absPath, err := filepath.Abs(target); err == nil {
actualTarget = absPath
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected local file path, converted to absolute: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
}
params := map[string]interface{}{
"target": actualTarget,
}
if detectedMimeType != "" {
params["mimeType"] = detectedMimeType
}
if len(detectedCategories) > 0 {
params["categories"] = detectedCategories
}
if detectedRequestType != "" {
params["requestType"] = detectedRequestType
}
method := "apppicker.open"
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
method = "browser.open"
params["url"] = target
}
req := models.Request{
ID: 1,
Method: method,
Params: params,
}
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
if err := json.NewEncoder(conn).Encode(req); err != nil {
log.Fatalf("Failed to send request: %v", err)
}
log.Infof("Request sent successfully")
}

View File

@@ -1,14 +1,12 @@
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"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/log"
tea "github.com/charmbracelet/bubbletea"
@@ -76,14 +74,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
return nil
}
func runInteractiveMode(cmd *cobra.Command, args []string) {
detector, err := 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)
}
detector, _ := dms.NewDetector()
if !detector.IsDMSInstalled() {
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()
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
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)
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
@@ -374,6 +383,7 @@ func runShellDaemon(session bool) {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
@@ -387,6 +397,10 @@ func runShellDaemon(session bool) {
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()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {

View File

@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os/exec"
"strings"
)
func commandExists(cmd string) bool {
@@ -24,3 +25,68 @@ func isArchPackageInstalled(packageName string) bool {
err := cmd.Run()
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,65 @@ go 1.24.6
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.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/log v0.4.2
github.com/godbus/dbus/v5 v5.1.0
github.com/spf13/cobra v1.9.1
github.com/fsnotify/fsnotify v1.9.0
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/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/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/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
)
require (
github.com/atotto/clipboard v0.1.4 // 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/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/ansi v0.11.2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect
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/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
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
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0
golang.org/x/text v0.29.0 // indirect
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0
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/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/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/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/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
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.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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/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/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA=
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 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
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/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-20250929195514-145daf2492dd/go.mod h1:lz8PQr/p79XpFq5ODVBwRJu5LnOF8Et7j95ehqmCMJU=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
github.com/go-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/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/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/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
@@ -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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
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.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
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=
@@ -91,46 +115,48 @@ github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
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/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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 h1:iTAt1me6SBYsuzrl/CmrxtATPlOG/pVviosM3DhUdKE=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34/go.mod h1:jzmUN5lUAv2O8e63OvcauV4S30rIZ1BvF/PNYE37vDo=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

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

View File

@@ -218,6 +218,11 @@ window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
open-floating true
}
binds {
// === System & 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
}
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 {
if hex[0] == '#' {
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}))
}
func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
baseColor := DeriveContainer(primaryColor, opts.IsLight)
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
palette := make([]string, 0, 16)
var palette Palette
var normalTextTarget, secondaryTarget float64
if opts.UseDPS {
@@ -335,7 +377,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
} else {
bgColor = "#1a1a1a"
}
palette = append(palette, bgColor)
palette.Color0 = NewColorInfo(bgColor)
hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15
@@ -344,39 +386,39 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
var redColor string
if opts.IsLight {
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 {
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)
var greenColor string
if opts.IsLight {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette = 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)
var yellowColor string
if opts.IsLight {
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 {
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
if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
}
magH := hsv.H - 0.03
@@ -388,65 +430,64 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
hh := RGBToHSV(hr)
if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
}
cyanH := hsv.H + 0.08
if 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 {
palette = append(palette, "#1a1a1a")
palette = append(palette, "#2e2e2e")
palette.Color7 = NewColorInfo("#1a1a1a")
palette.Color8 = NewColorInfo("#2e2e2e")
} else {
palette = append(palette, "#abb2bf")
palette = append(palette, "#5c6370")
palette.Color7 = NewColorInfo("#abb2bf")
palette.Color8 = NewColorInfo("#5c6370")
}
if opts.IsLight {
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}))
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}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
palette = 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)}))
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)}))
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette = 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}))
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}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
// Make it way brighter for type names in dark mode
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
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)}))
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 {
brightCyanH -= 1.0
}
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
}
if opts.IsLight {
palette = append(palette, "#1a1a1a")
palette.Color15 = NewColorInfo("#1a1a1a")
} else {
palette = append(palette, "#ffffff")
palette.Color15 = NewColorInfo("#ffffff")
}
return palette

View File

@@ -1,7 +1,6 @@
package dank16
import (
"encoding/json"
"math"
"testing"
)
@@ -346,106 +345,36 @@ func TestGeneratePalette(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 {
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
colors := []ColorInfo{
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 {
if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
for i, color := range colors {
if len(color.Hex) != 7 || color.Hex[0] != '#' {
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 {
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
if tt.opts.Background != "" && result.Color0.Hex != 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.Color0.Hex != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex)
} else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
}
if tt.opts.IsLight && result[15] != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
} else if !tt.opts.IsLight && result[15] != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
}
})
}
}
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) {
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) {
result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 {
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
colors := []ColorInfo{
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 {
if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
for i, color := range colors {
if len(color.Hex) != 7 || color.Hex[0] != '#' {
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++ {
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
minLc := 30.0
if lc < minLc && lc > 0 {
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)
paletteDPS := GeneratePalette(base, optsDPS)
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
t.Fatal("Both palettes should have 16 colors")
wcagColors := []ColorInfo{
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] {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
if paletteWCAG.Color0.Hex != paletteDPS.Color0.Hex {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG.Color0.Hex, paletteDPS.Color0.Hex)
}
differentCount := 0
for i := 0; i < 16; i++ {
if paletteWCAG[i] != paletteDPS[i] {
if wcagColors[i].Hex != dpsColors[i].Hex {
differentCount++
}
}

View File

@@ -6,135 +6,104 @@ import (
"strings"
)
func GenerateJSON(colors []string) string {
colorMap := make(map[string]string)
for i, color := range colors {
colorMap[fmt.Sprintf("color%d", i)] = color
}
marshalled, _ := json.Marshal(colorMap)
func GenerateJSON(p Palette) string {
marshalled, _ := json.Marshal(p)
return string(marshalled)
}
func GenerateKittyTheme(colors []string) 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},
}
func GenerateKittyTheme(p Palette) string {
var result strings.Builder
for _, kc := range kittyColors {
fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index])
}
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
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()
}
func GenerateFootTheme(colors []string) 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},
}
func GenerateFootTheme(p Palette) string {
var result strings.Builder
for _, fc := range footColors {
fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#"))
}
fmt.Fprintf(&result, "regular0=%s\n", p.Color0.HexStripped)
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()
}
func GenerateAlacrittyTheme(colors []string) 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},
}
func GenerateAlacrittyTheme(p Palette) string {
var result strings.Builder
currentSection := ""
for _, ac := range alacrittyColors {
if ac.section != currentSection {
if currentSection != "" {
result.WriteString("\n")
}
fmt.Fprintf(&result, "[colors.%s]\n", ac.section)
currentSection = ac.section
}
fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index])
}
result.WriteString("[colors.normal]\n")
fmt.Fprintf(&result, "black = '%s'\n", p.Color0.Hex)
fmt.Fprintf(&result, "red = '%s'\n", p.Color1.Hex)
fmt.Fprintf(&result, "green = '%s'\n", p.Color2.Hex)
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color3.Hex)
fmt.Fprintf(&result, "blue = '%s'\n", p.Color4.Hex)
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color5.Hex)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color6.Hex)
fmt.Fprintf(&result, "white = '%s'\n", p.Color7.Hex)
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()
}
func GenerateGhosttyTheme(colors []string) string {
func GenerateGhosttyTheme(p Palette) string {
var result strings.Builder
for i, color := range colors {
fmt.Fprintf(&result, "palette = %d=%s\n", i, color)
}
fmt.Fprintf(&result, "palette = 0=%s\n", p.Color0.Hex)
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()
}
func GenerateWeztermTheme(colors []string) string {
func GenerateWeztermTheme(p Palette) string {
var result strings.Builder
labels := []string{"ansi", "brights"}
for j, label := range labels {
start := j * 8
colorSlice := make([]string, 8)
for i, color := range colors[start : start+8] {
colorSlice[i] = fmt.Sprintf("'%s'", color)
}
fmt.Fprintf(&result, "%s = [%s]\n", label, strings.Join(colorSlice, ", "))
}
fmt.Fprintf(&result, "ansi = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
p.Color0.Hex, p.Color1.Hex, p.Color2.Hex, p.Color3.Hex,
p.Color4.Hex, p.Color5.Hex, p.Color6.Hex, p.Color7.Hex)
fmt.Fprintf(&result, "brights = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
p.Color8.Hex, p.Color9.Hex, p.Color10.Hex, p.Color11.Hex,
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
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 {
return NewArchDistribution(config, logChan)
})
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
}
type ArchDistribution struct {

View File

@@ -209,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
}
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 {
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 {
return NewFedoraDistribution(config, logChan)
})
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
}
type FedoraDistribution struct {
@@ -165,7 +167,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
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
@@ -203,7 +205,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
if variant == deps.VariantGit {
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 {
@@ -506,6 +508,14 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{

View File

@@ -478,6 +478,95 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
return fmt.Errorf("failed to create cache directory: %w", err)
}
// Install hyprutils first
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.05,
Step: "Building hyprutils dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
}
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
return fmt.Errorf("failed to create hyprutils directory: %w", err)
}
defer os.RemoveAll(hyprutilsDir)
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
if err := cloneUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprutils: %w", err)
}
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-DBUILD_TESTING=off",
"-S", ".",
"-B", "./build")
configureUtilsCmd.Dir = hyprutilsDir
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
return fmt.Errorf("failed to configure hyprutils: %w", err)
}
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
buildUtilsCmd.Dir = hyprutilsDir
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
return fmt.Errorf("failed to build hyprutils: %w", err)
}
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installUtilsCmd.Dir = hyprutilsDir
if err := installUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprutils: %w", err)
}
// Install hyprwayland-scanner
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Building hyprwayland-scanner dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
}
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
if err := os.MkdirAll(scannerDir, 0755); err != nil {
return fmt.Errorf("failed to create scanner directory: %w", err)
}
defer os.RemoveAll(scannerDir)
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
if err := cloneScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
}
configureScannerCmd := exec.CommandContext(ctx, "cmake",
"-DCMAKE_INSTALL_PREFIX=/usr",
"-B", "build")
configureScannerCmd.Dir = scannerDir
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
}
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
buildScannerCmd.Dir = scannerDir
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
}
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installScannerCmd.Dir = scannerDir
if err := installScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
}
// Now build hyprpicker
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
@@ -486,7 +575,7 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Progress: 0.35,
Step: "Cloning hyprpicker repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
@@ -499,16 +588,39 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.4,
Step: "Building hyprpicker...",
Progress: 0.45,
Step: "Configuring hyprpicker build...",
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.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)
}
@@ -518,10 +630,10 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
Step: "Installing hyprpicker...",
IsComplete: false,
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
if err := installCmd.Run(); err != nil {
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{}
}
isNixOS := d.isNixOS()
var components []DependencyInfo
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{
Name: dep.Name,
Status: dep.Status,
@@ -142,23 +135,6 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
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 {
Name string
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, ".cache"), ".cache 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...")
@@ -239,8 +240,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
}
}
// Set ACL to allow greeter user execute (traverse) permission
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:x", dir.path); err != nil {
// Set ACL to allow greeter user read+execute permission (for session discovery)
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(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
continue
@@ -287,6 +288,8 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
{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 {
@@ -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 {
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) {
expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") {
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/hyprland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
)
@@ -26,7 +25,7 @@ func (h *HyprlandProvider) Name() string {
}
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := hyprland.ParseKeys(h.configPath)
section, err := ParseHyprlandKeys(h.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
}
@@ -41,7 +40,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
}, 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
if section.Name != "" {
currentSubcat = section.Name
@@ -86,29 +85,31 @@ 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)
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
desc := kb.Comment
if desc == "" {
desc = h.generateDescription(kb.Dispatcher, kb.Params)
desc = rawAction
}
return keybinds.Keybind{
Key: key,
Description: desc,
Action: rawAction,
Subcategory: subcategory,
}
}
func (h *HyprlandProvider) generateDescription(dispatcher, params string) string {
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
if params != "" {
return dispatcher + " " + params
}
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 = append(parts, kb.Mods...)
parts = append(parts, kb.Key)

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
)
@@ -83,6 +84,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
var flatBinds []struct {
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Category string `json:"cat,omitempty"`
Subcategory string `json:"subcat,omitempty"`
}
@@ -99,6 +101,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
kb := keybinds.Keybind{
Key: bind.Key,
Description: bind.Description,
Action: bind.Action,
Subcategory: bind.Subcategory,
}
categorizedBinds[category] = append(categorizedBinds[category], kb)
@@ -118,7 +121,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") {
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
)
type MangoWCProvider struct {
@@ -26,7 +25,7 @@ func (m *MangoWCProvider) Name() string {
}
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
keybinds_list, err := mangowc.ParseKeys(m.configPath)
keybinds_list, err := ParseMangoWCKeys(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
}
@@ -83,28 +82,30 @@ 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)
rawAction := m.formatRawAction(kb.Command, kb.Params)
desc := kb.Comment
if desc == "" {
desc = m.generateDescription(kb.Command, kb.Params)
desc = rawAction
}
return keybinds.Keybind{
Key: key,
Description: desc,
Action: rawAction,
}
}
func (m *MangoWCProvider) generateDescription(command, params string) string {
func (m *MangoWCProvider) formatRawAction(command, params string) string {
if params != "" {
return command + " " + params
}
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 = append(parts, kb.Mods...)
parts = append(parts, kb.Key)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,137 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
)
type NiriProvider struct {
configDir string
}
func NewNiriProvider(configDir string) *NiriProvider {
if configDir == "" {
configDir = defaultNiriConfigDir()
}
return &NiriProvider{
configDir: configDir,
}
}
func defaultNiriConfigDir() string {
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome != "" {
return filepath.Join(configHome, "niri")
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "niri")
}
func (n *NiriProvider) Name() string {
return "niri"
}
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := ParseNiriKeys(n.configDir)
if err != nil {
return nil, fmt.Errorf("failed to parse niri config: %w", err)
}
categorizedBinds := make(map[string][]keybinds.Keybind)
n.convertSection(section, "", categorizedBinds)
return &keybinds.CheatSheet{
Title: "Niri Keybinds",
Provider: n.Name(),
Binds: categorizedBinds,
}, nil
}
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
currentSubcat := subcategory
if section.Name != "" {
currentSubcat = section.Name
}
for _, kb := range section.Keybinds {
category := n.categorizeByAction(kb.Action)
bind := n.convertKeybind(&kb, currentSubcat)
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
for _, child := range section.Children {
n.convertSection(&child, currentSubcat, categorizedBinds)
}
}
func (n *NiriProvider) categorizeByAction(action string) string {
switch {
case action == "next-window" || action == "previous-window":
return "Alt-Tab"
case strings.Contains(action, "screenshot"):
return "Screenshot"
case action == "show-hotkey-overlay" || action == "toggle-overview":
return "Overview"
case action == "quit" ||
action == "power-off-monitors" ||
action == "toggle-keyboard-shortcuts-inhibit" ||
strings.Contains(action, "dpms"):
return "System"
case action == "spawn":
return "Execute"
case strings.Contains(action, "workspace"):
return "Workspace"
case strings.HasPrefix(action, "focus-monitor") ||
strings.HasPrefix(action, "move-column-to-monitor") ||
strings.HasPrefix(action, "move-window-to-monitor"):
return "Monitor"
case strings.Contains(action, "window") ||
strings.Contains(action, "focus") ||
strings.Contains(action, "move") ||
strings.Contains(action, "swap") ||
strings.Contains(action, "resize") ||
strings.Contains(action, "column"):
return "Window"
default:
return "Other"
}
}
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind {
key := n.formatKey(kb)
desc := kb.Description
rawAction := n.formatRawAction(kb.Action, kb.Args)
if desc == "" {
desc = rawAction
}
return keybinds.Keybind{
Key: key,
Description: desc,
Action: rawAction,
Subcategory: subcategory,
}
}
func (n *NiriProvider) formatRawAction(action string, args []string) string {
if len(args) == 0 {
return action
}
return action + " " + strings.Join(args, " ")
}
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}

View File

@@ -0,0 +1,229 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document"
)
type NiriKeyBinding struct {
Mods []string
Key string
Action string
Args []string
Description string
}
type NiriSection struct {
Name string
Keybinds []NiriKeyBinding
Children []NiriSection
}
type NiriParser struct {
configDir string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
}
func NewNiriParser(configDir string) *NiriParser {
return &NiriParser{
configDir: configDir,
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
}
}
func (p *NiriParser) Parse() (*NiriSection, error) {
configPath := filepath.Join(p.configDir, "config.kdl")
section, err := p.parseFile(configPath, "")
if err != nil {
return nil, err
}
section.Keybinds = p.finalizeBinds()
return section, nil
}
func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
for _, key := range p.bindOrder {
if kb, ok := p.bindMap[key]; ok {
binds = append(binds, *kb)
}
}
return binds
}
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb)
if _, exists := p.bindMap[key]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
p.bindMap[key] = kb
}
func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve path %s: %w", filePath, err)
}
if p.processedFiles[absPath] {
return &NiriSection{Name: sectionName}, nil
}
p.processedFiles[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
}
section := &NiriSection{
Name: sectionName,
}
baseDir := filepath.Dir(absPath)
p.processNodes(doc.Nodes, section, baseDir)
return section, nil
}
func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection, baseDir string) {
for _, node := range nodes {
name := node.Name.String()
switch name {
case "include":
p.handleInclude(node, section, baseDir)
case "binds":
p.extractBinds(node, section, "")
case "recent-windows":
p.handleRecentWindows(node, section)
}
}
}
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
if len(node.Arguments) == 0 {
return
}
includePath := node.Arguments[0].String()
includePath = strings.Trim(includePath, "\"")
var fullPath string
if filepath.IsAbs(includePath) {
fullPath = includePath
} else {
fullPath = filepath.Join(baseDir, includePath)
}
includedSection, err := p.parseFile(fullPath, "")
if err != nil {
return
}
section.Children = append(section.Children, includedSection.Children...)
}
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
if node.Children == nil {
return
}
for _, child := range node.Children {
if child.Name.String() != "binds" {
continue
}
p.extractBinds(child, section, "Alt-Tab")
}
}
func (p *NiriParser) extractBinds(node *document.Node, section *NiriSection, subcategory string) {
if node.Children == nil {
return
}
for _, child := range node.Children {
kb := p.parseKeybindNode(child, subcategory)
if kb == nil {
continue
}
p.addBind(kb)
}
}
func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *NiriKeyBinding {
keyCombo := node.Name.String()
if keyCombo == "" {
return nil
}
mods, key := p.parseKeyCombo(keyCombo)
var action string
var args []string
if len(node.Children) > 0 {
actionNode := node.Children[0]
action = actionNode.Name.String()
for _, arg := range actionNode.Arguments {
args = append(args, strings.Trim(arg.String(), "\""))
}
}
description := ""
if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
description = strings.Trim(val.String(), "\"")
}
}
return &NiriKeyBinding{
Mods: mods,
Key: key,
Action: action,
Args: args,
Description: description,
}
}
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
parts := strings.Split(combo, "+")
if len(parts) == 0 {
return nil, combo
}
if len(parts) == 1 {
return nil, parts[0]
}
mods := parts[:len(parts)-1]
key := parts[len(parts)-1]
return mods, key
}
func ParseNiriKeys(configDir string) (*NiriSection, error) {
parser := NewNiriParser(configDir)
return parser.Parse()
}

View File

@@ -0,0 +1,498 @@
package providers
import (
"os"
"path/filepath"
"testing"
)
func TestNiriParseKeyCombo(t *testing.T) {
tests := []struct {
combo string
expectedMods []string
expectedKey string
}{
{"Mod+Q", []string{"Mod"}, "Q"},
{"Mod+Shift+F", []string{"Mod", "Shift"}, "F"},
{"Ctrl+Alt+Delete", []string{"Ctrl", "Alt"}, "Delete"},
{"Print", nil, "Print"},
{"XF86AudioMute", nil, "XF86AudioMute"},
{"Super+Tab", []string{"Super"}, "Tab"},
{"Mod+Shift+Ctrl+H", []string{"Mod", "Shift", "Ctrl"}, "H"},
}
parser := NewNiriParser("")
for _, tt := range tests {
t.Run(tt.combo, func(t *testing.T) {
mods, key := parser.parseKeyCombo(tt.combo)
if len(mods) != len(tt.expectedMods) {
t.Errorf("Mods length = %d, want %d", len(mods), len(tt.expectedMods))
} else {
for i := range mods {
if mods[i] != tt.expectedMods[i] {
t.Errorf("Mods[%d] = %q, want %q", i, mods[i], tt.expectedMods[i])
}
}
}
if key != tt.expectedKey {
t.Errorf("Key = %q, want %q", key, tt.expectedKey)
}
})
}
}
func TestNiriParseBasicBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Q { close-window; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(section.Keybinds))
}
foundClose := false
foundFullscreen := false
foundTerminal := false
for _, kb := range section.Keybinds {
switch kb.Action {
case "close-window":
foundClose = true
if kb.Key != "Q" || len(kb.Mods) != 1 || kb.Mods[0] != "Mod" {
t.Errorf("close-window keybind mismatch: %+v", kb)
}
case "fullscreen-window":
foundFullscreen = true
case "spawn":
foundTerminal = true
if kb.Description != "Open Terminal" {
t.Errorf("spawn description = %q, want %q", kb.Description, "Open Terminal")
}
if len(kb.Args) != 1 || kb.Args[0] != "kitty" {
t.Errorf("spawn args = %v, want [kitty]", kb.Args)
}
}
}
if !foundClose {
t.Error("close-window keybind not found")
}
if !foundFullscreen {
t.Error("fullscreen-window keybind not found")
}
if !foundTerminal {
t.Error("spawn keybind not found")
}
}
func TestNiriParseRecentWindows(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(section.Keybinds))
}
foundNext := false
foundPrev := false
for _, kb := range section.Keybinds {
switch kb.Action {
case "next-window":
foundNext = true
case "previous-window":
foundPrev = true
}
}
if !foundNext {
t.Error("next-window keybind not found")
}
if !foundPrev {
t.Error("previous-window keybind not found")
}
}
func TestNiriParseInclude(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
mainConfig := filepath.Join(tmpDir, "config.kdl")
includeConfig := filepath.Join(subDir, "binds.kdl")
mainContent := `binds {
Mod+Q { close-window; }
}
include "dms/binds.kdl"
`
includeContent := `binds {
Mod+T hotkey-overlay-title="Terminal" { spawn "kitty"; }
}
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
t.Fatalf("Failed to write include config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(section.Keybinds))
}
}
func TestNiriParseIncludeOverride(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
mainConfig := filepath.Join(tmpDir, "config.kdl")
includeConfig := filepath.Join(subDir, "binds.kdl")
mainContent := `binds {
Mod+T hotkey-overlay-title="Main Terminal" { spawn "alacritty"; }
}
include "dms/binds.kdl"
`
includeContent := `binds {
Mod+T hotkey-overlay-title="Override Terminal" { spawn "kitty"; }
}
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
t.Fatalf("Failed to write include config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(section.Keybinds))
}
if len(section.Keybinds) > 0 {
kb := section.Keybinds[0]
if kb.Description != "Override Terminal" {
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
}
if len(kb.Args) != 1 || kb.Args[0] != "kitty" {
t.Errorf("Expected args [kitty] (from include), got %v", kb.Args)
}
}
}
func TestNiriParseCircularInclude(t *testing.T) {
tmpDir := t.TempDir()
mainConfig := filepath.Join(tmpDir, "config.kdl")
otherConfig := filepath.Join(tmpDir, "other.kdl")
mainContent := `binds {
Mod+Q { close-window; }
}
include "other.kdl"
`
otherContent := `binds {
Mod+T { spawn "kitty"; }
}
include "config.kdl"
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(otherConfig, []byte(otherContent), 0644); err != nil {
t.Fatalf("Failed to write other config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(section.Keybinds))
}
}
func TestNiriParseMissingInclude(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Q { close-window; }
}
include "nonexistent/file.kdl"
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
}
if len(section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(section.Keybinds))
}
}
func TestNiriParseNoBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `cursor {
xcursor-theme "Bibata"
xcursor-size 24
}
input {
keyboard {
numlock
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 0 {
t.Errorf("Expected 0 keybinds, got %d", len(section.Keybinds))
}
}
func TestNiriParseErrors(t *testing.T) {
tests := []struct {
name string
path string
}{
{
name: "nonexistent_directory",
path: "/nonexistent/path/that/does/not/exist",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseNiriKeys(tt.path)
if err == nil {
t.Error("Expected error, got nil")
}
})
}
}
func TestNiriBindOverrideBehavior(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+T hotkey-overlay-title="First" { spawn "first"; }
Mod+Q { close-window; }
Mod+T hotkey-overlay-title="Second" { spawn "second"; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 3 {
t.Fatalf("Expected 3 unique keybinds, got %d", len(section.Keybinds))
}
var modT *NiriKeyBinding
for i := range section.Keybinds {
kb := &section.Keybinds[i]
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
modT = kb
break
}
}
if modT == nil {
t.Fatal("Mod+T keybind not found")
}
if modT.Description != "Third" {
t.Errorf("Mod+T description = %q, want 'Third' (last definition wins)", modT.Description)
}
if len(modT.Args) != 1 || modT.Args[0] != "third" {
t.Errorf("Mod+T args = %v, want [third] (last definition wins)", modT.Args)
}
}
func TestNiriBindOverrideWithIncludes(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "custom")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
mainConfig := filepath.Join(tmpDir, "config.kdl")
includeConfig := filepath.Join(subDir, "overrides.kdl")
mainContent := `binds {
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+T hotkey-overlay-title="Default Terminal" { spawn "xterm"; }
}
include "custom/overrides.kdl"
binds {
Mod+3 { focus-workspace 3; }
}
`
includeContent := `binds {
Mod+T hotkey-overlay-title="Custom Terminal" { spawn "kitty"; }
Mod+2 { focus-workspace 22; }
}
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
t.Fatalf("Failed to write include config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 4 {
t.Errorf("Expected 4 unique keybinds, got %d", len(section.Keybinds))
}
bindMap := make(map[string]*NiriKeyBinding)
for i := range section.Keybinds {
kb := &section.Keybinds[i]
key := ""
for _, m := range kb.Mods {
key += m + "+"
}
key += kb.Key
bindMap[key] = kb
}
if kb, ok := bindMap["Mod+T"]; ok {
if kb.Description != "Custom Terminal" {
t.Errorf("Mod+T should be overridden by include, got description %q", kb.Description)
}
} else {
t.Error("Mod+T not found")
}
if kb, ok := bindMap["Mod+2"]; ok {
if len(kb.Args) != 1 || kb.Args[0] != "22" {
t.Errorf("Mod+2 should be overridden by include with workspace 22, got args %v", kb.Args)
}
} else {
t.Error("Mod+2 not found")
}
if _, ok := bindMap["Mod+1"]; !ok {
t.Error("Mod+1 should exist (not overridden)")
}
if _, ok := bindMap["Mod+3"]; !ok {
t.Error("Mod+3 should exist (added after include)")
}
}
func TestNiriParseMultipleArgs(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 1 {
t.Fatalf("Expected 1 keybind, got %d", len(section.Keybinds))
}
kb := section.Keybinds[0]
if len(kb.Args) != 5 {
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
}
expectedArgs := []string{"dms", "ipc", "call", "spotlight", "toggle"}
for i, arg := range expectedArgs {
if i < len(kb.Args) && kb.Args[i] != arg {
t.Errorf("Args[%d] = %q, want %q", i, kb.Args[i], arg)
}
}
}

View File

@@ -0,0 +1,261 @@
package providers
import (
"os"
"path/filepath"
"testing"
)
func TestNiriProviderName(t *testing.T) {
provider := NewNiriProvider("")
if provider.Name() != "niri" {
t.Errorf("Name() = %q, want %q", provider.Name(), "niri")
}
}
func TestNiriProviderGetCheatSheet(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Q { close-window; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
Mod+1 { focus-workspace 1; }
Mod+Shift+1 { move-column-to-workspace 1; }
Print { screenshot; }
Mod+Shift+E { quit; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
provider := NewNiriProvider(tmpDir)
cheatSheet, err := provider.GetCheatSheet()
if err != nil {
t.Fatalf("GetCheatSheet failed: %v", err)
}
if cheatSheet.Title != "Niri Keybinds" {
t.Errorf("Title = %q, want %q", cheatSheet.Title, "Niri Keybinds")
}
if cheatSheet.Provider != "niri" {
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
}
windowBinds := cheatSheet.Binds["Window"]
if len(windowBinds) < 2 {
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
}
execBinds := cheatSheet.Binds["Execute"]
if len(execBinds) < 1 {
t.Errorf("Expected at least 1 Execute bind, got %d", len(execBinds))
}
workspaceBinds := cheatSheet.Binds["Workspace"]
if len(workspaceBinds) < 2 {
t.Errorf("Expected at least 2 Workspace binds, got %d", len(workspaceBinds))
}
screenshotBinds := cheatSheet.Binds["Screenshot"]
if len(screenshotBinds) < 1 {
t.Errorf("Expected at least 1 Screenshot bind, got %d", len(screenshotBinds))
}
systemBinds := cheatSheet.Binds["System"]
if len(systemBinds) < 1 {
t.Errorf("Expected at least 1 System bind, got %d", len(systemBinds))
}
}
func TestNiriCategorizeByAction(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
action string
expected string
}{
{"focus-workspace", "Workspace"},
{"focus-workspace-up", "Workspace"},
{"move-column-to-workspace", "Workspace"},
{"focus-monitor-left", "Monitor"},
{"move-column-to-monitor-right", "Monitor"},
{"close-window", "Window"},
{"fullscreen-window", "Window"},
{"maximize-column", "Window"},
{"toggle-window-floating", "Window"},
{"focus-column-left", "Window"},
{"move-column-right", "Window"},
{"spawn", "Execute"},
{"quit", "System"},
{"power-off-monitors", "System"},
{"screenshot", "Screenshot"},
{"screenshot-window", "Screenshot"},
{"toggle-overview", "Overview"},
{"show-hotkey-overlay", "Overview"},
{"next-window", "Alt-Tab"},
{"previous-window", "Alt-Tab"},
{"unknown-action", "Other"},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
result := provider.categorizeByAction(tt.action)
if result != tt.expected {
t.Errorf("categorizeByAction(%q) = %q, want %q", tt.action, result, tt.expected)
}
})
}
}
func TestNiriFormatRawAction(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
action string
args []string
expected string
}{
{"spawn", []string{"kitty"}, "spawn kitty"},
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
{"close-window", nil, "close-window"},
{"fullscreen-window", nil, "fullscreen-window"},
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
{"move-column-to-workspace", []string{"5"}, "move-column-to-workspace 5"},
{"set-column-width", []string{"+10%"}, "set-column-width +10%"},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
result := provider.formatRawAction(tt.action, tt.args)
if result != tt.expected {
t.Errorf("formatRawAction(%q, %v) = %q, want %q", tt.action, tt.args, result, tt.expected)
}
})
}
}
func TestNiriFormatKey(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
mods []string
key string
expected string
}{
{[]string{"Mod"}, "Q", "Mod+Q"},
{[]string{"Mod", "Shift"}, "F", "Mod+Shift+F"},
{[]string{"Ctrl", "Alt"}, "Delete", "Ctrl+Alt+Delete"},
{nil, "Print", "Print"},
{[]string{}, "XF86AudioMute", "XF86AudioMute"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
kb := &NiriKeyBinding{
Mods: tt.mods,
Key: tt.key,
}
result := provider.formatKey(kb)
if result != tt.expected {
t.Errorf("formatKey(%v) = %q, want %q", kb, result, tt.expected)
}
})
}
}
func TestNiriDefaultConfigDir(t *testing.T) {
originalXDG := os.Getenv("XDG_CONFIG_HOME")
defer os.Setenv("XDG_CONFIG_HOME", originalXDG)
os.Setenv("XDG_CONFIG_HOME", "/custom/config")
dir := defaultNiriConfigDir()
if dir != "/custom/config/niri" {
t.Errorf("With XDG_CONFIG_HOME set, got %q, want %q", dir, "/custom/config/niri")
}
os.Unsetenv("XDG_CONFIG_HOME")
dir = defaultNiriConfigDir()
home, _ := os.UserHomeDir()
expected := filepath.Join(home, ".config", "niri")
if dir != expected {
t.Errorf("Without XDG_CONFIG_HOME, got %q, want %q", dir, expected)
}
}
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Shift+Ctrl+D { debug-toggle-damage; }
Super+D { spawn "niri" "msg" "action" "toggle-overview"; }
Super+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
Mod+Shift+E { quit; }
}
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
provider := NewNiriProvider(tmpDir)
cheatSheet, err := provider.GetCheatSheet()
if err != nil {
t.Fatalf("GetCheatSheet failed: %v", err)
}
totalBinds := 0
for _, binds := range cheatSheet.Binds {
totalBinds += len(binds)
}
if totalBinds < 20 {
t.Errorf("Expected at least 20 keybinds, got %d", totalBinds)
}
if len(cheatSheet.Binds["Alt-Tab"]) < 2 {
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
}
}

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
)
type SwayProvider struct {
@@ -26,7 +25,7 @@ func (s *SwayProvider) Name() string {
}
func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := sway.ParseKeys(s.configPath)
section, err := ParseSwayKeys(s.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse sway config: %w", err)
}
@@ -41,7 +40,7 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
}, 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
if 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)
desc := kb.Comment
@@ -100,11 +99,12 @@ func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) k
return keybinds.Keybind{
Key: key,
Description: desc,
Action: kb.Command,
Subcategory: subcategory,
}
}
func (s *SwayProvider) formatKey(kb *sway.KeyBinding) string {
func (s *SwayProvider) formatKey(kb *SwayKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)

View File

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

View File

@@ -1,4 +1,4 @@
package sway
package providers
import (
"os"
@@ -6,7 +6,7 @@ import (
"testing"
)
func TestAutogenerateComment(t *testing.T) {
func TestSwayAutogenerateComment(t *testing.T) {
tests := []struct {
command string
expected string
@@ -46,25 +46,25 @@ func TestAutogenerateComment(t *testing.T) {
for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) {
result := autogenerateComment(tt.command)
result := swayAutogenerateComment(tt.command)
if result != tt.expected {
t.Errorf("autogenerateComment(%q) = %q, want %q",
t.Errorf("swayAutogenerateComment(%q) = %q, want %q",
tt.command, result, tt.expected)
}
})
}
}
func TestGetKeybindAtLine(t *testing.T) {
func TestSwayGetKeybindAtLine(t *testing.T) {
tests := []struct {
name string
line string
expected *KeyBinding
expected *SwayKeyBinding
}{
{
name: "basic_keybind",
line: "bindsym Mod4+q kill",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "q",
Command: "kill",
@@ -74,7 +74,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_with_exec",
line: "bindsym Mod4+t exec kitty",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "t",
Command: "exec kitty",
@@ -84,7 +84,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_with_comment",
line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "Space",
Command: "exec dms ipc call spotlight toggle",
@@ -99,7 +99,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_multiple_mods",
line: "bindsym Mod4+Shift+e exit",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4", "Shift"},
Key: "e",
Command: "exit",
@@ -109,7 +109,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_no_mods",
line: "bindsym Print exec grim screenshot.png",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{},
Key: "Print",
Command: "exec grim screenshot.png",
@@ -119,7 +119,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_with_flags",
line: "bindsym --release Mod4+x exec notify-send released",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "x",
Command: "exec notify-send released",
@@ -129,7 +129,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_focus_direction",
line: "bindsym Mod4+Left focus left",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "Left",
Command: "focus left",
@@ -139,7 +139,7 @@ func TestGetKeybindAtLine(t *testing.T) {
{
name: "keybind_workspace",
line: "bindsym Mod4+1 workspace number 1",
expected: &KeyBinding{
expected: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "1",
Command: "workspace number 1",
@@ -150,7 +150,7 @@ func TestGetKeybindAtLine(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser()
parser := NewSwayParser()
parser.contentLines = []string{tt.line}
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()
configFile := filepath.Join(tmpDir, "config")
@@ -204,9 +204,9 @@ bindsym $mod+d exec $menu
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(configFile)
section, err := ParseSwayKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
t.Fatalf("ParseSwayKeys failed: %v", err)
}
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()
configFile := filepath.Join(tmpDir, "config")
@@ -251,9 +251,9 @@ bindsym $mod+t exec kitty # Terminal
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(tmpDir)
section, err := ParseSwayKeys(tmpDir)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
t.Fatalf("ParseSwayKeys failed: %v", err)
}
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 {
name string
path string
@@ -313,7 +313,7 @@ func TestReadContentErrors(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseKeys(tt.path)
_, err := ParseSwayKeys(tt.path)
if err == 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()
if err != nil {
t.Skip("Cannot get home directory")
@@ -343,7 +343,7 @@ func TestReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path")
}
parser := NewParser()
parser := NewSwayParser()
tildePathMatch := "~/" + relPath
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()
configFile := filepath.Join(tmpDir, "config")
@@ -369,9 +369,9 @@ bindsym Mod4+t exec kitty
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(configFile)
section, err := ParseSwayKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
t.Fatalf("ParseSwayKeys failed: %v", err)
}
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()
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)
}
section, err := ParseKeys(configFile)
section, err := ParseSwayKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
t.Fatalf("ParseSwayKeys failed: %v", err)
}
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 {
input string
expected bool
@@ -462,9 +462,9 @@ func TestIsMod(t *testing.T) {
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := isMod(tt.input)
result := swayIsMod(tt.input)
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"
"path/filepath"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
)
func TestSwayProviderName(t *testing.T) {
@@ -76,12 +74,12 @@ func TestSwayCategorizeByCommand(t *testing.T) {
func TestSwayFormatKey(t *testing.T) {
tests := []struct {
name string
keybind *sway.KeyBinding
keybind *SwayKeyBinding
expected string
}{
{
name: "single_mod",
keybind: &sway.KeyBinding{
keybind: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "q",
},
@@ -89,7 +87,7 @@ func TestSwayFormatKey(t *testing.T) {
},
{
name: "multiple_mods",
keybind: &sway.KeyBinding{
keybind: &SwayKeyBinding{
Mods: []string{"Mod4", "Shift"},
Key: "e",
},
@@ -97,7 +95,7 @@ func TestSwayFormatKey(t *testing.T) {
},
{
name: "no_mods",
keybind: &sway.KeyBinding{
keybind: &SwayKeyBinding{
Mods: []string{},
Key: "Print",
},
@@ -119,13 +117,13 @@ func TestSwayFormatKey(t *testing.T) {
func TestSwayConvertKeybind(t *testing.T) {
tests := []struct {
name string
keybind *sway.KeyBinding
keybind *SwayKeyBinding
wantKey string
wantDesc string
}{
{
name: "with_comment",
keybind: &sway.KeyBinding{
keybind: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "t",
Command: "exec kitty",
@@ -136,7 +134,7 @@ func TestSwayConvertKeybind(t *testing.T) {
},
{
name: "without_comment",
keybind: &sway.KeyBinding{
keybind: &SwayKeyBinding{
Mods: []string{"Mod4"},
Key: "r",
Command: "reload",

View File

@@ -3,6 +3,7 @@ package keybinds
type Keybind struct {
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"`
}

View File

@@ -1,4 +1,4 @@
package logger
package log
import (
"bufio"
@@ -64,7 +64,7 @@ func (l *FileLogger) writeToFile(message string) {
redacted := l.redactPassword(message)
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()
}
@@ -93,7 +93,7 @@ func (l *FileLogger) Close() error {
defer l.mu.Unlock()
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()
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}
}
// 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
func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error {
ret := _m.Called(printer, purge)
@@ -116,6 +209,312 @@ func (_c *MockCUPSClientInterface_CancelJob_Call) RunAndReturn(run func(int, boo
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
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)
@@ -180,6 +579,63 @@ func (_c *MockCUPSClientInterface_GetJobs_Call) RunAndReturn(run func(string, st
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
func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) {
ret := _m.Called(attributes)
@@ -238,6 +694,100 @@ func (_c *MockCUPSClientInterface_GetPrinters_Call) RunAndReturn(run func([]stri
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
func (_m *MockCUPSClientInterface) PausePrinter(printer string) error {
ret := _m.Called(printer)
@@ -284,6 +834,156 @@ func (_c *MockCUPSClientInterface_PausePrinter_Call) RunAndReturn(run func(strin
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
func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error {
ret := _m.Called(printer)
@@ -390,6 +1090,147 @@ func (_c *MockCUPSClientInterface_SendRequest_Call) RunAndReturn(run func(string
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.
// The first argument is typically a *testing.T value.
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
}
// 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
func (_m *MockBackend) DisconnectAllVPN() error {
ret := _m.Called()
@@ -418,6 +464,52 @@ func (_c *MockBackend_DisconnectEthernet_Call) RunAndReturn(run func() error) *M
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
func (_m *MockBackend) DisconnectVPN(uuidOrName string) error {
ret := _m.Called(uuidOrName)
@@ -509,6 +601,52 @@ func (_c *MockBackend_DisconnectWiFi_Call) RunAndReturn(run func() error) *MockB
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
func (_m *MockBackend) ForgetWiFiNetwork(ssid string) error {
ret := _m.Called(ssid)
@@ -612,6 +750,53 @@ func (_c *MockBackend_GetCurrentState_Call) RunAndReturn(run func() (*network.Ba
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
func (_m *MockBackend) GetPromptBroker() network.PromptBroker {
ret := _m.Called()
@@ -659,6 +844,111 @@ func (_c *MockBackend_GetPromptBroker_Call) RunAndReturn(run func() network.Prom
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
func (_m *MockBackend) GetWiFiEnabled() (bool, error) {
ret := _m.Called()
@@ -887,6 +1177,65 @@ func (_c *MockBackend_GetWiredNetworkDetails_Call) RunAndReturn(run func(string)
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
func (_m *MockBackend) Initialize() error {
ret := _m.Called()
@@ -989,6 +1338,63 @@ func (_c *MockBackend_ListActiveVPN_Call) RunAndReturn(run func() ([]network.VPN
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
func (_m *MockBackend) ListVPNProfiles() ([]network.VPNProfile, error) {
ret := _m.Called()
@@ -1091,6 +1497,52 @@ func (_c *MockBackend_ScanWiFi_Call) RunAndReturn(run func() error) *MockBackend
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
func (_m *MockBackend) SetPromptBroker(broker network.PromptBroker) error {
ret := _m.Called(broker)
@@ -1137,6 +1589,55 @@ func (_c *MockBackend_SetPromptBroker_Call) RunAndReturn(run func(network.Prompt
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
func (_m *MockBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
ret := _m.Called(ssid, autoconnect)
@@ -1356,6 +1857,53 @@ func (_c *MockBackend_SubmitCredentials_Call) RunAndReturn(run func(string, map[
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.
// The first argument is typically a *testing.T value.
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 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)
}
} else {
@@ -130,7 +130,7 @@ func (m *Manager) Install(plugin Plugin) error {
}
} else {
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)
}
}

View File

@@ -1,12 +1,12 @@
// 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
//
// dwl_ipc_unstable_v2 Protocol Copyright:
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].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
@@ -157,6 +157,16 @@ type ZdwlIpcOutputV2 struct {
appidHandler ZdwlIpcOutputV2AppidHandlerFunc
layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc
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
@@ -251,6 +261,60 @@ func (i *ZdwlIpcOutputV2) SetLayout(index uint32) error {
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
// ZdwlIpcOutputV2TagState :
@@ -399,6 +463,136 @@ func (i *ZdwlIpcOutputV2) SetFrameHandler(f ZdwlIpcOutputV2FrameHandlerFunc) {
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) {
switch opcode {
case 0:
@@ -487,5 +681,111 @@ func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
var e ZdwlIpcOutputV2FrameEvent
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
// 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
//
// ext_workspace_v1 Protocol Copyright:
@@ -35,7 +35,8 @@ import (
"reflect"
"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.
@@ -61,8 +62,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
return
}
objectsMap := reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem()
objectsMap.SetMapIndex(reflect.ValueOf(serverID), reflect.ValueOf(proxy))
objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
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].

View File

@@ -1,5 +1,5 @@
// 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
//
// wlr_gamma_control_unstable_v1 Protocol Copyright:
@@ -31,7 +31,7 @@
package wlr_gamma_control
import (
"github.com/yaslama/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)

View File

@@ -1,5 +1,5 @@
// 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
//
// wlr_output_management_unstable_v1 Protocol Copyright:
@@ -33,7 +33,8 @@ import (
"reflect"
"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) {
@@ -47,9 +48,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
if !objectsField.IsValid() {
return
}
objectsField = reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem()
objectsMap := objectsField.Interface().(map[uint32]client.Proxy)
objectsMap[serverID] = proxy
objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap.Store(serverID, proxy)
}
// 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.
</description>
<interface name="zdwl_ipc_manager_v2" version="1">
<interface name="zdwl_ipc_manager_v2" version="2">
<description summary="manage dwl state">
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>
</interface>
<interface name="zdwl_ipc_output_v2" version="1">
<interface name="zdwl_ipc_output_v2" version="2">
<description summary="control 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"/>
<arg name="index" type="uint" summary="index of a layout recieved by dwl_ipc_manager.layout"/>
</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>
</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

@@ -0,0 +1,64 @@
package apppicker
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type Request struct {
ID int `json:"id"`
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "apppicker.open", "browser.open":
handleOpen(conn, req, manager)
default:
models.RespondError(conn, req.ID, "unknown method")
}
}
func handleOpen(conn net.Conn, req Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := req.Params["target"].(string)
if !ok {
target, ok = req.Params["url"].(string)
if !ok {
log.Warnf("AppPicker: Invalid target parameter in request")
models.RespondError(conn, req.ID, "invalid target parameter")
return
}
}
event := OpenEvent{
Target: target,
RequestType: "url",
}
if mimeType, ok := req.Params["mimeType"].(string); ok {
event.MimeType = mimeType
}
if categories, ok := req.Params["categories"].([]interface{}); ok {
event.Categories = make([]string, 0, len(categories))
for _, cat := range categories {
if catStr, ok := cat.(string); ok {
event.Categories = append(event.Categories, catStr)
}
}
}
if requestType, ok := req.Params["requestType"].(string); ok {
event.RequestType = requestType
}
log.Infof("AppPicker: Broadcasting event: %+v", event)
manager.RequestOpen(event)
models.Respond(conn, req.ID, "ok")
log.Infof("AppPicker: Request handled successfully")
}

View File

@@ -0,0 +1,48 @@
package apppicker
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Manager struct {
subscribers syncmap.Map[string, chan OpenEvent]
closeOnce sync.Once
}
func NewManager() *Manager {
return &Manager{}
}
func (m *Manager) Subscribe(id string) chan OpenEvent {
ch := make(chan OpenEvent, 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) RequestOpen(event OpenEvent) {
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
select {
case ch <- event:
default:
}
return true
})
}
func (m *Manager) Close() {
m.closeOnce.Do(func() {
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
})
}

View File

@@ -0,0 +1,8 @@
package apppicker
type OpenEvent struct {
Target string `json:"target"`
MimeType string `json:"mimeType,omitempty"`
Categories []string `json:"categories,omitempty"`
RequestType string `json:"requestType"`
}

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)
if entered == 0 {
pk := passkey
_, err := a.promptFor(device, "display-passkey", []string{}, nil)
passkeyStr := strconv.FormatUint(uint64(passkey), 10)
_, err := a.promptFor(device, "display-passkey", []string{}, &passkeyStr)
if err != nil {
log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err)
}
_ = pk
}
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 {
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 {
log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err)
return a.errorFrom(err)

View File

@@ -30,17 +30,13 @@ func NewManager() (*Manager, error) {
PairedDevices: []Device{},
ConnectedDevices: []Device{},
},
stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan BluetoothState),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dbusConn: conn,
signals: make(chan *dbus.Signal, 256),
pairingSubscribers: make(map[string]chan PairingPrompt),
pairingSubMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
pendingPairings: make(map[string]bool),
eventQueue: make(chan func(), 32),
stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dbusConn: conn,
signals: make(chan *dbus.Signal, 256),
dirty: make(chan struct{}, 1),
eventQueue: make(chan func(), 32),
}
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
@@ -358,26 +354,25 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
_, hasTrusted := changed["Trusted"]
if hasPaired {
if paired, ok := pairedVar.Value().(bool); ok && paired {
devicePath := string(path)
m.pendingPairingsMux.Lock()
wasPending := m.pendingPairings[devicePath]
if wasPending {
delete(m.pendingPairings, devicePath)
}
m.pendingPairingsMux.Unlock()
devicePath := string(path)
if paired, ok := pairedVar.Value().(bool); ok {
if paired {
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
if wasPending {
select {
case m.eventQueue <- func() {
time.Sleep(300 * time.Millisecond)
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
if err := m.ConnectDevice(devicePath); err != nil {
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
if wasPending {
select {
case m.eventQueue <- func() {
time.Sleep(300 * time.Millisecond)
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
if err := m.ConnectDevice(devicePath); err != nil {
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
}
}:
default:
}
}:
default:
}
} else {
m.pendingPairings.Delete(devicePath)
}
}
}
@@ -430,28 +425,20 @@ func (m *Manager) notifier() {
}
m.updateDevices()
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false
continue
}
for _, ch := range m.subscribers {
m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
select {
case ch <- currentState:
default:
}
}
m.subMutex.RUnlock()
return true
})
stateCopy := currentState
m.lastNotifiedState = &stateCopy
@@ -484,48 +471,36 @@ func (m *Manager) snapshotState() BluetoothState {
func (m *Manager) Subscribe(id string) chan BluetoothState {
ch := make(chan BluetoothState, 64)
m.subMutex.Lock()
m.subscribers[id] = ch
m.subMutex.Unlock()
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch)
delete(m.subscribers, id)
}
m.subMutex.Unlock()
}
func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
ch := make(chan PairingPrompt, 16)
m.pairingSubMutex.Lock()
m.pairingSubscribers[id] = ch
m.pairingSubMutex.Unlock()
m.pairingSubscribers.Store(id, ch)
return ch
}
func (m *Manager) UnsubscribePairing(id string) {
m.pairingSubMutex.Lock()
if ch, ok := m.pairingSubscribers[id]; ok {
if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok {
close(ch)
delete(m.pairingSubscribers, id)
}
m.pairingSubMutex.Unlock()
}
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
m.pairingSubMutex.RLock()
defer m.pairingSubMutex.RUnlock()
for _, ch := range m.pairingSubscribers {
m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
select {
case ch <- prompt:
default:
}
}
return true
})
}
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 {
m.pendingPairingsMux.Lock()
m.pendingPairings[devicePath] = true
m.pendingPairingsMux.Unlock()
m.pendingPairings.Store(devicePath, true)
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
err := obj.Call(device1Iface+".Pair", 0).Err
if err != nil {
m.pendingPairingsMux.Lock()
delete(m.pendingPairings, devicePath)
m.pendingPairingsMux.Unlock()
m.pendingPairings.Delete(devicePath)
}
return err
@@ -618,19 +589,17 @@ func (m *Manager) Close() {
m.agent.Close()
}
m.subMutex.Lock()
for _, ch := range m.subscribers {
m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
close(ch)
}
m.subscribers = make(map[string]chan BluetoothState)
m.subMutex.Unlock()
m.subscribers.Delete(key)
return true
})
m.pairingSubMutex.Lock()
for _, ch := range m.pairingSubscribers {
m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
close(ch)
}
m.pairingSubscribers = make(map[string]chan PairingPrompt)
m.pairingSubMutex.Unlock()
m.pairingSubscribers.Delete(key)
return true
})
if m.dbusConn != nil {
m.dbusConn.Close()

View File

@@ -3,22 +3,19 @@ package bluez
import (
"context"
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type SubscriptionBroker struct {
mu sync.RWMutex
pending map[string]chan PromptReply
requests map[string]PromptRequest
pending syncmap.Map[string, chan PromptReply]
requests syncmap.Map[string, PromptRequest]
broadcastPrompt func(PairingPrompt)
}
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
return &SubscriptionBroker{
pending: make(map[string]chan PromptReply),
requests: make(map[string]PromptRequest),
broadcastPrompt: broadcastPrompt,
}
}
@@ -30,10 +27,8 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
}
replyChan := make(chan PromptReply, 1)
b.mu.Lock()
b.pending[token] = replyChan
b.requests[token] = req
b.mu.Unlock()
b.pending.Store(token, replyChan)
b.requests.Store(token, req)
if b.broadcastPrompt != nil {
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) {
b.mu.RLock()
replyChan, exists := b.pending[token]
b.mu.RUnlock()
replyChan, exists := b.pending.Load(token)
if !exists {
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 {
b.mu.RLock()
replyChan, exists := b.pending[token]
b.mu.RUnlock()
replyChan, exists := b.pending.Load(token)
if !exists {
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) {
b.mu.Lock()
delete(b.pending, token)
delete(b.requests, token)
b.mu.Unlock()
b.pending.Delete(token)
b.requests.Delete(token)
}

View File

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

View File

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

View File

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

View File

@@ -15,10 +15,8 @@ func NewManager() (*Manager, error) {
func NewManagerWithOptions(exponential bool) (*Manager, error) {
m := &Manager{
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
exponential: exponential,
stopChan: make(chan struct{}),
exponential: exponential,
}
go m.initLogind()
@@ -360,20 +358,13 @@ func (m *Manager) broadcastDeviceUpdate(deviceID string) {
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)
for _, ch := range m.updateSubscribers {
m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
select {
case ch <- update:
default:
}
}
return true
})
}

View File

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

View File

@@ -31,9 +31,8 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -41,13 +40,11 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
}
m := &Manager{
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{
@@ -105,9 +102,8 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -115,13 +111,11 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
}
m := &Manager{
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{
@@ -175,9 +169,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
}
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -185,13 +178,11 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
}
m := &Manager{
logindBackend: nil,
sysfsBackend: sysfs,
logindReady: false,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: nil,
sysfsBackend: sysfs,
logindReady: false,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{
@@ -240,9 +231,8 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"leds"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"leds"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -250,13 +240,11 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
}
m := &Manager{
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{

View File

@@ -160,26 +160,21 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
}
b := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight", "leds"},
}
if err := b.scanDevices(); err != nil {
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"
if _, ok := b.deviceCache[backlightID]; !ok {
if _, ok := b.deviceCache.Load(backlightID); !ok {
t.Errorf("backlight device not found")
}
ledID := "leds:test_led"
if _, ok := b.deviceCache[ledID]; !ok {
if _, ok := b.deviceCache.Load(ledID); !ok {
t.Errorf("LED device not found")
}
}

View File

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

View File

@@ -0,0 +1,28 @@
package browser
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type Request struct {
ID int `json:"id"`
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
}
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
switch req.Method {
case "browser.open":
url, ok := req.Params["url"].(string)
if !ok {
models.RespondError(conn, req.ID, "invalid url parameter")
return
}
manager.RequestOpen(url)
models.Respond(conn, req.ID, "ok")
default:
models.RespondError(conn, req.ID, "unknown method")
}
}

View File

@@ -0,0 +1,49 @@
package browser
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Manager struct {
subscribers syncmap.Map[string, chan OpenEvent]
closeOnce sync.Once
}
func NewManager() *Manager {
return &Manager{}
}
func (m *Manager) Subscribe(id string) chan OpenEvent {
ch := make(chan OpenEvent, 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) RequestOpen(url string) {
event := OpenEvent{URL: url}
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
select {
case ch <- event:
default:
}
return true
})
}
func (m *Manager) Close() {
m.closeOnce.Do(func() {
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
})
}

View File

@@ -0,0 +1,5 @@
package browser
type OpenEvent struct {
URL string `json:"url"`
}

View File

@@ -1,12 +1,34 @@
package cups
import (
"errors"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"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) {
attributes := []string{
ipp.AttributePrinterName,
@@ -21,6 +43,9 @@ func (m *Manager) GetPrinters() ([]Printer, error) {
printerAttrs, err := m.client.GetPrinters(attributes)
if err != nil {
if isNoPrintersError(err) {
return []Printer{}, nil
}
return nil, err
}
@@ -91,17 +116,289 @@ func (m *Manager) GetJobs(printerName string, whichJobs string) ([]Job, 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 {
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 {
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 {
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) {
tests := []struct {
name string
mockErr error
wantErr bool
}{
{
name: "success",
mockErr: nil,
wantErr: false,
},
{
name: "error",
mockErr: errors.New("test error"),
wantErr: true,
},
}
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.CancelJob(1)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.CancelJob(1))
}
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,
},
}
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.PausePrinter("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.PausePrinter("printer1"))
}
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,
},
}
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(tt.mockErr)
m := &Manager{
client: mockClient,
}
err := m.ResumePrinter("printer1")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
m := NewTestManager(mockClient, nil)
assert.NoError(t, m.ResumePrinter("printer1"))
}
func TestManager_PurgeJobs(t *testing.T) {
@@ -269,11 +185,12 @@ func TestManager_PurgeJobs(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr)
m := &Manager{
client: mockClient,
if !tt.wantErr {
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
}
m := NewTestManager(mockClient, nil)
err := m.PurgeJobs("printer1")
if tt.wantErr {
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)
case "cups.purgeJobs":
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:
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) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().PausePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{
client: mockClient,
}
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
@@ -173,10 +172,9 @@ func TestHandlePausePrinter(t *testing.T) {
func TestHandleResumePrinter(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().ResumePrinter("printer1").Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{
client: mockClient,
}
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
@@ -201,10 +199,9 @@ func TestHandleResumePrinter(t *testing.T) {
func TestHandleCancelJob(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelJob(1, false).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{
client: mockClient,
}
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
@@ -229,10 +226,9 @@ func TestHandleCancelJob(t *testing.T) {
func TestHandlePurgeJobs(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil)
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil)
m := &Manager{
client: mockClient,
}
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
@@ -277,3 +273,439 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
assert.Nil(t, resp.Result)
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
import (
"errors"
"fmt"
"os"
"strconv"
@@ -31,17 +32,25 @@ func NewManager() (*Manager, error) {
client := ipp.NewCUPSClient(host, port, username, password, false)
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{
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: client,
baseURL: baseURL,
stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
subMutex: sync.RWMutex{},
client: client,
pkHelper: pkHelper,
baseURL: baseURL,
stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
if err := m.updateState(); err != nil {
@@ -100,6 +109,12 @@ func (m *Manager) eventHandler() {
func (m *Manager) updateState() error {
printers, err := m.GetPrinters()
if err != nil {
if isNoPrintersError(err) {
m.stateMutex.Lock()
m.state.Printers = make(map[string]*Printer)
m.stateMutex.Unlock()
return nil
}
return err
}
@@ -121,6 +136,19 @@ func (m *Manager) updateState() error {
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() {
defer m.notifierWg.Done()
const minGap = 100 * time.Millisecond
@@ -142,28 +170,21 @@ func (m *Manager) notifier() {
if !pending {
continue
}
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false
continue
}
for _, ch := range m.subscribers {
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
select {
case ch <- currentState:
default:
}
}
m.subMutex.RUnlock()
return true
})
stateCopy := currentState
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 {
return m.snapshotState()
}
@@ -199,10 +228,14 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64)
m.subMutex.Lock()
wasEmpty := len(m.subscribers) == 0
m.subscribers[id] = ch
m.subMutex.Unlock()
wasEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
wasEmpty = false
return false
})
m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != 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) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
close(ch)
delete(m.subscribers, id)
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
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 {
m.subscription.Stop()
@@ -241,12 +276,11 @@ func (m *Manager) Close() {
m.eventWG.Wait()
m.notifierWg.Wait()
m.subMutex.Lock()
for _, ch := range m.subscribers {
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
close(ch)
}
m.subscribers = make(map[string]chan CUPSState)
m.subMutex.Unlock()
m.subscribers.Delete(key)
return true
})
}
func stateChanged(old, new *CUPSState) bool {
@@ -260,6 +294,7 @@ func stateChanged(old, new *CUPSState) bool {
}
if oldPrinter.State != newPrinter.State ||
oldPrinter.StateReason != newPrinter.StateReason ||
oldPrinter.Accepting != newPrinter.Accepting ||
len(oldPrinter.Jobs) != len(newPrinter.Jobs) {
return true
}
@@ -338,3 +373,18 @@ func getBoolAttr(attrs ipp.Attributes, key string) bool {
}
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

@@ -13,10 +13,9 @@ func TestNewManager(t *testing.T) {
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: nil,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: nil,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
assert.NotNil(t, m)
@@ -35,10 +34,9 @@ func TestManager_GetState(t *testing.T) {
},
},
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
state := m.GetState()
@@ -53,18 +51,28 @@ func TestManager_Subscribe(t *testing.T) {
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
ch := m.Subscribe("test-client")
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")
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) {
@@ -74,10 +82,9 @@ func TestManager_Close(t *testing.T) {
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
m.eventWG.Add(1)
@@ -93,7 +100,12 @@ func TestManager_Close(t *testing.T) {
}()
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) {

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
}

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