mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
528 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc307aff98 | |||
| cee7957e15 | |||
| 602697ef51 | |||
| ce548cb29c | |||
| 61510c2186 | |||
| 237fb57d5e | |||
| fe561f2b5d | |||
| 1c01254e9b | |||
| 958672badd | |||
| 93c3030a1e | |||
| 53c729f685 | |||
| e0480bf383 | |||
| e63d210358 | |||
| 30d9a34011 | |||
| f0bdaa7f72 | |||
| fa6c36ad57 | |||
| 5de0339506 | |||
| 814871d982 | |||
| 68e60f036d | |||
| d185135dd7 | |||
| 70ceee139b | |||
| e58b41498c | |||
| 116cef0b3a | |||
| dc75f1f01d | |||
| 367ad5f69a | |||
| 8577c6b815 | |||
| 3d9374d2b2 | |||
| e300b14dde | |||
| dc4916b9e0 | |||
| fb65a23fac | |||
| a5b5ed7e51 | |||
| dfbbfe1793 | |||
| a69acf171a | |||
| 1402d2312a | |||
| d003683689 | |||
| 57fd39d84d | |||
| 6239e5a8e4 | |||
| 4c59d61f89 | |||
| 2a8b88c1f0 | |||
| dbaf2e922c | |||
| eb330f62e1 | |||
| 90291bd627 | |||
| 069df80b22 | |||
| 2846363f55 | |||
| 0cdb065739 | |||
| 2114ece0df | |||
| 4a90bd3fb1 | |||
| 698b66cf91 | |||
| 8fc38de0a1 | |||
| 235f0668b8 | |||
| 5dfd875b9e | |||
| 00b22df802 | |||
| b22b1bef9b | |||
| 5cae48a85d | |||
| 4200270b55 | |||
| c2c180d304 | |||
| 3938e60ce4 | |||
| 2986e354e8 | |||
| bf12665adb | |||
| f8fe16738e | |||
| 3c688cfbd3 | |||
| 92ba2bb57d | |||
| 504c132f78 | |||
| bdaa6bcc9d | |||
| a50bb2cab3 | |||
| b17a90a412 | |||
| 0ffec5011b | |||
| adbbd9b112 | |||
| 95e1d6b72e | |||
| 4cb09dcc1b | |||
| ed44d02202 | |||
| 4352f49ae3 | |||
| 9ddea82063 | |||
| 3c0f2cbc48 | |||
| 6c45413d7a | |||
| 149e1e64b1 | |||
| 5589cfb783 | |||
| ff71502083 | |||
| 1ef5371c3b | |||
| 3951cd6b18 | |||
| 9686f29246 | |||
| 6d206c9521 | |||
| 48e36ae146 | |||
| 7c24e85c19 | |||
| fe1a783ec2 | |||
| 358496134c | |||
| dc924618fb | |||
| fb2dbced08 | |||
| cdaedad969 | |||
| bf408f8d00 | |||
| b169fe0d77 | |||
| 9b67cedaa1 | |||
| 2edf70a144 | |||
| 72a71cacba | |||
| 0440e40a82 | |||
| ec27d4643c | |||
| f18d36f6c2 | |||
| 3254cc6a1e | |||
| 4af3225f21 | |||
| 3c5245914f | |||
| 89814a2c65 | |||
| 5d2093e54a | |||
| 3cd52ca327 | |||
| 5b41d699fa | |||
| 544599bf1c | |||
| 296b3a3d7e | |||
| 729a990fa7 | |||
| c3fa7b2e1d | |||
| 2c5a1a2804 | |||
| 1973526c4e | |||
| bba5502960 | |||
| 52740290b2 | |||
| 27703575bc | |||
| e6504add7b | |||
| f1e9121295 | |||
| 9ff751b82a | |||
| 21eaaef056 | |||
| bb0be2b215 | |||
| 25847c3f03 | |||
| 7b5c25c50f | |||
| 7535b70fa6 | |||
| e3034e4e94 | |||
| 3da19e5c15 | |||
| ca89e12963 | |||
| 4fb6995796 | |||
| 3f5a54aa88 | |||
| d379d251b9 | |||
| 7ab0e01573 | |||
| 2cb48aaf6b | |||
| e4657aa5f9 | |||
| 197d17ac4e | |||
| 31ea83584b | |||
| ea66b136ba | |||
| 06c0ea2afb | |||
| f4f47c0bc5 | |||
| 8a0ed8a50f | |||
| 63eea01243 | |||
| f590a2965a | |||
| 4ab03deded | |||
| 846d07d86a | |||
| 55d88d8dfb | |||
| a803f96f41 | |||
| da7cc5648b | |||
| 75443758f9 | |||
| eaecff93a4 | |||
| 6e7c20c59c | |||
| 406dcfe110 | |||
| e2b3a2e3ca | |||
| 71ab752e1b | |||
| 204ecd0461 | |||
| 0fdfac366e | |||
| 52123458c7 | |||
| 26b2955cf3 | |||
| d82d86df5c | |||
| c445597f83 | |||
| 6a58adfb29 | |||
| 05feb211ba | |||
| a3b2167e58 | |||
| 0b69feaa1a | |||
| 494144a7c7 | |||
| 2a2c1ca9e6 | |||
| 56b7ecb008 | |||
| 867102b82c | |||
| 4bdb7d17b2 | |||
| c44ffae751 | |||
| ce1595d62d | |||
| 45f6232e32 | |||
| 9cf2ca7196 | |||
| c0eeed4e89 | |||
| d0a4c1c56e | |||
| cb0dc9c68d | |||
| 0439d017b9 | |||
| 8008238ca0 | |||
| a095d0ed90 | |||
| a48cce59d4 | |||
| 4c806f83f1 | |||
| bdfd565b72 | |||
| f3b698590c | |||
| 7826d42ce4 | |||
| 6cc30c289b | |||
| 43cc8e69d1 | |||
| a62ae336e0 | |||
| e94af2a7aa | |||
| bb9e69e0c1 | |||
| 1cc9218ff6 | |||
| 56712f46fa | |||
| 93168ee073 | |||
| 1724aedd49 | |||
| d799175c07 | |||
| ee6f7b4798 | |||
| 0511cd19df | |||
| 71b1901ab0 | |||
| 32e2d96e55 | |||
| 647766b9fa | |||
| 0cd8974110 | |||
| 0c4c5fc146 | |||
| b447e16374 | |||
| 0bb8353a33 | |||
| 19a7dcf17d | |||
| 8a1acb63c9 | |||
| 6f298d3f52 | |||
| 3e481a566b | |||
| 405ea708b3 | |||
| cc0bec2682 | |||
| 240adfa28f | |||
| c175f4823a | |||
| 35613ebba8 | |||
| 583e46f2f0 | |||
| 590555dfc6 | |||
| c11169eb4c | |||
| bddcdb3f99 | |||
| 6ec67cefd0 | |||
| 8d9d7ff0ef | |||
| acc39ceb16 | |||
| 43863a86fb | |||
| 36ad34a555 | |||
| 78e823e23a | |||
| b6a27dc713 | |||
| 79dfd34ca2 | |||
| eedba0e8c9 | |||
| 2b12292895 | |||
| dd7f4f3ac5 | |||
| 3df801e3b2 | |||
| 74d220767d | |||
| a3fba8fcb6 | |||
| 33384e4d5d | |||
| 78f2ea4642 | |||
| 7dffe85e94 | |||
| c5c3105469 | |||
| 8f100960cf | |||
| 8a5e7c78fb | |||
| 8a85ab06a1 | |||
| 30ceda97a0 | |||
| fefc0afa6f | |||
| b01042d530 | |||
| 7355cc54b2 | |||
| 187b7ba8ff | |||
| c0e7c1b2b4 | |||
| ea0c235d6e | |||
| 57a04cf409 | |||
| fd99558ce5 | |||
| 81a4d3b4e0 | |||
| bfda5f2d66 | |||
| 65c4167ab2 | |||
| e7bd8e4002 | |||
| 25a1b125b7 | |||
| d766c0773c | |||
| 5d12962fd6 | |||
| 9911698237 | |||
| 7e095415a7 | |||
| 68d5d7c5ed | |||
| 7b1c542585 | |||
| 52ed719489 | |||
| 6e508d416d | |||
| 518cc93276 | |||
| 8cb4e21716 | |||
| 19b44b1782 | |||
| 0509694d78 | |||
| da44e07cf7 | |||
| b34941e3b8 | |||
| c554d973ef | |||
| 2861cc89c6 | |||
| 1c5965f1e4 | |||
| 66d552b3d5 | |||
| ebad8e2dda | |||
| 8dadf73d52 | |||
| 9f77b3127c | |||
| 46f8457777 | |||
| 8465ed4311 | |||
| 6e3e178721 | |||
| 27230227ef | |||
| 7178563c2c | |||
| dfe309a543 | |||
| 04962ffed1 | |||
| 5008406be8 | |||
| 930a36bcfc | |||
| 3253adb6dd | |||
| b8fd52e00f | |||
| 8e7126472e | |||
| 61108b7668 | |||
| 09ca37c9fc | |||
| a2513e939a | |||
| a73380b310 | |||
| 69d7c1fb13 | |||
| d1eb655e46 | |||
| a3ecef7627 | |||
| 186621ab81 | |||
| 31efe9b385 | |||
| 6cdb891551 | |||
| 3bb3da228c | |||
| 0c99e4b8d1 | |||
| 8b79d1dad3 | |||
| 85fa3326c1 | |||
| 2fade1a617 | |||
| 597ba597e5 | |||
| 957020c9ea | |||
| ae520620ac | |||
| 4c43bed2f8 | |||
| 2663bcd96c | |||
| e423e17807 | |||
| 70fa0ec903 | |||
| db3615805c | |||
| 8162e60475 | |||
| 5594ec27ff | |||
| ca050ace9c | |||
| 093acdbff7 | |||
| a98417d66b | |||
| 83b9cdcb27 | |||
| 4ae8ef927c | |||
| 2128e6f14e | |||
| 9ff085c46a | |||
| 57d08f6b3b | |||
| 7964b66827 | |||
| 38a03a4485 | |||
| a1727d18fd | |||
| aabcef0c9c | |||
| 19406e99b9 | |||
| 5432d264f1 | |||
| d704a0ba3d | |||
| 161118122e | |||
| 9d550d7c11 | |||
| e40faaa442 | |||
| 894efa58a3 | |||
| 50570023ff | |||
| f293def3a8 | |||
| de3e8cffa0 | |||
| a874f76d72 | |||
| 424d4ed701 | |||
| 8f2a50c00e | |||
| 46cddb965f | |||
| 1ca64abad9 | |||
| 32d68a034f | |||
| 1a176920d4 | |||
| afa6fd9e2e | |||
| 6bee1b2c86 | |||
| 48f6a0c632 | |||
| 03d86f78f4 | |||
| 0bc89429bc | |||
| dd3eb71485 | |||
| 016527fda9 | |||
| d1f833fbbb | |||
| fd65a8cea9 | |||
| bcd6006e25 | |||
| cde0d83620 | |||
| 92994061da | |||
| 1e7ee25e13 | |||
| 838d21aae8 | |||
| 2fc3b8ee4a | |||
| 52834c6a35 | |||
| 1a39b7f66c | |||
| aea5189abb | |||
| 774796ada2 | |||
| 14f21b6c6d | |||
| 21ddc3cc3f | |||
| 8b94b149b2 | |||
| 99cc3b8449 | |||
| b2e728315b | |||
| bed11feaa4 | |||
| 28f40afccf | |||
| 0e7901ebbe | |||
| 73f3a72d00 | |||
| 63feabf517 | |||
| 84f87dd7f3 | |||
| 39c247d9cb | |||
| a98c4f6c54 | |||
| bed6622530 | |||
| c5e08a6f2c | |||
| ad32af6eeb | |||
| 02ced4b87b | |||
| 5b28a63f75 | |||
| b5e2e68a22 | |||
| 556c0819c4 | |||
| b0edccb80e | |||
| 4e1c921ffb | |||
| fdbbcafb6c | |||
| a043d477fb | |||
| 4cbe766cbd | |||
| 8610b915ec | |||
| 59fd6db83e | |||
| 465cf7355b | |||
| de91b78943 | |||
| 4203148cab | |||
| 5adc0c464a | |||
| 097290f7da | |||
| 475ef5d1ca | |||
| 2f37019782 | |||
| 9f4123cc3c | |||
| 482a87a80d | |||
| b925010cb3 | |||
| 085ce01da6 | |||
| 7af530de8f | |||
| 9a4cff4e49 | |||
| 480ffa4ac2 | |||
| d5ac0c9aa0 | |||
| 29f19b07a9 | |||
| 39301c534c | |||
| 58b9e4bda7 | |||
| 820a9ce983 | |||
| 68410e882d | |||
| f26c0af39a | |||
| 0ca451483f | |||
| 2849dd0ba2 | |||
| df41ae4acb | |||
| 2692777707 | |||
| ca1a45ccf8 | |||
| 2f39f248fc | |||
| 90f8ce5035 | |||
| cb29125580 | |||
| 988b54515e | |||
| 2fd9de5062 | |||
| fd5aabcb17 | |||
| 85b63219b9 | |||
| ddf943846f | |||
| e7221ec623 | |||
| 78daaf0cb4 | |||
| a6ab3bab4c | |||
| 53cea7023f | |||
| a098088f03 | |||
| 59998e9fd2 | |||
| 1df7e478df | |||
| 1fc4890857 | |||
| f5d52f1506 | |||
| 2026ba5bd2 | |||
| db56c8d74d | |||
| 9d1a81c93c | |||
| 3701b3d7a3 | |||
| bae98daa5c | |||
| b34a04f723 | |||
| 1c0245f2db | |||
| 7777e87dc8 | |||
| 820fa07846 | |||
| 66794582c9 | |||
| 73eb471ae3 | |||
| 0f2f4b96c4 | |||
| d53809cf2b | |||
| 08fd6e26d8 | |||
| 29e8470f2e | |||
| 573785d4ce | |||
| 5483303714 | |||
| 5a5cc4f4e9 | |||
| cd672c341f | |||
| 12438d63c2 | |||
| 35255e4053 | |||
| 8856d45887 | |||
| 38af56c6fd | |||
| 9111e4809d | |||
| d08c7c5e55 | |||
| 69f3dee25a | |||
| 8155970ba2 | |||
| d356957dad | |||
| e7ccb702a3 | |||
| bf3ce6deb2 | |||
| f5295fb35d | |||
| 6c5836722a | |||
| 5716249bd9 | |||
| 4d0aab773b | |||
| e50ac208e3 | |||
| bcb5617194 | |||
| d3c23ba737 | |||
| e0ab0a6b90 | |||
| 713ce5f430 | |||
| 8eb23bcc29 | |||
| 4181343ef3 | |||
| d16566aa8d | |||
| 45eb101f40 | |||
| 59431869dc | |||
| 6e7aca8b15 | |||
| 6f387b0481 | |||
| 82d4364032 | |||
| e3de54c941 | |||
| 6991b45fbe | |||
| e5fff91ae6 | |||
| 2f2d4c9d9b | |||
| bfca1b46a6 | |||
| b117c80e47 | |||
| d20aa3b80a | |||
| a34fda984d | |||
| 510269dda9 | |||
| d51b34797c | |||
| d2905072c0 | |||
| 1ee42506b6 | |||
| 84fe2d751f | |||
| 5d0fc48706 | |||
| 335c5b4ac5 | |||
| 8c20f448ed | |||
| 0a668df138 | |||
| 3e4d2b4d46 | |||
| 12e43d120e | |||
| a9845bf3cd | |||
| e51ceed175 | |||
| 304baf6f60 | |||
| 6b141a9b06 | |||
| 0c3659a612 | |||
| a44bef5796 | |||
| b1ac6b0ef9 | |||
| 98844a3b85 | |||
| a32b8911c7 | |||
| 3118e7b9c3 | |||
| 2ca2bc5fb8 | |||
| 4bfb08f6ef | |||
| 0689339780 | |||
| a265625851 | |||
| 389fffaf64 | |||
| b7daf3f64a | |||
| 461da22b08 | |||
| 2b661e241d | |||
| d7df3800c2 | |||
| f2961f9b6a | |||
| f2d5ee4692 | |||
| 7c2d5ce15e | |||
| 5ceb908b8b | |||
| d819865853 | |||
| 38176ab543 | |||
| 53936d7034 | |||
| aafc2ea4d7 | |||
| 8a4be4936a | |||
| af097d0f33 | |||
| 44867e7b43 | |||
| a366bf3ca0 | |||
| 89f86be00a | |||
| 12a744e985 | |||
| 54f272ba1e | |||
| 60b64f22c6 | |||
| 97666dc73d | |||
| 6c6756936b | |||
| 91f8ca4efe | |||
| 045ac59a44 | |||
| 078180fe42 |
@@ -2,15 +2,16 @@
|
|||||||
name: dms-plugin-dev
|
name: dms-plugin-dev
|
||||||
description: >
|
description: >
|
||||||
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
|
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
|
||||||
Quickshell. Supports four plugin types: widget (bar + Control Center), daemon (background
|
Quickshell. Supports five plugin types: widget (bar + Control Center), daemon (background
|
||||||
service), launcher (search + actions), and desktop (draggable desktop widgets). Covers
|
service), launcher (search + actions), desktop (draggable desktop widgets), and composite
|
||||||
manifest creation, QML component development, settings UI, data persistence, theme
|
(multi-surface). Covers manifest creation, QML component development, startup checks,
|
||||||
integration, PopoutService usage, and external command execution. Use when the user wants
|
settings UI, data persistence, theme integration, PopoutService usage, IPC runtime
|
||||||
to create, modify, or debug a DMS plugin, or asks about the DMS plugin API.
|
discovery, and external command execution. Use when the user wants to create, modify,
|
||||||
|
or debug a DMS plugin, or asks about the DMS plugin API.
|
||||||
compatibility: Designed for Claude Code (or similar products)
|
compatibility: Designed for Claude Code (or similar products)
|
||||||
metadata:
|
metadata:
|
||||||
author: DankMaterialShell
|
author: DankMaterialShell
|
||||||
version: "1.0"
|
version: "1.1"
|
||||||
domain: qml-desktop-development
|
domain: qml-desktop-development
|
||||||
framework: DankMaterialShell
|
framework: DankMaterialShell
|
||||||
languages: qml, javascript
|
languages: qml, javascript
|
||||||
@@ -37,14 +38,15 @@ integrations, and desktop widgets. Plugins are QML components discovered from
|
|||||||
|
|
||||||
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
|
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
|
||||||
|
|
||||||
**Four plugin types:**
|
**Five plugin types:**
|
||||||
|
|
||||||
| Type | Purpose | Base Component | Bar pills | CC integration |
|
| Type | Purpose | Base Component | Bar pills | CC integration |
|
||||||
|------------|--------------------------------|----------------------------|-----------|----------------|
|
|-------------|--------------------------------|----------------------------|-----------|----------------|
|
||||||
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
|
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
|
||||||
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
|
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
|
||||||
| `launcher` | Searchable items in launcher | `Item` | No | No |
|
| `launcher` | Searchable items in launcher | `Item` | No | No |
|
||||||
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
|
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
|
||||||
|
| `composite` | Multi-surface plugin | One component per surface | Optional | Optional |
|
||||||
|
|
||||||
## Step 1: Determine Plugin Type
|
## Step 1: Determine Plugin Type
|
||||||
|
|
||||||
@@ -58,6 +60,9 @@ Choose the type based on what the plugin does:
|
|||||||
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
|
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
|
||||||
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
|
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
|
||||||
desktop layer.
|
desktop layer.
|
||||||
|
- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination
|
||||||
|
of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own
|
||||||
|
QML component file.
|
||||||
|
|
||||||
## Step 2: Create the Manifest
|
## Step 2: Create the Manifest
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**With settings and permissions:**
|
**With settings, startup check, and permissions:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -92,7 +97,31 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
"component": "./YourWidget.qml",
|
"component": "./YourWidget.qml",
|
||||||
"icon": "extension",
|
"icon": "extension",
|
||||||
"settings": "./Settings.qml",
|
"settings": "./Settings.qml",
|
||||||
|
"startupCheck": "./StartupCheck.qml",
|
||||||
"requires_dms": ">=0.1.0",
|
"requires_dms": ">=0.1.0",
|
||||||
|
"dependencies": ["mytool"],
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Composite plugin (multi-surface):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myComposite",
|
||||||
|
"name": "My Composite Plugin",
|
||||||
|
"description": "Daemon + widget + desktop from one plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"type": "composite",
|
||||||
|
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
|
||||||
|
"icon": "extension",
|
||||||
|
"components": {
|
||||||
|
"daemon": "./MyDaemon.qml",
|
||||||
|
"widget": "./MyBarWidget.qml",
|
||||||
|
"desktop": "./MyDesktopWidget.qml"
|
||||||
|
},
|
||||||
|
"settings": "./Settings.qml",
|
||||||
"permissions": ["settings_read", "settings_write"]
|
"permissions": ["settings_read", "settings_write"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -100,9 +129,11 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
**Key rules:**
|
**Key rules:**
|
||||||
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
|
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
|
||||||
- `version` must be semver (e.g., `1.0.0`)
|
- `version` must be semver (e.g., `1.0.0`)
|
||||||
- `component` must start with `./` and end with `.qml`
|
- Provide either `component` (single-surface) or `components` (multi-surface), not both
|
||||||
- `type: "launcher"` requires a `trigger` field
|
- `component` / component paths must start with `./` and end with `.qml`
|
||||||
|
- `type: "launcher"` (or a `components` object with a `launcher` key) requires a `trigger` field
|
||||||
- `settings_write` permission is **required** if the plugin has a settings component
|
- `settings_write` permission is **required** if the plugin has a settings component
|
||||||
|
- `dependencies` replaces the deprecated `requires` field
|
||||||
|
|
||||||
## Step 3: Create the Main Component
|
## Step 3: Create the Main Component
|
||||||
|
|
||||||
@@ -249,7 +280,69 @@ PluginComponent {
|
|||||||
|
|
||||||
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
|
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
|
||||||
|
|
||||||
## Step 4: Add Settings (Optional)
|
### Composite
|
||||||
|
|
||||||
|
For composite plugins, create a separate QML file per surface. Each surface uses the same
|
||||||
|
base component as the corresponding single-surface type (PluginComponent for widget/daemon,
|
||||||
|
Item for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.
|
||||||
|
|
||||||
|
```
|
||||||
|
MyCompositePlugin/
|
||||||
|
plugin.json
|
||||||
|
MyBarWidget.qml # PluginComponent (widget surface)
|
||||||
|
MyDaemon.qml # PluginComponent (daemon surface)
|
||||||
|
MyDesktopWidget.qml # Item with desktop widget properties
|
||||||
|
Settings.qml # Shared settings for all surfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `pluginService.pluginHasSurface(pluginId, "widget")` to check whether a specific surface
|
||||||
|
is registered for a plugin at runtime.
|
||||||
|
|
||||||
|
## Step 4: Add Startup Check (Optional)
|
||||||
|
|
||||||
|
Gate plugin activation on dependency checks by providing a `startupCheck` component. This
|
||||||
|
runs before the plugin loads and blocks activation if a required tool or condition is missing.
|
||||||
|
|
||||||
|
Create a `StartupCheck.qml` (non-visual QtObject):
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
function check(done) {
|
||||||
|
Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
done(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done({
|
||||||
|
"title": I18n.tr("mytool is required"),
|
||||||
|
"details": I18n.tr("Install 'mytool' and re-enable this plugin.")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `done` callback accepts:
|
||||||
|
- `null` - allow activation
|
||||||
|
- A string - block with a short error message
|
||||||
|
- `{ title, details }` - block with a title and expandable details body
|
||||||
|
|
||||||
|
A synchronous variant (no `done` parameter, return the result directly) is also supported.
|
||||||
|
|
||||||
|
Failed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.
|
||||||
|
|
||||||
|
Add to your manifest:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"startupCheck": "./StartupCheck.qml",
|
||||||
|
"dependencies": ["mytool"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Add Settings (Optional)
|
||||||
|
|
||||||
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
|
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
|
||||||
|
|
||||||
@@ -293,7 +386,7 @@ See [settings-components-reference.md](references/settings-components-reference.
|
|||||||
|
|
||||||
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
|
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
|
||||||
|
|
||||||
## Step 5: Use Data Persistence
|
## Step 6: Use Data Persistence
|
||||||
|
|
||||||
Three tiers of persistence:
|
Three tiers of persistence:
|
||||||
|
|
||||||
@@ -302,6 +395,7 @@ Three tiers of persistence:
|
|||||||
| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |
|
| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |
|
||||||
| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |
|
| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |
|
||||||
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
|
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
|
||||||
|
| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |
|
||||||
|
|
||||||
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
|
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
|
||||||
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
|
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
|
||||||
@@ -309,7 +403,7 @@ Three tiers of persistence:
|
|||||||
|
|
||||||
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
|
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
|
||||||
|
|
||||||
## Step 6: Theme Integration
|
## Step 7: Theme Integration
|
||||||
|
|
||||||
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
|
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
|
||||||
|
|
||||||
@@ -324,7 +418,7 @@ Always use `Theme.*` properties from `qs.Common` - never hardcode colors or size
|
|||||||
|
|
||||||
See [theme-reference.md](references/theme-reference.md) for the complete property list.
|
See [theme-reference.md](references/theme-reference.md) for the complete property list.
|
||||||
|
|
||||||
## Step 7: Add Popout Content (Widgets Only)
|
## Step 8: Add Popout Content (Widgets Only)
|
||||||
|
|
||||||
Add a popout that opens when the bar pill is clicked:
|
Add a popout that opens when the bar pill is clicked:
|
||||||
|
|
||||||
@@ -360,7 +454,7 @@ PluginComponent {
|
|||||||
|
|
||||||
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
||||||
|
|
||||||
## Step 8: Control Center Integration (Widgets Only)
|
## Step 9: Control Center Integration (Widgets Only)
|
||||||
|
|
||||||
Add your widget to the Control Center grid:
|
Add your widget to the Control Center grid:
|
||||||
|
|
||||||
@@ -389,7 +483,7 @@ PluginComponent {
|
|||||||
|
|
||||||
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
|
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
|
||||||
|
|
||||||
## Step 9: External Commands and Clipboard
|
## Step 10: External Commands and Clipboard
|
||||||
|
|
||||||
**Run commands and capture output:**
|
**Run commands and capture output:**
|
||||||
|
|
||||||
@@ -420,18 +514,33 @@ Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
|||||||
|
|
||||||
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
||||||
|
|
||||||
## Step 10: Validate and Test
|
## Step 11: Validate and Test
|
||||||
|
|
||||||
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
|
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
|
||||||
2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`
|
2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`
|
||||||
3. Open Settings > Plugins > Scan for Plugins
|
3. Open Settings > Plugins > Scan for Plugins
|
||||||
4. Enable your plugin and add it to the DankBar layout
|
4. Enable your plugin and add it to the DankBar layout
|
||||||
|
|
||||||
|
**Runtime plugin discovery via IPC:**
|
||||||
|
|
||||||
|
Plugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dms ipc plugin-scan scan # Trigger a full rescan of all plugin directories
|
||||||
|
dms ipc plugin-scan rescan <id> # Force rescan of a specific plugin
|
||||||
|
dms ipc plugin-scan reload <id> # Force reload of a loaded plugin
|
||||||
|
dms ipc plugin-scan list # List all known plugins (TSV: id, loaded, type, name)
|
||||||
|
dms ipc plugin-scan status <id> # Get status of a specific plugin (TSV: loaded, type, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugin IDs are validated against `^[a-zA-Z0-9_\-:]{1,64}$`.
|
||||||
|
|
||||||
**Common issues:**
|
**Common issues:**
|
||||||
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
|
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
|
||||||
- Widget not showing: ensure it's enabled AND added to a DankBar section
|
- Widget not showing: ensure it's enabled AND added to a DankBar section
|
||||||
- Settings error: verify `settings_write` permission is declared
|
- Settings error: verify `settings_write` permission is declared
|
||||||
- Data not persisting: check pluginService injection and permissions
|
- Data not persisting: check pluginService injection and permissions
|
||||||
|
- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`
|
||||||
|
|
||||||
## Common Mistakes
|
## Common Mistakes
|
||||||
|
|
||||||
@@ -445,6 +554,9 @@ Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
|||||||
8. **Forgetting `categories` in launcher items** - Items won't display without it
|
8. **Forgetting `categories` in launcher items** - Items won't display without it
|
||||||
9. **Not handling null pluginService** - Always use optional chaining or null checks
|
9. **Not handling null pluginService** - Always use optional chaining or null checks
|
||||||
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
|
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
|
||||||
|
11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`
|
||||||
|
12. **Providing both `component` and `components`** - Use one or the other, not both
|
||||||
|
13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key
|
||||||
|
|
||||||
## Quick Reference: Imports
|
## Quick Reference: Imports
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
"version",
|
"version",
|
||||||
"author",
|
"author",
|
||||||
"type",
|
"type",
|
||||||
"capabilities",
|
"capabilities"
|
||||||
"component"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -42,8 +41,8 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plugin type",
|
"description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
|
||||||
"enum": ["widget", "daemon", "launcher", "desktop"]
|
"enum": ["widget", "daemon", "launcher", "desktop", "composite"]
|
||||||
},
|
},
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -55,9 +54,37 @@
|
|||||||
},
|
},
|
||||||
"component": {
|
"component": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Relative path to main QML component file",
|
"description": "Relative path to main QML component file. Required unless 'components' is provided.",
|
||||||
"pattern": "^\\./.*\\.qml$"
|
"pattern": "^\\./.*\\.qml$"
|
||||||
},
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.",
|
||||||
|
"properties": {
|
||||||
|
"widget": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Bar/Control Center widget component (PluginComponent)",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"desktop": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Desktop widget component",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"daemon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Background daemon component (instantiated once)",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Launcher provider component (requires 'trigger')",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1
|
||||||
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Trigger string for launcher activation (required for launcher type)"
|
"description": "Trigger string for launcher activation (required for launcher type)"
|
||||||
@@ -71,14 +98,26 @@
|
|||||||
"description": "Path to settings component QML file",
|
"description": "Path to settings component QML file",
|
||||||
"pattern": "^\\./.*\\.qml$"
|
"pattern": "^\\./.*\\.qml$"
|
||||||
},
|
},
|
||||||
|
"startupCheck": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
"requires_dms": {
|
"requires_dms": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
||||||
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of required system tools/dependencies (registry metadata)",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Array of required system tools/dependencies",
|
"description": "Deprecated alias for 'dependencies'.",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -109,6 +148,29 @@
|
|||||||
"then": {
|
"then": {
|
||||||
"required": ["trigger"]
|
"required": ["trigger"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"required": ["components"],
|
||||||
|
"properties": {
|
||||||
|
"components": {
|
||||||
|
"required": ["launcher"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["trigger"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": ["component"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": ["components"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
|||||||
@@ -166,6 +166,16 @@ function increment() {
|
|||||||
| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |
|
| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |
|
||||||
| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |
|
| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |
|
||||||
|
|
||||||
|
## Plugin Path
|
||||||
|
|
||||||
|
Retrieve a plugin's installation directory at runtime:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
var dir = pluginService.getPluginPath(pluginId)
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the absolute path to the plugin's directory (e.g., `~/.config/DankMaterialShell/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
1. **pluginData is reactive** - bindings update automatically when data changes
|
1. **pluginData is reactive** - bindings update automatically when data changes
|
||||||
|
|||||||
@@ -9,15 +9,22 @@
|
|||||||
| `description` | string | Short description (shown in UI) | Non-empty |
|
| `description` | string | Short description (shown in UI) | Non-empty |
|
||||||
| `version` | string | Semantic version | Pattern `^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` |
|
| `version` | string | Semantic version | Pattern `^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` |
|
||||||
| `author` | string | Creator name or email | Non-empty |
|
| `author` | string | Creator name or email | Non-empty |
|
||||||
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop` |
|
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `composite` |
|
||||||
| `capabilities` | array | Plugin capabilities | At least 1 string item |
|
| `capabilities` | array | Plugin capabilities | At least 1 string item |
|
||||||
| `component` | string | Path to main QML file | Must start with `./`, end with `.qml` |
|
|
||||||
|
One of `component` or `components` is required (not both):
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
|-------|------|-------------|------------|
|
||||||
|
| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |
|
||||||
|
| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher` |
|
||||||
|
|
||||||
## Conditional Requirements
|
## Conditional Requirements
|
||||||
|
|
||||||
| Condition | Required Field | Description |
|
| Condition | Required Field | Description |
|
||||||
|-----------|---------------|-------------|
|
|-----------|---------------|-------------|
|
||||||
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
|
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
|
||||||
|
| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |
|
||||||
|
|
||||||
## Optional Fields
|
## Optional Fields
|
||||||
|
|
||||||
@@ -25,8 +32,10 @@
|
|||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
|
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
|
||||||
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
|
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
|
||||||
|
| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |
|
||||||
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
|
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
|
||||||
| `requires` | array | System tool dependencies (e.g., `["curl", "jq"]`) |
|
| `dependencies` | array | System tool dependencies (e.g., `["curl", "jq"]`). Registry metadata. |
|
||||||
|
| `requires` | array | Deprecated alias for `dependencies` |
|
||||||
| `permissions` | array | Required permissions |
|
| `permissions` | array | Required permissions |
|
||||||
| `trigger` | string | Launcher trigger string (required for launcher type) |
|
| `trigger` | string | Launcher trigger string (required for launcher type) |
|
||||||
|
|
||||||
@@ -53,6 +62,65 @@ Capabilities are free-form strings that describe what the plugin does. Common va
|
|||||||
- `ai` - AI/LLM integration
|
- `ai` - AI/LLM integration
|
||||||
- `slideout` - uses slideout panel
|
- `slideout` - uses slideout panel
|
||||||
|
|
||||||
|
## Startup Check
|
||||||
|
|
||||||
|
The `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
function check(done) {
|
||||||
|
Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
done(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done({
|
||||||
|
"title": I18n.tr("mytool is required"),
|
||||||
|
"details": I18n.tr("Install 'mytool' and re-enable this plugin.")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `done` callback accepts:
|
||||||
|
- `null` - allow activation
|
||||||
|
- A string - block with a short error message
|
||||||
|
- `{ title, details }` - block with a title and expandable details
|
||||||
|
|
||||||
|
A synchronous variant (no `done` parameter, return the result directly) is also supported.
|
||||||
|
|
||||||
|
Failed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.
|
||||||
|
|
||||||
|
## Components (Composite Plugins)
|
||||||
|
|
||||||
|
The `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myComposite",
|
||||||
|
"name": "My Composite Plugin",
|
||||||
|
"description": "Daemon + widget + desktop from one plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Developer Name",
|
||||||
|
"type": "composite",
|
||||||
|
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
|
||||||
|
"icon": "extension",
|
||||||
|
"components": {
|
||||||
|
"daemon": "./MyDaemon.qml",
|
||||||
|
"widget": "./MyBarWidget.qml",
|
||||||
|
"desktop": "./MyDesktopWidget.qml"
|
||||||
|
},
|
||||||
|
"settings": "./Settings.qml",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`. Provide any subset. Each surface is loaded independently in the appropriate registry.
|
||||||
|
|
||||||
## Complete Example
|
## Complete Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -67,8 +135,9 @@ Capabilities are free-form strings that describe what the plugin does. Common va
|
|||||||
"component": "./MyWidget.qml",
|
"component": "./MyWidget.qml",
|
||||||
"icon": "extension",
|
"icon": "extension",
|
||||||
"settings": "./Settings.qml",
|
"settings": "./Settings.qml",
|
||||||
|
"startupCheck": "./StartupCheck.qml",
|
||||||
"requires_dms": ">=0.1.18",
|
"requires_dms": ">=0.1.18",
|
||||||
"requires": ["curl", "jq"],
|
"dependencies": ["curl", "jq"],
|
||||||
"permissions": ["settings_read", "settings_write", "process", "network"]
|
"permissions": ["settings_read", "settings_write", "process", "network"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -235,10 +235,12 @@ Conditionally show/hide the bar pill:
|
|||||||
```qml
|
```qml
|
||||||
PluginComponent {
|
PluginComponent {
|
||||||
visibilityCommand: "pgrep -x myapp"
|
visibilityCommand: "pgrep -x myapp"
|
||||||
visibilityInterval: 5000 // check every 5 seconds
|
visibilityInterval: 5 // seconds between checks
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.
|
||||||
|
|
||||||
## Popout Namespace
|
## Popout Namespace
|
||||||
|
|
||||||
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
|
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- What does this PR do and why? -->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!-- Check all that apply. -->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change that adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that changes existing behavior)
|
||||||
|
- [ ] Refactor / internal cleanup
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
|
<!-- e.g. "Fixes #123", "Closes #123". Leave blank if none. -->
|
||||||
|
|
||||||
|
## Screenshots / video
|
||||||
|
|
||||||
|
<!-- Include screenshots or a video for any user-facing or visual change. -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] My code follows the conventions in CONTRIBUTING.md
|
||||||
|
- [ ] I have tested my changes locally
|
||||||
|
- [ ] New user-facing strings are wrapped in `I18n.tr()` with translator context, reusing existing terms where possible
|
||||||
|
- [ ] Go changes: ran `make fmt`, added/updated tests, `make test` passes, and `go mod tidy` is clean
|
||||||
|
- [ ] QML changes: ran `make lint-qml` with no new warnings
|
||||||
|
- [ ] I have opened a corresponding pull request in dlx-docs to document any new behaviors: https://github.com/AvengeMedia/DankLinux-Docs
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Categories for GitHub's auto-generated release notes
|
||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
authors:
|
||||||
|
- github-actions[bot]
|
||||||
|
- dependabot[bot]
|
||||||
|
labels:
|
||||||
|
- internal
|
||||||
|
categories:
|
||||||
|
- title: Breaking Changes
|
||||||
|
labels: [breaking]
|
||||||
|
- title: Features
|
||||||
|
labels: [feature]
|
||||||
|
- title: Fixes
|
||||||
|
labels: [fix]
|
||||||
|
- title: Packaging
|
||||||
|
labels: [packaging]
|
||||||
|
- title: Internationalization
|
||||||
|
labels: [i18n]
|
||||||
|
- title: Documentation
|
||||||
|
labels: [docs]
|
||||||
|
- title: Other Changes
|
||||||
|
labels: ["*"]
|
||||||
Executable
+170
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "usage: $0 download|publish" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name
|
||||||
|
for name in "$@"; do
|
||||||
|
if [[ -z "${!name:-}" ]]; then
|
||||||
|
echo "error: $name is required" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env R2_BUCKET R2_ENDPOINT R2_PREFIX REPOSITORY_DIR
|
||||||
|
|
||||||
|
aws_r2() {
|
||||||
|
aws --endpoint-url "$R2_ENDPOINT" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
mkdir -p "$REPOSITORY_DIR/current" "$REPOSITORY_DIR/previous-current"
|
||||||
|
|
||||||
|
aws_r2 s3 sync \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/" \
|
||||||
|
"$REPOSITORY_DIR/current/" \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
cp -a "$REPOSITORY_DIR/current/." "$REPOSITORY_DIR/previous-current/"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_manifest() {
|
||||||
|
local packages='[]'
|
||||||
|
local file filename pkgver name version revision sha size
|
||||||
|
|
||||||
|
for file in "$REPOSITORY_DIR"/current/*.xbps; do
|
||||||
|
filename="$(basename "$file")"
|
||||||
|
pkgver="$(xbps-uhelper binpkgver "$filename")"
|
||||||
|
name="$(xbps-uhelper getpkgname "$pkgver")"
|
||||||
|
version="$(xbps-uhelper getpkgversion "$pkgver")"
|
||||||
|
revision="$(xbps-uhelper getpkgrevision "$pkgver")"
|
||||||
|
version="${version%_"${revision}"}"
|
||||||
|
sha="$(sha256sum "$file" | cut -d' ' -f1)"
|
||||||
|
size="$(stat -c '%s' "$file")"
|
||||||
|
packages="$(jq \
|
||||||
|
--arg name "$name" \
|
||||||
|
--arg version "$version" \
|
||||||
|
--arg revision "$revision" \
|
||||||
|
--arg filename "$filename" \
|
||||||
|
--arg sha256 "$sha" \
|
||||||
|
--argjson size "$size" \
|
||||||
|
'. + [{name: $name, version: $version, revision: $revision, filename: $filename, sha256: $sha256, size: $size}]' \
|
||||||
|
<<<"$packages")"
|
||||||
|
done
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg repository "$R2_PREFIX" \
|
||||||
|
--arg source_commit "$SOURCE_COMMIT" \
|
||||||
|
--arg published_at "$PUBLISHED_AT" \
|
||||||
|
--argjson packages "$packages" \
|
||||||
|
'{schema: 1, repository: $repository, source_commit: $source_commit, published_at: $published_at, packages: $packages}' \
|
||||||
|
> "$REPOSITORY_DIR/current/manifest.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_immutable_packages() {
|
||||||
|
local file previous
|
||||||
|
|
||||||
|
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
|
||||||
|
previous="$REPOSITORY_DIR/previous-current/$(basename "$file")"
|
||||||
|
if [[ -f "$previous" ]] && ! cmp -s "$previous" "$file"; then
|
||||||
|
echo "error: refusing to replace immutable object $(basename "$file")" >&2
|
||||||
|
echo "bump the XBPS revision or version before publishing a changed build" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_retired() {
|
||||||
|
local old filename
|
||||||
|
local archive_prefix="archive/${R2_PREFIX}/${PUBLISHED_AT//:/-}"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for old in "$REPOSITORY_DIR"/previous-current/*.xbps "$REPOSITORY_DIR"/previous-current/*.sig2; do
|
||||||
|
filename="$(basename "$old")"
|
||||||
|
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$old" \
|
||||||
|
"s3://${R2_BUCKET}/${archive_prefix}/${filename}" \
|
||||||
|
--cache-control 'private,no-store' \
|
||||||
|
--only-show-errors
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_current() {
|
||||||
|
local file filename old
|
||||||
|
|
||||||
|
# Versioned package objects must exist before repodata can reference them.
|
||||||
|
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
|
||||||
|
filename="$(basename "$file")"
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$file" \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
|
||||||
|
--cache-control 'public,max-age=31536000,immutable' \
|
||||||
|
--only-show-errors
|
||||||
|
done
|
||||||
|
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$REPOSITORY_DIR/current/x86_64-repodata" \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/x86_64-repodata" \
|
||||||
|
--cache-control 'no-cache' \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
# The manifest is the publication marker and is always uploaded last.
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$REPOSITORY_DIR/current/manifest.json" \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/manifest.json" \
|
||||||
|
--cache-control 'no-cache' \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
# Once the new index and marker are live, remove objects no longer referenced.
|
||||||
|
shopt -s nullglob
|
||||||
|
for old in "$REPOSITORY_DIR"/previous-current/*; do
|
||||||
|
filename="$(basename "$old")"
|
||||||
|
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
|
||||||
|
aws_r2 s3 rm \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
|
||||||
|
--only-show-errors
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
publish() {
|
||||||
|
require_env SOURCE_COMMIT
|
||||||
|
PUBLISHED_AT="${PUBLISHED_AT:-$(date -u +'%Y-%m-%dT%H:%M:%SZ')}"
|
||||||
|
export PUBLISHED_AT
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
local packages=("$REPOSITORY_DIR"/current/*.xbps)
|
||||||
|
if (( ${#packages[@]} == 0 )); then
|
||||||
|
echo "error: refusing to publish an empty XBPS repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ -s "$REPOSITORY_DIR/current/x86_64-repodata" ]] || {
|
||||||
|
echo "error: x86_64-repodata is missing or empty" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
for file in "${packages[@]}"; do
|
||||||
|
[[ -s "${file}.sig2" ]] || {
|
||||||
|
echo "error: signature is missing for $(basename "$file")" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
verify_immutable_packages
|
||||||
|
build_manifest
|
||||||
|
archive_retired
|
||||||
|
upload_current
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
download) download ;;
|
||||||
|
publish) publish ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Sync flake.lock to dank-qml-common
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "dank-qml-common"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
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@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Point flake input at the submodule commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
|
||||||
|
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
|
||||||
|
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
|
||||||
|
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
|
||||||
|
|
||||||
|
- name: Commit and push flake.lock update
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if git diff --quiet flake.lock; then
|
||||||
|
echo "No changes to flake.lock"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
|
||||||
|
git pull --rebase origin ${{ github.ref_name }}
|
||||||
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||||
@@ -7,14 +7,17 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-stable:
|
update-stable:
|
||||||
|
# skip prerelease tags
|
||||||
|
if: ${{ !contains(github.ref_name, '-') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Create GitHub App token
|
- name: Create GitHub App token
|
||||||
id: app_token
|
id: app_token
|
||||||
uses: actions/create-github-app-token@v1
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
@@ -28,4 +31,54 @@ jobs:
|
|||||||
- name: Push to stable branch
|
- name: Push to stable branch
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# don't roll stable backwards
|
||||||
|
if git fetch origin stable --quiet 2>/dev/null; then
|
||||||
|
stable_tag=$(git describe --tags --abbrev=0 FETCH_HEAD 2>/dev/null || echo "v0.0.0")
|
||||||
|
newest=$(printf '%s\n%s\n' "$stable_tag" "${GITHUB_REF_NAME}" | sort -V | tail -1)
|
||||||
|
if [ "$newest" != "${GITHUB_REF_NAME}" ]; then
|
||||||
|
echo "skipping: ${GITHUB_REF_NAME} is older than stable (${stable_tag})"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:refs/heads/stable --force
|
||||||
|
|
||||||
|
cut-release-branch:
|
||||||
|
# create stable-X.Y at each vX.Y.0 tag
|
||||||
|
if: ${{ !contains(github.ref_name, '-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Create release branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! "${GITHUB_REF_NAME}" =~ ^v([0-9]+)\.([0-9]+)\.0$ ]]; then
|
||||||
|
echo "not a vX.Y.0 tag, no release branch to cut"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
branch="stable-${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
||||||
|
if git ls-remote --exit-code origin "refs/heads/${branch}" >/dev/null 2>&1; then
|
||||||
|
echo "${branch} already exists"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:refs/heads/${branch}"
|
||||||
|
echo "created ${branch} at ${GITHUB_REF_NAME}"
|
||||||
|
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh label create "port ${branch}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--description "Auto-port merged PR to ${branch}" --color 0e8a16 ||
|
||||||
|
echo "label 'port ${branch}' may already exist"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
- "core/**"
|
- "core/**"
|
||||||
- ".github/workflows/go-ci.yml"
|
- ".github/workflows/go-ci.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main, "stable-*"]
|
||||||
paths:
|
paths:
|
||||||
- "core/**"
|
- "core/**"
|
||||||
- ".github/workflows/go-ci.yml"
|
- ".github/workflows/go-ci.yml"
|
||||||
@@ -27,6 +27,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -48,8 +50,39 @@ jobs:
|
|||||||
- name: Build dms
|
- name: Build dms
|
||||||
run: go build -v ./cmd/dms
|
run: go build -v ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dms (embedded shell)
|
||||||
|
run: make build
|
||||||
|
|
||||||
- name: Build dms (distropkg)
|
- name: Build dms (distropkg)
|
||||||
run: go build -v -tags distro_binary ./cmd/dms
|
run: go build -v -tags 'distro_binary withshell' ./cmd/dms
|
||||||
|
|
||||||
- name: Build dankinstall
|
- name: Build dankinstall
|
||||||
run: go build -v ./cmd/dankinstall
|
run: go build -v ./cmd/dankinstall
|
||||||
|
|
||||||
|
build-freebsd:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Build all packages
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Build dms (distropkg, embedded shell)
|
||||||
|
run: |
|
||||||
|
make sync-shell
|
||||||
|
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Nix flake and NixOS tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main, "stable-*"]
|
||||||
paths:
|
paths:
|
||||||
- "flake.*"
|
- "flake.*"
|
||||||
- "distro/nix/**"
|
- "distro/nix/**"
|
||||||
@@ -33,8 +33,6 @@ jobs:
|
|||||||
- name: Run NixOS service start test
|
- name: Run NixOS service start test
|
||||||
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
|
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
|
||||||
|
|
||||||
- name: Run greeter niri test
|
|
||||||
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
|
|
||||||
|
|
||||||
- name: Run home-manager module test
|
- name: Run home-manager module test
|
||||||
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: Point release
|
||||||
|
|
||||||
|
# Cuts vX.Y.Z from stable-X.Y: runs the port audit (warn-only), bumps
|
||||||
|
# quickshell/VERSION, tags, and dispatches the Release workflow. Distro
|
||||||
|
# builds are dispatched separately.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Point release version (e.g. 1.5.1)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
notify_issues:
|
||||||
|
description: "Comment on open related/fixes issues asking reporters to retest"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: point-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Validate version and derive branch
|
||||||
|
id: derive
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::error::version must be X.Y.Z (got '$VERSION')"; exit 1
|
||||||
|
fi
|
||||||
|
echo "branch=stable-${VERSION%.*}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout release branch
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ steps.derive.outputs.branch }}
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Preflight — verify Release workflow is dispatchable
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh api "repos/${{ github.repository }}/actions/workflows/release.yml" \
|
||||||
|
--jq '.state' | grep -qx active ||
|
||||||
|
{ echo "::error::release.yml is not dispatchable; aborting before any push"; exit 1; }
|
||||||
|
|
||||||
|
- name: Port audit (informational)
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
bash scripts/port-audit.sh "${{ steps.derive.outputs.branch }}" ||
|
||||||
|
echo "::warning::port audit failed; continuing"
|
||||||
|
|
||||||
|
- name: Bump VERSION, tag, and push
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
TAG: ${{ steps.derive.outputs.tag }}
|
||||||
|
BRANCH: ${{ steps.derive.outputs.branch }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
|
||||||
|
echo "::error::tag ${TAG} already exists"; exit 1
|
||||||
|
fi
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
echo "${TAG}" > quickshell/VERSION
|
||||||
|
git add quickshell/VERSION
|
||||||
|
git commit -m "bump VERSION to ${TAG}"
|
||||||
|
git tag "${TAG}"
|
||||||
|
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH}" "refs/tags/${TAG}"
|
||||||
|
|
||||||
|
- name: Dispatch Release workflow
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh workflow run release.yml --ref "${{ steps.derive.outputs.tag }}" \
|
||||||
|
-f tag="${{ steps.derive.outputs.tag }}" \
|
||||||
|
-f notify_issues="${{ inputs.notify_issues }}"
|
||||||
|
|
||||||
|
- name: Next steps
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "## ${{ steps.derive.outputs.tag }} tagged on ${{ steps.derive.outputs.branch }} — Release workflow dispatched"
|
||||||
|
echo ""
|
||||||
|
echo "Distro builds are manual: run the per-distro workflows (COPR/OBS/PPA/XBPS) once the release is published."
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Port audit
|
||||||
|
|
||||||
|
# On-demand report of master commits not yet ported to a release branch.
|
||||||
|
# Updates the "Port status: <branch>" tracking issue and the step summary.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target:
|
||||||
|
description: "Release branch to audit (default: newest release/*)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Run audit
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: bash scripts/port-audit.sh ${{ inputs.target }} --issue
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Port to release branch
|
||||||
|
|
||||||
|
# Ports flagged commits from master onto stable-X.Y branches:
|
||||||
|
# - "port 1.5" flag in a commit message pushed to master — own line or
|
||||||
|
# mid-line; separators : / - or space; "stable-1.5" also accepted;
|
||||||
|
# comma lists ("Port: 1.5, 1.4") work in the own-line form
|
||||||
|
# - "port stable-1.5" label on a merged PR
|
||||||
|
# Conflicts are reported to the "Port status: <branch>" tracking issue.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request_target:
|
||||||
|
types: [closed, labeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: port-engine
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
port:
|
||||||
|
name: Port flagged commits
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
# Base-repo code only; PR head code is never checked out or executed.
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Port trailer-flagged commits
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
COMMITS: ${{ toJSON(github.event.commits) }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
for sha in $(jq -r '.[].id' <<<"$COMMITS"); do
|
||||||
|
git cat-file -e "$sha" 2>/dev/null || continue
|
||||||
|
# skip merge commits (handled by the label path)
|
||||||
|
[ "$(git rev-list --no-walk --count --min-parents=2 "$sha")" -eq 0 ] || continue
|
||||||
|
|
||||||
|
# own-line form (supports comma lists), validated version-shaped
|
||||||
|
t1=$(git log -1 --format=%B "$sha" |
|
||||||
|
{ grep -iE '^Port[:/ -]' || true; } | sed -E 's|^port[-:/ ]+||I' | tr ',' '\n' |
|
||||||
|
sed 's/[[:space:]]//g; /^$/d' | sed -E 's,^stable-,,I' |
|
||||||
|
{ grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' || true; })
|
||||||
|
# mid-line form, e.g. "fix thing fixes #2802 port 1.5"
|
||||||
|
t2=$(git log -1 --format=%B "$sha" |
|
||||||
|
{ grep -oiE '\bport[-: /]+(stable-)?[0-9]+\.[0-9]+(\.[0-9]+)?\b' || true; } |
|
||||||
|
sed -E 's|^port[-:/ ]+||I' | sed -E 's,^stable-,,I')
|
||||||
|
targets=$(printf '%s\n%s\n' "$t1" "$t2" | sed '/^$/d' | sort -u)
|
||||||
|
for ver in $targets; do
|
||||||
|
echo "::group::port $sha -> stable-$ver"
|
||||||
|
bash scripts/port.sh "stable-$ver" "$sha"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Port label-flagged PR
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request_target' &&
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
(github.event.action == 'closed' ||
|
||||||
|
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'port ')))
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
||||||
|
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||||
|
PORT_SOURCE_PR: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
targets=$(jq -r '.[] | select(startswith("port ")) | sub("^port +"; "")' <<<"$LABELS" |
|
||||||
|
sed -E 's,^stable-,,' | sort -u)
|
||||||
|
[ -n "$targets" ] || { echo "no port labels, nothing to do"; exit 0; }
|
||||||
|
[ -n "$MERGE_SHA" ] || { echo "::error::PR has no merge commit sha"; exit 1; }
|
||||||
|
|
||||||
|
for ver in $targets; do
|
||||||
|
echo "::group::port PR #${PORT_SOURCE_PR} ($MERGE_SHA) -> stable-$ver"
|
||||||
|
bash scripts/port.sh "stable-$ver" "$MERGE_SHA"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
@@ -3,13 +3,15 @@ name: Pre-commit Checks
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main, "stable-*"]
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit-check:
|
pre-commit-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -26,4 +28,4 @@ jobs:
|
|||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
- name: run pre-commit hooks
|
- name: run pre-commit hooks
|
||||||
uses: j178/prek-action@v1
|
uses: j178/prek-action@v2
|
||||||
|
|||||||
+130
-50
@@ -7,10 +7,16 @@ on:
|
|||||||
description: "Tag to release (e.g., v1.0.1)"
|
description: "Tag to release (e.g., v1.0.1)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
notify_issues:
|
||||||
|
description: "Comment on open related/fixes issues asking reporters to retest"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
actions: write
|
actions: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ inputs.tag }}
|
group: release-${{ inputs.tag }}
|
||||||
@@ -21,7 +27,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [amd64, arm64]
|
include:
|
||||||
|
- { goos: linux, arch: amd64 }
|
||||||
|
- { goos: linux, arch: arm64 }
|
||||||
|
- { goos: freebsd, arch: amd64 }
|
||||||
|
- { goos: freebsd, arch: arm64 }
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -29,6 +39,8 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
|
# linux assets keep their historical arch-only names
|
||||||
|
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -36,6 +48,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -43,6 +56,7 @@ jobs:
|
|||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
run: |
|
run: |
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
echo "The following files are not formatted:"
|
echo "The following files are not formatted:"
|
||||||
@@ -51,38 +65,43 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
- name: Build dankinstall (${{ matrix.arch }})
|
- name: Build dankinstall (${{ env.ASSET }})
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dankinstall
|
cd cmd/dankinstall
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dankinstall-${{ matrix.arch }}
|
-o ../../dankinstall-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
gzip -9 -k dankinstall-${{ env.ASSET }}
|
||||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Build dms (${{ matrix.arch }})
|
- name: Sync embedded shell
|
||||||
|
run: make sync-shell
|
||||||
|
|
||||||
|
- name: Build dms (${{ env.ASSET }})
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-${{ matrix.arch }}
|
-o ../../dms-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-${{ matrix.arch }}
|
gzip -9 -k dms-${{ env.ASSET }}
|
||||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Generate shell completions
|
- name: Generate shell completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
chmod +x dms-amd64
|
chmod +x dms-amd64
|
||||||
@@ -90,46 +109,58 @@ jobs:
|
|||||||
./dms-amd64 completion fish > completion.fish
|
./dms-amd64 completion fish > completion.fish
|
||||||
./dms-amd64 completion zsh > completion.zsh
|
./dms-amd64 completion zsh > completion.zsh
|
||||||
|
|
||||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
- name: Build dms-distropkg (${{ env.ASSET }})
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
-o ../../dms-distropkg-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
gzip -9 -k dms-distropkg-${{ env.ASSET }}
|
||||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ env.ASSET }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ env.ASSET }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ env.ASSET }}.gz
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-${{ matrix.arch }}.gz
|
core/dms-${{ env.ASSET }}.gz
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload artifacts (${{ env.ASSET }}, no installer)
|
||||||
|
if: matrix.goos != 'linux'
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: core-assets-${{ env.ASSET }}
|
||||||
|
path: |
|
||||||
|
core/dms-${{ env.ASSET }}.gz
|
||||||
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ env.ASSET }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ env.ASSET }}.gz
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-${{ matrix.arch }}.gz
|
core/dms-${{ env.ASSET }}.gz
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
core/completion.bash
|
core/completion.bash
|
||||||
core/completion.fish
|
core/completion.fish
|
||||||
core/completion.zsh
|
core/completion.zsh
|
||||||
@@ -185,11 +216,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Fetch updated tag after version bump
|
- name: Fetch updated tag after version bump
|
||||||
run: |
|
run: |
|
||||||
git fetch origin --force tag ${TAG}
|
git fetch origin --force tag ${TAG}
|
||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -205,13 +238,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
CHANGELOG=""
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
if [ -n "$PREVIOUS_TAG" ]; then
|
||||||
else
|
# PR-based notes with author credits; falls back to raw git log below
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
CHANGELOG=$(python3 scripts/release-notes.py "${PREVIOUS_TAG}..${TAG}" --format github --bare 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$CHANGELOG" ]; then
|
||||||
|
echo "release-notes.py unavailable or empty, using git log fallback"
|
||||||
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
||||||
|
else
|
||||||
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > RELEASE_BODY.md << 'EOF'
|
cat > RELEASE_BODY.md << 'EOF'
|
||||||
@@ -226,16 +269,23 @@ jobs:
|
|||||||
### Complete Packages
|
### Complete Packages
|
||||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
|
||||||
|
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
|
||||||
|
|
||||||
### Individual Components
|
### Individual Components
|
||||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
|
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
|
||||||
|
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
|
||||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
|
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
|
||||||
|
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
|
||||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||||
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||||
- **`dms-qml.tar.gz`** - QML source code only
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
||||||
@@ -283,6 +333,22 @@ jobs:
|
|||||||
# Copy completions
|
# Copy completions
|
||||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Replace the DankCommon symlink with real submodule content for packaging
|
||||||
|
rm quickshell/DankCommon
|
||||||
|
cp -r dank-qml-common/DankCommon quickshell/DankCommon
|
||||||
|
|
||||||
|
# Create full source tarball (GitHub tag archives never contain submodule content)
|
||||||
|
VERSION_NUM=${TAG#v}
|
||||||
|
tar --exclude='.git' \
|
||||||
|
--exclude='.github' \
|
||||||
|
--exclude='dank-qml-common' \
|
||||||
|
--exclude='_release_assets' \
|
||||||
|
--exclude='_core_assets' \
|
||||||
|
--exclude='RELEASE_BODY.md' \
|
||||||
|
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
|
||||||
|
-czf _release_assets/dms-source.tar.gz .
|
||||||
|
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
|
||||||
|
|
||||||
# Create QML source package (exclude build artifacts and git files)
|
# Create QML source package (exclude build artifacts and git files)
|
||||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
cp LICENSE CONTRIBUTING.md quickshell/
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
@@ -299,8 +365,8 @@ jobs:
|
|||||||
# Generate checksum for QML package
|
# Generate checksum for QML package
|
||||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||||
|
|
||||||
# Create full packages for each architecture
|
# Create full packages for each os/architecture
|
||||||
for arch in amd64 arm64; do
|
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
|
||||||
mkdir -p _temp_full/dms
|
mkdir -p _temp_full/dms
|
||||||
mkdir -p _temp_full/bin
|
mkdir -p _temp_full/bin
|
||||||
mkdir -p _temp_full/completions
|
mkdir -p _temp_full/completions
|
||||||
@@ -343,18 +409,16 @@ jobs:
|
|||||||
|
|
||||||
## Installation Steps
|
## Installation Steps
|
||||||
|
|
||||||
1. **Install quickshell assets:**
|
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/`
|
||||||
```bash
|
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a
|
||||||
mkdir -p ~/.config/quickshell
|
modified copy instead of the embedded UI.
|
||||||
cp -r dms ~/.config/quickshell/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install the DMS CLI binaries:**
|
1. **Install the DMS CLI binaries:**
|
||||||
```bash
|
```bash
|
||||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Install shell completions (optional):**
|
2. **Install shell completions (optional):**
|
||||||
```bash
|
```bash
|
||||||
# Bash
|
# Bash
|
||||||
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
||||||
@@ -366,7 +430,7 @@ jobs:
|
|||||||
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Start the shell:**
|
3. **Start the shell:**
|
||||||
```bash
|
```bash
|
||||||
dms run
|
dms run
|
||||||
```
|
```
|
||||||
@@ -417,3 +481,19 @@ jobs:
|
|||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Ask reporters on still-open "related/fixes #N" issues to retest.
|
||||||
|
# Opt-in via notify_issues (Point release defaults it on). Skipped for
|
||||||
|
# prereleases. Preview: scripts/notify-issues.sh <prev> <tag> --dry-run
|
||||||
|
- name: Notify open issues to retest
|
||||||
|
if: ${{ inputs.notify_issues && !contains(env.TAG, '-') }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)
|
||||||
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
|
echo "no previous tag; skipping issue notify"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG"
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: 'Package to build (dms, dms-greeter, or both)'
|
description: 'Package to build'
|
||||||
required: false
|
required: false
|
||||||
default: 'dms'
|
default: 'dms'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- both
|
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
@@ -31,11 +29,7 @@ jobs:
|
|||||||
id: set-packages
|
id: set-packages
|
||||||
run: |
|
run: |
|
||||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-and-upload:
|
build-and-upload:
|
||||||
needs: determine-packages
|
needs: determine-packages
|
||||||
@@ -163,8 +157,6 @@ jobs:
|
|||||||
PACKAGE="${{ matrix.package }}"
|
PACKAGE="${{ matrix.package }}"
|
||||||
if [ "$PACKAGE" = "dms" ]; then
|
if [ "$PACKAGE" = "dms" ]; then
|
||||||
COPR_PROJECT="avengemedia/dms"
|
COPR_PROJECT="avengemedia/dms"
|
||||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
|
||||||
COPR_PROJECT="avengemedia/danklinux"
|
|
||||||
else
|
else
|
||||||
echo "❌ Unknown package: $PACKAGE"
|
echo "❌ Unknown package: $PACKAGE"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -24,6 +23,7 @@ jobs:
|
|||||||
check-updates:
|
check-updates:
|
||||||
name: Check for updates
|
name: Check for updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
@@ -35,6 +35,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
@@ -73,27 +74,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to check dms-greeter stable tag
|
|
||||||
check_dms_greeter_stable() {
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
|
||||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
|
||||||
|
|
||||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
|
||||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Run from tag with no package specified - update both stable packages
|
# Run from tag with no package specified - update stable package
|
||||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
@@ -119,7 +105,7 @@ jobs:
|
|||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
# Check each stable package and build list of those needing updates
|
# Check the stable package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
PACKAGES_TO_UPDATE+=("dms")
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
@@ -127,10 +113,6 @@ jobs:
|
|||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
|
||||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -139,7 +121,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "✓ Both packages up to date"
|
echo "✓ All packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -163,18 +145,6 @@ jobs:
|
|||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
else
|
||||||
# Unknown package - proceed anyway
|
# Unknown package - proceed anyway
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
@@ -198,6 +168,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Wait before OBS upload
|
- name: Wait before OBS upload
|
||||||
run: sleep 3
|
run: sleep 3
|
||||||
@@ -218,9 +189,9 @@ jobs:
|
|||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
# Manual workflow dispatch
|
||||||
|
|
||||||
# Determine version for dms stable and dms-greeter using the API
|
# Determine version for dms stable using the API
|
||||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
@@ -286,7 +257,7 @@ jobs:
|
|||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
- name: Update stable version (dms + dms-greeter)
|
- name: Update stable version (dms)
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
@@ -322,25 +293,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
|
||||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
{
|
|
||||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
|
||||||
echo ""
|
|
||||||
echo " * Update to $VERSION stable release"
|
|
||||||
echo ""
|
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
|
||||||
} > "distro/debian/dms-greeter/debian/changelog"
|
|
||||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update Debian _service files for packages in upload list (download_url paths)
|
# Update Debian _service files for packages in upload list (download_url paths)
|
||||||
for service in distro/debian/*/_service; do
|
for service in distro/debian/*/_service; do
|
||||||
if [[ -f "$service" ]]; then
|
if [[ -f "$service" ]]; then
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
# Update tar_scm revision parameter (for dms-git)
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
# Update download_url paths (for dms, dms-greeter stable)
|
# Update download_url paths (for dms stable)
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
fi
|
fi
|
||||||
@@ -401,7 +359,7 @@ jobs:
|
|||||||
UPLOADED_PACKAGES=()
|
UPLOADED_PACKAGES=()
|
||||||
SKIPPED_PACKAGES=()
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
# PACKAGES can be a space-separated list (from the "all" check)
|
||||||
# Loop through each package and upload
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
echo ""
|
echo ""
|
||||||
@@ -504,9 +462,6 @@ jobs:
|
|||||||
dms-git)
|
dms-git)
|
||||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -24,6 +23,7 @@ jobs:
|
|||||||
check-updates:
|
check-updates:
|
||||||
name: Check package/series updates
|
name: Check package/series updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
@@ -35,6 +35,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -93,6 +94,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -132,7 +134,6 @@ jobs:
|
|||||||
case "$PACKAGE" in
|
case "$PACKAGE" in
|
||||||
dms) PPA_NAME="dms" ;;
|
dms) PPA_NAME="dms" ;;
|
||||||
dms-git) PPA_NAME="dms-git" ;;
|
dms-git) PPA_NAME="dms-git" ;;
|
||||||
dms-greeter) PPA_NAME="danklinux" ;;
|
|
||||||
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -147,4 +148,3 @@ jobs:
|
|||||||
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
name: Void Linux XBPS Repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
|
# release trigger disabled; dispatch manually after a release
|
||||||
|
# release:
|
||||||
|
# types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Stable release tag/version to build (e.g. v1.5.0 or 1.5.0). Leave blank to use the selected ref/template version'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
force_rebuild:
|
||||||
|
description: 'Force rebuilding packages even if they already exist in the repository'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
build_git:
|
||||||
|
description: 'Build dms-git package'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_dms:
|
||||||
|
description: 'Build stable dms package'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: void-xbps-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
preflight:
|
||||||
|
name: Check for unpublished changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_build: ${{ steps.check.outputs.should_build }}
|
||||||
|
steps:
|
||||||
|
- name: Check published commit
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
MANIFEST_URL: https://void.danklinux.com/dms/current/manifest.json
|
||||||
|
run: |
|
||||||
|
SHOULD_BUILD=true
|
||||||
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
|
PUBLISHED_COMMIT="$(curl -fsSL --retry 3 "$MANIFEST_URL" | jq -r '.source_commit // empty' 2>/dev/null || true)"
|
||||||
|
if [ "$PUBLISHED_COMMIT" = "${{ github.sha }}" ]; then
|
||||||
|
SHOULD_BUILD=false
|
||||||
|
echo "${{ github.sha }} is already published; skipping the scheduled build."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build-and-deploy:
|
||||||
|
name: Build & Deploy XBPS packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: preflight
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell' && needs.preflight.outputs.should_build == 'true'
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: true
|
||||||
|
R2_BUCKET: danklinux-void
|
||||||
|
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
||||||
|
R2_PREFIX: dms
|
||||||
|
REPOSITORY_DIR: ${{ github.workspace }}/r2-repo
|
||||||
|
SOURCE_COMMIT: ${{ github.sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Record checked-out source commit
|
||||||
|
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Download current R2 repository
|
||||||
|
run: |
|
||||||
|
aws --version
|
||||||
|
bash .github/scripts/publish-void-r2.sh download
|
||||||
|
|
||||||
|
- name: Install XBPS Static Utilities
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ github.workspace }}/xbps
|
||||||
|
curl -s -L https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz | tar -xJ -C ${{ github.workspace }}/xbps
|
||||||
|
echo "${{ github.workspace }}/xbps/usr/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Clone void-packages
|
||||||
|
run: |
|
||||||
|
git clone --depth=1 https://github.com/void-linux/void-packages.git
|
||||||
|
|
||||||
|
- name: Inject templates
|
||||||
|
run: |
|
||||||
|
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
|
||||||
|
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
|
||||||
|
|
||||||
|
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
|
||||||
|
run: |
|
||||||
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||||
|
|
||||||
|
- name: Bootstrap xbps-src
|
||||||
|
run: |
|
||||||
|
cd void-packages
|
||||||
|
./xbps-src binary-bootstrap
|
||||||
|
|
||||||
|
- name: Configure signing keys and trust
|
||||||
|
run: |
|
||||||
|
# Write private key; extract and register the public key
|
||||||
|
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
||||||
|
chmod 600 /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
# Extract public key in PEM format
|
||||||
|
openssl rsa -in /tmp/xbps_privkey.pem -pubout -out /tmp/dms-key.pub
|
||||||
|
rm -f /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
# Compute MD5 fingerprint in colon-separated hex format
|
||||||
|
FINGERPRINT=$(openssl rsa -pubin -in /tmp/dms-key.pub -outform DER 2>/dev/null | openssl dgst -md5 -c | tr '[:upper:]' '[:lower:]' | awk '{print $NF}')
|
||||||
|
|
||||||
|
# Format key in XML property list (plist) format as expected by xbps
|
||||||
|
mkdir -p /tmp/keys
|
||||||
|
cat <<EOF > "/tmp/keys/${FINGERPRINT}.plist"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>public-key</key>
|
||||||
|
<data>$(base64 -w0 /tmp/dms-key.pub)</data>
|
||||||
|
<key>public-key-size</key>
|
||||||
|
<integer>4096</integer>
|
||||||
|
<key>signature-by</key>
|
||||||
|
<string>AvengeMedia</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Copy keys to all host and chroot trust stores
|
||||||
|
sudo mkdir -p /var/db/xbps/keys
|
||||||
|
sudo cp "/tmp/keys/${FINGERPRINT}.plist" "/var/db/xbps/keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
mkdir -p void-packages/masterdir/var/db/xbps/keys
|
||||||
|
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/masterdir/var/db/xbps/keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
mkdir -p void-packages/etc/xbps.d/keys
|
||||||
|
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/etc/xbps.d/keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
mkdir -p void-packages/common/repo-keys
|
||||||
|
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/common/repo-keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
rm -rf /tmp/keys /tmp/dms-key.pub
|
||||||
|
|
||||||
|
- name: Configure repositories
|
||||||
|
run: |
|
||||||
|
# Append the repository to repos-remote templates so xbps-src translates it automatically
|
||||||
|
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
|
||||||
|
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
|
||||||
|
|
||||||
|
# Add any existing compiled packages to the build cache directory to avoid rebuilds
|
||||||
|
if [ -d "r2-repo/current" ]; then
|
||||||
|
mkdir -p void-packages/hostdir/binpkgs
|
||||||
|
cp -L r2-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
|
||||||
|
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: |
|
||||||
|
mkdir -p r2-repo/current
|
||||||
|
|
||||||
|
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
|
||||||
|
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
|
||||||
|
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
|
BUILD_DMS="false"
|
||||||
|
BUILD_GIT="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
|
BUILD_DMS="true"
|
||||||
|
BUILD_GIT="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_TAG=""
|
||||||
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
|
RELEASE_TAG="${{ github.event.release.tag_name }}"
|
||||||
|
elif [ -n "${{ github.event.inputs.version || '' }}" ]; then
|
||||||
|
RELEASE_TAG="${{ github.event.inputs.version }}"
|
||||||
|
elif [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
RELEASE_TAG="${{ github.ref_name }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_TAG" ]; then
|
||||||
|
RELEASE_VER="${RELEASE_TAG#v}"
|
||||||
|
ARCHIVE_TAG="v${RELEASE_VER}"
|
||||||
|
echo "Stable release tag: $ARCHIVE_TAG"
|
||||||
|
else
|
||||||
|
RELEASE_VER=""
|
||||||
|
ARCHIVE_TAG=""
|
||||||
|
echo "Stable release tag: (template version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Starting Builds ==="
|
||||||
|
echo "DMS stable build enabled: $BUILD_DMS"
|
||||||
|
echo "Git build enabled: $BUILD_GIT"
|
||||||
|
echo "Force rebuild: $FORCE_REBUILD"
|
||||||
|
|
||||||
|
cd void-packages
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
|
||||||
|
echo "🔧 Updating stable template for $ARCHIVE_TAG"
|
||||||
|
TARBALL="$(mktemp)"
|
||||||
|
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
||||||
|
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
||||||
|
rm -f "$TARBALL"
|
||||||
|
|
||||||
|
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
|
||||||
|
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Build dms-git (development package)
|
||||||
|
if [ "$BUILD_GIT" = "true" ]; then
|
||||||
|
# Calculate dynamic git version (tag.commits.hash)
|
||||||
|
GIT_VER=$(git -C .. describe --tags --always | sed 's/^v//; s/-/./g')
|
||||||
|
echo "🔨 Preparing dms-git version $GIT_VER"
|
||||||
|
|
||||||
|
# Stage source tarball in the xbps-src source cache.
|
||||||
|
# --transform adds a top-level directory so xbps-src can extract
|
||||||
|
# into $wrksrc (create_wrksrc=yes handles the rest).
|
||||||
|
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
||||||
|
mkdir -p "$SRC_CACHE"
|
||||||
|
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||||
|
--exclude=void-packages \
|
||||||
|
--exclude=r2-repo \
|
||||||
|
--exclude=.git \
|
||||||
|
--exclude=danklinux \
|
||||||
|
-C .. .
|
||||||
|
|
||||||
|
CHECKSUM=$(sha256sum "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# Dynamically patch template version, checksum, and distfiles
|
||||||
|
sed -i "s/^version=.*/version=${GIT_VER}/" srcpkgs/dms-git/template
|
||||||
|
sed -i "s/^checksum=.*/checksum=${CHECKSUM}/" srcpkgs/dms-git/template
|
||||||
|
sed -i "s|^distfiles=.*|distfiles=\"dms-git-${GIT_VER}.tar.gz\"|" srcpkgs/dms-git/template
|
||||||
|
|
||||||
|
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
|
||||||
|
|
||||||
|
if [ -f "../r2-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
|
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
|
||||||
|
else
|
||||||
|
echo "🔨 Compiling dms-git..."
|
||||||
|
./xbps-src pkg dms-git
|
||||||
|
rm -f "../r2-repo/current/${EXPECTED_GIT_FILE}"
|
||||||
|
cp -L "hostdir/binpkgs/${EXPECTED_GIT_FILE}" ../r2-repo/current/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Build stable dms package
|
||||||
|
if [ "$BUILD_DMS" = "true" ]; then
|
||||||
|
STABLE_VER=$(grep -E '^version=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
|
||||||
|
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
|
||||||
|
|
||||||
|
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
|
||||||
|
if [ -f "../r2-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
|
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
|
||||||
|
else
|
||||||
|
echo "🔨 Compiling dms ($STABLE_VER)..."
|
||||||
|
./xbps-src pkg dms
|
||||||
|
rm -f "../r2-repo/current/${EXPECTED_DMS_FILE}"
|
||||||
|
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../r2-repo/current/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Index and sign repository
|
||||||
|
run: |
|
||||||
|
cd r2-repo/current
|
||||||
|
|
||||||
|
rm -f *.sig
|
||||||
|
|
||||||
|
# Guard: nothing to index if no .xbps files exist
|
||||||
|
if ! ls *.xbps 1>/dev/null 2>&1; then
|
||||||
|
echo "⚠️ No .xbps files found to index, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Regenerate repo index
|
||||||
|
xbps-rindex -a $(pwd)/*.xbps
|
||||||
|
xbps-rindex --remove-obsoletes $(pwd)
|
||||||
|
|
||||||
|
# Remove signatures left behind by obsolete packages. Signatures for
|
||||||
|
# unchanged immutable packages are retained byte-for-byte.
|
||||||
|
for sig in *.sig2; do
|
||||||
|
[ -e "$sig" ] || break
|
||||||
|
[ -f "${sig%.sig2}" ] || rm -f "$sig"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sign repository
|
||||||
|
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
||||||
|
chmod 600 /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
xbps-rindex --sign --signedby "AvengeMedia <AvengeMedia.US@gmail.com>" --privkey /tmp/xbps_privkey.pem $(pwd)
|
||||||
|
xbps-rindex --sign-pkg --privkey /tmp/xbps_privkey.pem $(pwd)/*.xbps
|
||||||
|
|
||||||
|
rm -f /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
- name: Publish repository to R2
|
||||||
|
run: bash .github/scripts/publish-void-r2.sh publish
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create GitHub App token
|
- name: Create GitHub App token
|
||||||
id: app_token
|
id: app_token
|
||||||
uses: actions/create-github-app-token@v1
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|||||||
+16
@@ -110,8 +110,24 @@ bin/
|
|||||||
# Core dumps
|
# Core dumps
|
||||||
core.*
|
core.*
|
||||||
|
|
||||||
|
# prek-installed local git hooks (generated from .pre-commit-config.yaml)
|
||||||
|
.githooks/
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
quickshell/dms-plugins
|
quickshell/dms-plugins
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Void (xbps) build artifacts
|
||||||
|
*.xbps
|
||||||
|
distro/void/temp/
|
||||||
|
distro/void/hostdir/
|
||||||
|
distro/void/masterdir*/
|
||||||
|
|
||||||
|
# Often gets built
|
||||||
|
core/dms
|
||||||
|
|
||||||
|
core/internal/shellembed/dist
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "dank-qml-common"]
|
||||||
|
path = dank-qml-common
|
||||||
|
url = https://github.com/AvengeMedia/dank-qml-common.git
|
||||||
|
branch = master
|
||||||
@@ -20,6 +20,30 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
files: ^core/.*\.(go|mod|sum)$
|
files: ^core/.*\.(go|mod|sum)$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: settings-search-index
|
||||||
|
name: settings search index is up to date
|
||||||
|
entry: bash -c 'python3 quickshell/translations/extract_settings_index.py >/dev/null || exit 1; if ! git diff --exit-code -- quickshell/translations/settings_search_index.json; then echo "settings_search_index.json is out of date; run quickshell/translations/extract_settings_index.py and stage the result" >&2; exit 1; fi'
|
||||||
|
language: system
|
||||||
|
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
|
||||||
|
pass_filenames: false
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: i18n-term-freeze
|
||||||
|
name: i18n term freeze (no new I18n.tr/qsTr terms)
|
||||||
|
entry: python3 quickshell/translations/check_term_freeze.py
|
||||||
|
language: system
|
||||||
|
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
|
||||||
|
pass_filenames: false
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: i18n-term-variants
|
||||||
|
name: i18n term variants (no case/punctuation duplicates)
|
||||||
|
entry: python3 quickshell/translations/check_term_variants.py
|
||||||
|
language: system
|
||||||
|
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
|
||||||
|
pass_filenames: false
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: no-console-in-qml
|
- id: no-console-in-qml
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
|
# Next
|
||||||
|
- Go core migrated to dankgo (shared log/paths/errdefs/ipc/shellapp)
|
||||||
|
- Quickshell UI embedded in the dms binary, -c / DMS_SHELL_DIR override - breaking for setups relying on ~/.config/quickshell/dms auto-discovery (XDG search removed)
|
||||||
|
- release binaries + all -git packages build with withshell; sockets/pidfiles/env names unchanged, IPC APIVersion 28 wire-compatible
|
||||||
|
- -git packages (AUR dms-shell-git, fedora, opensuse, void, debian, ubuntu) no longer install /usr/share/quickshell/dms; dms greeter install falls back to the embedded UI when the greeter package is present
|
||||||
|
- ping over the socket returns {"pong":true} instead of "pong"
|
||||||
|
- startup: embedded UI resolution is keyed by build-time .dankrev (no per-start content hashing)
|
||||||
|
|
||||||
# 1.5.0
|
# 1.5.0
|
||||||
- Overhauled shadows
|
- Overhauled shadows
|
||||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
- App ID changed to com.danklinux.dms - breaking for window rules
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ To contribute fork this repository, make your changes, and open a pull request.
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
# or, in an existing clone:
|
||||||
|
git submodule update --init
|
||||||
|
```
|
||||||
|
|
||||||
|
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config submodule.recurse true
|
||||||
|
```
|
||||||
|
|
||||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -28,6 +42,47 @@ This will provide:
|
|||||||
|
|
||||||
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||||
|
|
||||||
|
## Building and running
|
||||||
|
|
||||||
|
The Quickshell UI is embedded into the `dms` binary at build time. `make build` copies `quickshell/` into `core/internal/shellembed/dist/` (generated, never committed) and compiles with the `withshell` tag. `make dev` builds without the tag — that binary carries no UI and requires an explicit config dir.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # embedded binary at core/bin/dms
|
||||||
|
make dev # untagged development build
|
||||||
|
make run # dev build, then launch against the live quickshell/ tree
|
||||||
|
```
|
||||||
|
|
||||||
|
The UI config dir resolves in order: `-c <dir>`, `DMS_SHELL_DIR`, the dir a running instance is using, then the embedded UI. Each candidate must contain `shell.qml`. `make run` uses `-c $(pwd)/quickshell`, so QML edits hot-reload from the working tree.
|
||||||
|
|
||||||
|
The Go core depends on [dankgo](https://github.com/AvengeMedia/dankgo) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
|
||||||
|
|
||||||
|
```
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
use (
|
||||||
|
./core
|
||||||
|
../dankgo
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shared widgets (dank-qml-common)
|
||||||
|
|
||||||
|
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
|
||||||
|
|
||||||
|
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
|
||||||
|
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
|
||||||
|
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
|
||||||
|
|
||||||
|
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --global url."git@github.com:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
|
||||||
|
|
||||||
|
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
|
||||||
|
|
||||||
## VSCode Setup
|
## 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.
|
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.
|
||||||
@@ -104,6 +159,8 @@ Text {
|
|||||||
|
|
||||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
||||||
|
|
||||||
|
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
|
||||||
|
|
||||||
### GO (`core` directory)
|
### GO (`core` directory)
|
||||||
|
|
||||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ BINARY_NAME=dms
|
|||||||
CORE_DIR=core
|
CORE_DIR=core
|
||||||
BUILD_DIR=$(CORE_DIR)/bin
|
BUILD_DIR=$(CORE_DIR)/bin
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
INSTALL_DIR=$(PREFIX)/bin
|
INSTALL_DIR=$(DESTDIR)$(PREFIX)/bin
|
||||||
DATA_DIR=$(PREFIX)/share
|
DATA_DIR=$(DESTDIR)$(PREFIX)/share
|
||||||
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
||||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
.PHONY: all build dev run clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -27,6 +27,12 @@ build:
|
|||||||
@$(MAKE) -C $(CORE_DIR) build
|
@$(MAKE) -C $(CORE_DIR) build
|
||||||
@echo "Build complete"
|
@echo "Build complete"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
@$(MAKE) -C $(CORE_DIR) dev
|
||||||
|
|
||||||
|
run: dev
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) run -c $(CURDIR)/$(SHELL_DIR)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@@ -35,6 +41,12 @@ clean:
|
|||||||
lint-qml:
|
lint-qml:
|
||||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||||
|
|
||||||
|
# Pull the latest dank-qml-common and pin it everywhere it is consumed
|
||||||
|
# (submodule pointer + nix flake input). Commit both in one change.
|
||||||
|
update-common:
|
||||||
|
git submodule update --remote --merge dank-qml-common
|
||||||
|
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -43,8 +55,9 @@ install-bin:
|
|||||||
|
|
||||||
install-shell:
|
install-shell:
|
||||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||||
|
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
|
||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||||
@echo "Shell files installed"
|
@echo "Shell files installed"
|
||||||
|
|
||||||
@@ -59,13 +72,17 @@ install-completions:
|
|||||||
@echo "Shell completions installed"
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
install-systemd:
|
install-systemd:
|
||||||
|
ifneq ($(shell uname),Linux)
|
||||||
|
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
|
||||||
|
else
|
||||||
@echo "Installing systemd user service..."
|
@echo "Installing systemd user service..."
|
||||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
||||||
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
@sed 's|/usr/bin/dms|$(PREFIX)/bin/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
endif
|
||||||
|
|
||||||
install-icon:
|
install-icon:
|
||||||
@echo "Installing icon..."
|
@echo "Installing icon..."
|
||||||
@@ -74,10 +91,12 @@ install-icon:
|
|||||||
@echo "Icon installed"
|
@echo "Icon installed"
|
||||||
|
|
||||||
install-desktop:
|
install-desktop:
|
||||||
@echo "Installing desktop entry..."
|
@echo "Installing desktop entries..."
|
||||||
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.notepad.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
|
||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entry installed"
|
@echo "Desktop entries installed"
|
||||||
|
|
||||||
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -116,10 +135,12 @@ uninstall-icon:
|
|||||||
@echo "Icon removed"
|
@echo "Icon removed"
|
||||||
|
|
||||||
uninstall-desktop:
|
uninstall-desktop:
|
||||||
@echo "Removing desktop entry..."
|
@echo "Removing desktop entries..."
|
||||||
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
|
||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entry removed"
|
@echo "Desktop entries removed"
|
||||||
|
|
||||||
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
|
|||||||
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||||
|
|
||||||
**Session Management**
|
**Session Management**
|
||||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
|
||||||
|
|
||||||
**Plugin System**
|
**Plugin System**
|
||||||
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||||
@@ -132,6 +132,17 @@ dms plugins search # Browse plugin registry
|
|||||||
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
||||||
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||||
|
|
||||||
|
## Dank Projects
|
||||||
|
|
||||||
|
DMS is one piece of the suite. The rest lives in its own repos:
|
||||||
|
|
||||||
|
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
|
||||||
|
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
|
||||||
|
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring CLI and API that powers the process list and dashboard widgets.
|
||||||
|
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher.
|
||||||
|
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
|
||||||
|
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See component-specific documentation:
|
See component-specific documentation:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DankMaterialShell
|
||||||
|
GenericName=Desktop Shell
|
||||||
|
Comment=Material 3 inspired shell for Wayland compositors
|
||||||
|
Exec=dms run
|
||||||
|
Icon=danklogo
|
||||||
|
Terminal=false
|
||||||
|
NoDisplay=true
|
||||||
|
StartupNotify=false
|
||||||
|
Categories=Utility;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DMS Notepad
|
||||||
|
GenericName=Text Editor
|
||||||
|
Comment=Quick notes and text files in the dank desktop shell
|
||||||
|
Exec=dms ipc call notepad openFile %f
|
||||||
|
Icon=danklogo
|
||||||
|
Terminal=false
|
||||||
|
NoDisplay=true
|
||||||
|
Categories=Utility;TextEditor;
|
||||||
|
MimeType=text/plain;text/markdown;
|
||||||
|
StartupNotify=false
|
||||||
+65
-62
@@ -1,82 +1,85 @@
|
|||||||
with-expecter: true
|
dir: internal/mocks/{{.InterfaceDirRelative}}
|
||||||
dir: "internal/mocks/{{.InterfaceDirRelative}}"
|
structname: Mock{{.InterfaceName}}
|
||||||
mockname: "Mock{{.InterfaceName}}"
|
pkgname: '{{.SrcPackageName}}'
|
||||||
outpkg: "{{.PackageName}}"
|
filename: 'mock_{{.InterfaceName}}.go'
|
||||||
|
template: testify
|
||||||
|
template-data:
|
||||||
|
unroll-variadic: true
|
||||||
packages:
|
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/geolocation:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/geolocation"
|
dir: internal/mocks/geolocation
|
||||||
outpkg: mocks_geolocation
|
pkgname: mocks_geolocation
|
||||||
interfaces:
|
interfaces:
|
||||||
Client:
|
Client: {}
|
||||||
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:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/cups"
|
dir: internal/mocks/cups
|
||||||
outpkg: mocks_cups
|
pkgname: mocks_cups
|
||||||
interfaces:
|
interfaces:
|
||||||
CUPSClientInterface:
|
CUPSClientInterface: {}
|
||||||
PkHelper:
|
PkHelper:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/cups_pkhelper"
|
dir: internal/mocks/cups_pkhelper
|
||||||
outpkg: mocks_cups_pkhelper
|
pkgname: mocks_cups_pkhelper
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/evdev"
|
dir: internal/mocks/evdev
|
||||||
outpkg: mocks_evdev
|
pkgname: mocks_evdev
|
||||||
interfaces:
|
interfaces:
|
||||||
EvdevDevice:
|
EvdevDevice: {}
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/version"
|
dir: internal/mocks/network
|
||||||
outpkg: mocks_version
|
pkgname: mocks_network
|
||||||
interfaces:
|
interfaces:
|
||||||
VersionFetcher:
|
Backend: {}
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/wlcontext"
|
dir: internal/mocks/wlcontext
|
||||||
outpkg: mocks_wlcontext
|
pkgname: mocks_wlcontext
|
||||||
interfaces:
|
interfaces:
|
||||||
WaylandContext:
|
WaylandContext: {}
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/wlclient"
|
|
||||||
outpkg: mocks_wlclient
|
|
||||||
interfaces:
|
|
||||||
WaylandDisplay:
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/utils"
|
dir: internal/mocks/utils
|
||||||
outpkg: mocks_utils
|
pkgname: mocks_utils
|
||||||
interfaces:
|
interfaces:
|
||||||
AppChecker:
|
AppChecker: {}
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
|
||||||
|
config:
|
||||||
|
dir: internal/mocks/version
|
||||||
|
pkgname: mocks_version
|
||||||
|
interfaces:
|
||||||
|
VersionFetcher: {}
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
||||||
|
config:
|
||||||
|
dir: internal/mocks/wlclient
|
||||||
|
pkgname: mocks_wlclient
|
||||||
|
interfaces:
|
||||||
|
WaylandDisplay: {}
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/plugins:
|
||||||
|
interfaces:
|
||||||
|
GitClient: {}
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness:
|
||||||
|
config:
|
||||||
|
dir: internal/mocks/brightness
|
||||||
|
pkgname: mocks_brightness
|
||||||
|
interfaces:
|
||||||
|
DBusConn: {}
|
||||||
|
github.com/Wifx/gonetworkmanager/v2:
|
||||||
|
interfaces:
|
||||||
|
AccessPoint: {}
|
||||||
|
ActiveConnection: {}
|
||||||
|
Connection: {}
|
||||||
|
Device: {}
|
||||||
|
DeviceWireless: {}
|
||||||
|
IP4Config: {}
|
||||||
|
NetworkManager: {}
|
||||||
|
Settings: {}
|
||||||
|
github.com/godbus/dbus/v5:
|
||||||
|
interfaces:
|
||||||
|
BusObject: {}
|
||||||
|
net:
|
||||||
|
interfaces:
|
||||||
|
Conn: {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.10.1
|
rev: v2.12.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
+50
-26
@@ -3,6 +3,8 @@ BINARY_NAME_INSTALL=dankinstall
|
|||||||
SOURCE_DIR=cmd/dms
|
SOURCE_DIR=cmd/dms
|
||||||
SOURCE_DIR_INSTALL=cmd/dankinstall
|
SOURCE_DIR_INSTALL=cmd/dankinstall
|
||||||
BUILD_DIR=bin
|
BUILD_DIR=bin
|
||||||
|
SHELL_SRC=../quickshell
|
||||||
|
EMBED_DIR=internal/shellembed/dist
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
INSTALL_DIR=$(PREFIX)/bin
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
|
|
||||||
@@ -21,17 +23,46 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
|
|||||||
|
|
||||||
# Architecture to build for dist target (amd64, arm64, or all)
|
# Architecture to build for dist target (amd64, arm64, or all)
|
||||||
ARCH ?= all
|
ARCH ?= all
|
||||||
|
# Target OSes for dist builds
|
||||||
|
DIST_OSES ?= linux freebsd
|
||||||
|
|
||||||
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
ifeq ($(ARCH),all)
|
||||||
|
DIST_ARCHS = amd64 arm64
|
||||||
|
else
|
||||||
|
DIST_ARCHS = $(ARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
# Copy the quickshell UI into the embed dir (gitignored) so tagged builds
|
||||||
|
# can bake it into the binary. Dev-only files are stripped; scripts/ is kept
|
||||||
|
# minus its dev entries, since Theme.qml and BluetoothService.qml run
|
||||||
|
# gtk.sh/qt.sh/bluez-card-profile.lua out of the resolved shell dir.
|
||||||
|
# tar -h dereferences the DankCommon submodule symlink; go:embed rejects
|
||||||
|
# symlinks. .qmlls.ini is excluded at copy time: it's a symlink into the
|
||||||
|
# quickshell runtime VFS and dereferencing it fails whenever the shell isn't
|
||||||
|
# running.
|
||||||
|
sync-shell:
|
||||||
|
@test -e $(SHELL_SRC)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
|
||||||
|
@rm -rf $(EMBED_DIR)
|
||||||
|
@mkdir -p $(EMBED_DIR)
|
||||||
|
@tar -C $(SHELL_SRC) --exclude=.qmlls.ini -chf - . | tar -C $(EMBED_DIR) -xf -
|
||||||
|
@rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github
|
||||||
|
@find $(EMBED_DIR) -type d \( -name .claude -o -name .vscode \) -prune -exec rm -rf {} +
|
||||||
|
@rm -f $(EMBED_DIR)/AGENTS.md $(EMBED_DIR)/qmlformat-all.sh
|
||||||
|
@rm -f $(EMBED_DIR)/scripts/i18nsync.py $(EMBED_DIR)/scripts/build-vscode-vsix.sh $(EMBED_DIR)/scripts/qmllint-entrypoints.sh
|
||||||
|
@rm -f $(EMBED_DIR)/scripts/spam-notifications.sh $(EMBED_DIR)/scripts/verify-notifications.sh
|
||||||
|
@rm -f $(EMBED_DIR)/translations/*.py $(EMBED_DIR)/translations/WORKFLOW.md
|
||||||
|
@cd $(EMBED_DIR) && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum | cut -c1-16 > .dankrev
|
||||||
|
|
||||||
# Build the main binary (dms)
|
# Build the main binary (dms)
|
||||||
build:
|
build: sync-shell
|
||||||
@echo "Building $(BINARY_NAME)..."
|
@echo "Building $(BINARY_NAME)..."
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
dankinstall:
|
dankinstall:
|
||||||
@@ -40,26 +71,18 @@ dankinstall:
|
|||||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
||||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
||||||
|
|
||||||
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
|
||||||
dist:
|
dist: sync-shell
|
||||||
ifeq ($(ARCH),all)
|
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
|
||||||
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@echo "Building for linux/amd64..."
|
@for os in $(DIST_OSES); do \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
for arch in $(DIST_ARCHS); do \
|
||||||
@echo "Building for linux/arm64..."
|
echo "Building for $$os/$$arch..."; \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
|
||||||
@echo "Distribution builds complete:"
|
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
done; \
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
done
|
||||||
else
|
@echo "Distribution builds complete"
|
||||||
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
@echo "Building for linux/$(ARCH)..."
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
|
|
||||||
@echo "Distribution build complete:"
|
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
@@ -99,7 +122,7 @@ uninstall-dankinstall:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@rm -rf $(BUILD_DIR)
|
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -141,10 +164,11 @@ print-version:
|
|||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " all - Build the main binary (dms) (default)"
|
@echo " all - Build the main binary (dms) (default)"
|
||||||
@echo " build - Build the main binary (dms)"
|
@echo " build - Build the main binary (dms) with the embedded UI"
|
||||||
|
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
|
||||||
@echo " dankinstall - Build dankinstall binary"
|
@echo " dankinstall - Build dankinstall binary"
|
||||||
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
|
||||||
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
|
||||||
@echo " build-all - Build both binaries"
|
@echo " build-all - Build both binaries"
|
||||||
@echo " install - Install dms to $(INSTALL_DIR)"
|
@echo " install - Install dms to $(INSTALL_DIR)"
|
||||||
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
||||||
|
|||||||
+9
-6
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
|
|||||||
Command-line interface and daemon for shell management and system control.
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
**dankinstall**
|
**dankinstall**
|
||||||
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
|
|||||||
- `dms brightness [list|set]` - Control display/monitor brightness
|
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||||
- `dms color pick` - Native color picker (see below)
|
- `dms color pick` - Native color picker (see below)
|
||||||
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
- `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
|
||||||
|
|
||||||
### Color Picker
|
### Color Picker
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ make test # Run tests
|
|||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update/greeter features
|
make dist # Build without update features
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
@@ -170,7 +170,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki
|
|||||||
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
||||||
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
||||||
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
||||||
| `--yes` | `-y` | Required for headless mode — confirms installation without interactive prompts |
|
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
|
||||||
|
|
||||||
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
||||||
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
||||||
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
|
|||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
|
||||||
|
|
||||||
**Arch Linux**
|
**Arch Linux**
|
||||||
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
||||||
@@ -206,7 +206,7 @@ Uses COPR repositories (`avengemedia/danklinux`, `avengemedia/dms`).
|
|||||||
Requires PPA support. Most packages built from source (slow first install).
|
Requires PPA support. Most packages built from source (slow first install).
|
||||||
|
|
||||||
**Debian**
|
**Debian**
|
||||||
Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
|
Debian 13+ (Trixie), testing, and sid. Stable quickshell from Debian (trixie-backports on 13). Other companions via OBS.
|
||||||
|
|
||||||
**openSUSE**
|
**openSUSE**
|
||||||
Most packages available in standard repos. Minimal building required.
|
Most packages available in standard repos. Minimal building required.
|
||||||
@@ -214,4 +214,7 @@ Most packages available in standard repos. Minimal building required.
|
|||||||
**Gentoo**
|
**Gentoo**
|
||||||
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
||||||
|
|
||||||
|
**Void Linux**
|
||||||
|
Uses XBPS with the DMS and DankLinux self-hosted repositories.
|
||||||
|
|
||||||
See installer output for distribution-specific details during installation.
|
See installer output for distribution-specific details during installation.
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ var (
|
|||||||
replaceConfigs []string
|
replaceConfigs []string
|
||||||
replaceConfigsAll bool
|
replaceConfigsAll bool
|
||||||
yes bool
|
yes bool
|
||||||
|
danksearch bool
|
||||||
|
dankcalendar bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -42,13 +44,15 @@ configure passwordless sudo for your user.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri or hyprland (enables headless mode)")
|
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
|
||||||
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
||||||
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
||||||
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
||||||
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
||||||
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
||||||
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
||||||
|
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
|
||||||
|
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -74,6 +78,8 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
|||||||
"replace-configs",
|
"replace-configs",
|
||||||
"replace-configs-all",
|
"replace-configs-all",
|
||||||
"yes",
|
"yes",
|
||||||
|
"danksearch",
|
||||||
|
"dankcalendar",
|
||||||
}
|
}
|
||||||
var set []string
|
var set []string
|
||||||
for _, name := range headlessOnly {
|
for _, name := range headlessOnly {
|
||||||
@@ -95,7 +101,7 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
|||||||
func runHeadless() error {
|
func runHeadless() error {
|
||||||
// Validate required flags
|
// Validate required flags
|
||||||
if compositor == "" {
|
if compositor == "" {
|
||||||
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
|
return fmt.Errorf("--compositor is required for headless mode (niri, hyprland, or mango)")
|
||||||
}
|
}
|
||||||
if term == "" {
|
if term == "" {
|
||||||
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
|
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
|
||||||
@@ -109,6 +115,8 @@ func runHeadless() error {
|
|||||||
ReplaceConfigs: replaceConfigs,
|
ReplaceConfigs: replaceConfigs,
|
||||||
ReplaceConfigsAll: replaceConfigsAll,
|
ReplaceConfigsAll: replaceConfigsAll,
|
||||||
Yes: yes,
|
Yes: yes,
|
||||||
|
DankSearch: danksearch,
|
||||||
|
DankCalendar: dankcalendar,
|
||||||
}
|
}
|
||||||
|
|
||||||
runner := headless.NewRunner(cfg)
|
runner := headless.NewRunner(cfg)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -12,7 +16,7 @@ import (
|
|||||||
var authCmd = &cobra.Command{
|
var authCmd = &cobra.Command{
|
||||||
Use: "auth",
|
Use: "auth",
|
||||||
Short: "Manage DMS authentication sync",
|
Short: "Manage DMS authentication sync",
|
||||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
Long: "Manage PAM/authentication setup for the DMS lock screen",
|
||||||
}
|
}
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
var authSyncCmd = &cobra.Command{
|
||||||
@@ -35,9 +39,142 @@ var authSyncCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authResolveLockCmd = &cobra.Command{
|
||||||
|
Use: "resolve-lock",
|
||||||
|
Short: "Generate the lock-screen PAM config from the system auth stack",
|
||||||
|
Long: "Resolve the distribution's PAM auth stack into a self-contained lock-screen config under the user state directory.\n" +
|
||||||
|
"Runs unprivileged (reads /etc/pam.d, writes to the user's state dir) and is used by the shell as a fallback when /etc/pam.d/dankshell is not managed.\n" +
|
||||||
|
"Prints the path of the generated file.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
quiet, _ := cmd.Flags().GetBool("quiet")
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
if !quiet {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path, err := sharedpam.WriteUserLockscreenPamConfig(logFunc)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error resolving lock-screen PAM config: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(path)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var authListServicesCmd = &cobra.Command{
|
||||||
|
Use: "list-services",
|
||||||
|
Short: "List candidate lock-screen PAM services available on this system",
|
||||||
|
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
services := sharedpam.ListLockscreenPamServices()
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
payload := struct {
|
||||||
|
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
|
||||||
|
}{Services: services}
|
||||||
|
data, err := json.MarshalIndent(payload, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error encoding services: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(services) == 0 {
|
||||||
|
fmt.Println("No candidate lock-screen PAM services found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, s := range services {
|
||||||
|
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var authValidateCmd = &cobra.Command{
|
||||||
|
Use: "validate",
|
||||||
|
Short: "Validate a PAM service file for use by the DMS lock screen",
|
||||||
|
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password or dedicated U2F stack. Exits 1 when the file is not usable.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
path, _ := cmd.Flags().GetString("path")
|
||||||
|
service, _ := cmd.Flags().GetString("service")
|
||||||
|
purpose, _ := cmd.Flags().GetString("purpose")
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if (path == "") == (service == "") {
|
||||||
|
log.Fatalf("Error: exactly one of --path or --service is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if purpose != "password" && purpose != "u2f" {
|
||||||
|
log.Fatalf("Error: --purpose must be password or u2f")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result sharedpam.LockscreenPamValidation
|
||||||
|
switch {
|
||||||
|
case service != "":
|
||||||
|
if purpose == "u2f" {
|
||||||
|
result = sharedpam.ValidateLockscreenU2fPamService(service)
|
||||||
|
} else {
|
||||||
|
result = sharedpam.ValidateLockscreenPamService(service)
|
||||||
|
}
|
||||||
|
case !filepath.IsAbs(path):
|
||||||
|
result = sharedpam.LockscreenPamValidation{
|
||||||
|
Path: path,
|
||||||
|
MissingModules: []string{},
|
||||||
|
Warnings: []string{},
|
||||||
|
Errors: []string{"--path must be an absolute file path"},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if purpose == "u2f" {
|
||||||
|
result = sharedpam.ValidateLockscreenU2fPamPath(path)
|
||||||
|
} else {
|
||||||
|
result = sharedpam.ValidateLockscreenPamPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
data, err := json.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error encoding validation: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
} else {
|
||||||
|
printLockscreenPamValidation(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Valid {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
|
||||||
|
fmt.Printf("Path: %s\n", result.Path)
|
||||||
|
fmt.Printf("Valid: %t\n", result.Valid)
|
||||||
|
fmt.Printf("Has auth: %t\n", result.HasAuth)
|
||||||
|
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
|
||||||
|
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
|
||||||
|
if len(result.MissingModules) > 0 {
|
||||||
|
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
|
||||||
|
}
|
||||||
|
for _, w := range result.Warnings {
|
||||||
|
fmt.Println("⚠ " + w)
|
||||||
|
}
|
||||||
|
for _, e := range result.Errors {
|
||||||
|
fmt.Println("✗ " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
||||||
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
||||||
|
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
|
||||||
|
|
||||||
|
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
|
||||||
|
|
||||||
|
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
|
||||||
|
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
|
||||||
|
authValidateCmd.Flags().String("purpose", "password", "Validation purpose: password or u2f")
|
||||||
|
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAuth(nonInteractive bool) error {
|
func syncAuth(nonInteractive bool) error {
|
||||||
@@ -75,3 +212,31 @@ func syncAuthInTerminal(nonInteractive bool) error {
|
|||||||
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
||||||
return runCommandInTerminal(shellCmd)
|
return runCommandInTerminal(shellCmd)
|
||||||
}
|
}
|
||||||
|
func runCommandInTerminal(shellCmd string) error {
|
||||||
|
terminals := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
|
||||||
|
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||||
|
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
|
||||||
|
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"kitty", []string{"bash", "-c", shellCmd}},
|
||||||
|
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||||
|
}
|
||||||
|
for _, t := range terminals {
|
||||||
|
if _, err := exec.LookPath(t.name); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd := exec.Command(t.name, t.args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ var clipPasteCmd = &cobra.Command{
|
|||||||
Run: runClipPaste,
|
Run: runClipPaste,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clipSendPasteCmd = &cobra.Command{
|
||||||
|
Use: "send-paste",
|
||||||
|
Short: "Send a paste keystroke to the focused window",
|
||||||
|
Long: "Emulate ctrl+v (or ctrl+shift+v with --shift) via a virtual keyboard. Works without server.",
|
||||||
|
Run: runClipSendPaste,
|
||||||
|
}
|
||||||
|
|
||||||
|
var clipSendPasteShift bool
|
||||||
|
|
||||||
var clipWatchCmd = &cobra.Command{
|
var clipWatchCmd = &cobra.Command{
|
||||||
Use: "watch [command]",
|
Use: "watch [command]",
|
||||||
Short: "Watch clipboard for changes",
|
Short: "Watch clipboard for changes",
|
||||||
@@ -216,8 +225,10 @@ func init() {
|
|||||||
|
|
||||||
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
||||||
|
|
||||||
|
clipSendPasteCmd.Flags().BoolVarP(&clipSendPasteShift, "shift", "s", false, "Send ctrl+shift+v (terminal paste)")
|
||||||
|
|
||||||
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
||||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipSendPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
@@ -314,6 +325,12 @@ func runClipPaste(cmd *cobra.Command, args []string) {
|
|||||||
os.Stdout.Write(data)
|
os.Stdout.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runClipSendPaste(cmd *cobra.Command, args []string) {
|
||||||
|
if err := clipboard.SendPasteKeystroke(clipSendPasteShift); err != nil {
|
||||||
|
log.Fatalf("send-paste: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runClipWatch(cmd *cobra.Command, args []string) {
|
func runClipWatch(cmd *cobra.Command, args []string) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ var (
|
|||||||
var colorCmd = &cobra.Command{
|
var colorCmd = &cobra.Command{
|
||||||
Use: "color",
|
Use: "color",
|
||||||
Short: "Color utilities",
|
Short: "Color utilities",
|
||||||
Long: "Color utilities including picking colors from the screen",
|
Long: `Color utilities including picking colors from the screen.
|
||||||
|
|
||||||
|
This is the screen eyedropper CLI. To open the in-shell color modal, use:
|
||||||
|
dms ipc call color-picker toggle
|
||||||
|
|
||||||
|
See: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var colorPickCmd = &cobra.Command{
|
var colorPickCmd = &cobra.Command{
|
||||||
@@ -29,6 +34,9 @@ var colorPickCmd = &cobra.Command{
|
|||||||
|
|
||||||
Click on any pixel to capture its color, or press Escape to cancel.
|
Click on any pixel to capture its color, or press Escape to cancel.
|
||||||
|
|
||||||
|
This is the screen eyedropper CLI. To open the in-shell color modal, use:
|
||||||
|
dms ipc call color-picker toggle
|
||||||
|
|
||||||
Output format flags (mutually exclusive, default: --hex):
|
Output format flags (mutually exclusive, default: --hex):
|
||||||
--hex - Hexadecimal (#RRGGBB)
|
--hex - Hexadecimal (#RRGGBB)
|
||||||
--rgb - RGB values (R G B)
|
--rgb - RGB values (R G B)
|
||||||
|
|||||||
+271
-86
@@ -6,10 +6,10 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,68 +19,16 @@ var versionCmd = &cobra.Command{
|
|||||||
Run: runVersion,
|
Run: runVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
var runCmd = &cobra.Command{
|
|
||||||
Use: "run",
|
|
||||||
Short: "Launch quickshell with DMS configuration",
|
|
||||||
Long: "Launch quickshell with DMS configuration (qs -c dms)",
|
|
||||||
PreRunE: findConfig,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
daemon, _ := cmd.Flags().GetBool("daemon")
|
|
||||||
session, _ := cmd.Flags().GetBool("session")
|
|
||||||
if v, _ := cmd.Flags().GetString("log-level"); v != "" {
|
|
||||||
if err := os.Setenv("DMS_LOG_LEVEL", v); err != nil {
|
|
||||||
log.Fatalf("Failed to set DMS_LOG_LEVEL: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, _ := cmd.Flags().GetString("log-file"); v != "" {
|
|
||||||
if err := os.Setenv("DMS_LOG_FILE", v); err != nil {
|
|
||||||
log.Fatalf("Failed to set DMS_LOG_FILE: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.ApplyEnvOverrides()
|
|
||||||
config.CleanupStrayHyprlandConfFile(log.Infof)
|
|
||||||
if daemon {
|
|
||||||
runShellDaemon(session)
|
|
||||||
} else {
|
|
||||||
runShellInteractive(session)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var restartCmd = &cobra.Command{
|
|
||||||
Use: "restart",
|
|
||||||
Short: "Restart quickshell with DMS configuration",
|
|
||||||
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
|
|
||||||
PreRunE: findConfig,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
restartShell()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var restartDetachedCmd = &cobra.Command{
|
|
||||||
Use: "restart-detached <pid>",
|
|
||||||
Hidden: true,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
PreRunE: findConfig,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
runDetachedRestart(args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var killCmd = &cobra.Command{
|
|
||||||
Use: "kill",
|
|
||||||
Short: "Kill running DMS shell processes",
|
|
||||||
Long: "Kill all running quickshell processes with DMS configuration",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
killShell()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
|
Long: `Send IPC commands to the running DMS shell.
|
||||||
|
|
||||||
|
dms ipc call <target> <function> [args...] invoke a command
|
||||||
|
dms ipc list list all targets and functions
|
||||||
|
|
||||||
|
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
@@ -88,11 +36,21 @@ var ipcCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ipcListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all IPC targets and functions",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
printIPCHelp()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
ipcCmd.AddCommand(ipcListCmd)
|
||||||
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
_ = findConfig(cmd, args)
|
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
})
|
})
|
||||||
|
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
|
||||||
|
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugSrvCmd = &cobra.Command{
|
var debugSrvCmd = &cobra.Command{
|
||||||
@@ -171,10 +129,22 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pluginsUpdateCmd = &cobra.Command{
|
var pluginsUpdateCmd = &cobra.Command{
|
||||||
Use: "update <plugin-id>",
|
Use: "update [plugin-id]",
|
||||||
Short: "Update a plugin by ID",
|
Short: "Update a plugin by ID, or all plugins",
|
||||||
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
|
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
updateAll, _ := cmd.Flags().GetBool("all")
|
||||||
|
if updateAll {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -182,6 +152,26 @@ var pluginsUpdateCmd = &cobra.Command{
|
|||||||
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
checkOnly, _ := cmd.Flags().GetBool("check")
|
||||||
|
updateAll, _ := cmd.Flags().GetBool("all")
|
||||||
|
if checkOnly {
|
||||||
|
if updateAll {
|
||||||
|
if err := checkAllPluginsCLI(); err != nil {
|
||||||
|
log.Fatalf("Error checking updates: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := checkPluginCLI(args[0]); err != nil {
|
||||||
|
log.Fatalf("Error checking update: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if updateAll {
|
||||||
|
if err := updateAllPluginsCLI(); err != nil {
|
||||||
|
log.Fatalf("Error updating plugins: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := updatePluginCLI(args[0]); err != nil {
|
if err := updatePluginCLI(args[0]); err != nil {
|
||||||
log.Fatalf("Error updating plugin: %v", err)
|
log.Fatalf("Error updating plugin: %v", err)
|
||||||
}
|
}
|
||||||
@@ -217,29 +207,41 @@ func formatVersion(version string) string {
|
|||||||
return fmt.Sprintf("dms %s", version)
|
return fmt.Sprintf("dms %s", version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var baseVersionRe = regexp.MustCompile(`^([\d.]+)`)
|
||||||
|
|
||||||
|
// Installed UI trees, for builds without an embedded UI.
|
||||||
|
var shellVersionPaths = []string{
|
||||||
|
"/usr/share/quickshell/dms/VERSION",
|
||||||
|
"/usr/local/share/quickshell/dms/VERSION",
|
||||||
|
"/etc/xdg/quickshell/dms/VERSION",
|
||||||
|
}
|
||||||
|
|
||||||
func getBaseVersion() string {
|
func getBaseVersion() string {
|
||||||
paths := []string{
|
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
|
||||||
"/usr/share/quickshell/dms/VERSION",
|
return ver
|
||||||
"/usr/local/share/quickshell/dms/VERSION",
|
|
||||||
"/etc/xdg/quickshell/dms/VERSION",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range shellVersionPaths {
|
||||||
if content, err := os.ReadFile(path); err == nil {
|
content, err := os.ReadFile(path)
|
||||||
ver := strings.TrimSpace(string(content))
|
if err != nil {
|
||||||
ver = strings.TrimPrefix(ver, "v")
|
continue
|
||||||
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
|
}
|
||||||
if matches := re.FindStringSubmatch(ver); matches != nil {
|
if ver := parseBaseVersion(string(content)); ver != "" {
|
||||||
return matches[1]
|
return ver
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
|
||||||
return "1.0.2"
|
return "1.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBaseVersion(raw string) string {
|
||||||
|
matches := baseVersionRe.FindStringSubmatch(strings.TrimPrefix(strings.TrimSpace(raw), "v"))
|
||||||
|
if matches == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
server.CLIVersion = Version
|
server.CLIVersion = Version
|
||||||
return server.Start(true)
|
return server.Start(true)
|
||||||
@@ -267,6 +269,13 @@ func browsePlugins() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feedback := plugins.FetchFeedback()
|
||||||
|
|
||||||
|
nameByID := make(map[string]string, len(pluginList))
|
||||||
|
for _, plugin := range pluginList {
|
||||||
|
nameByID[plugin.ID] = plugin.Name
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList))
|
fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList))
|
||||||
for _, plugin := range pluginList {
|
for _, plugin := range pluginList {
|
||||||
installed, _ := manager.IsInstalled(plugin)
|
installed, _ := manager.IsInstalled(plugin)
|
||||||
@@ -290,6 +299,26 @@ func browsePlugins() error {
|
|||||||
if len(plugin.Dependencies) > 0 {
|
if len(plugin.Dependencies) > 0 {
|
||||||
fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", "))
|
fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", "))
|
||||||
}
|
}
|
||||||
|
if fb, ok := feedback[plugin.ID]; ok {
|
||||||
|
fmt.Printf(" Upvotes: %d\n", fb.Upvotes)
|
||||||
|
if len(fb.Status) > 0 {
|
||||||
|
fmt.Printf(" Status: %s\n", strings.Join(fb.Status, ", "))
|
||||||
|
}
|
||||||
|
if fb.IssueURL != "" {
|
||||||
|
fmt.Printf(" Discuss: %s\n", fb.IssueURL)
|
||||||
|
}
|
||||||
|
if len(fb.Similar) > 0 {
|
||||||
|
names := make([]string, len(fb.Similar))
|
||||||
|
for i, id := range fb.Similar {
|
||||||
|
if name, ok := nameByID[id]; ok {
|
||||||
|
names[i] = name
|
||||||
|
} else {
|
||||||
|
names[i] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf(" Related: %s\n", strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +359,11 @@ func listInstalledPlugins() error {
|
|||||||
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
|
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
|
||||||
for _, id := range installedNames {
|
for _, id := range installedNames {
|
||||||
if plugin, ok := pluginMap[id]; ok {
|
if plugin, ok := pluginMap[id]; ok {
|
||||||
fmt.Printf(" %s\n", plugin.Name)
|
hasUpdateStr := ""
|
||||||
|
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
|
||||||
|
hasUpdateStr = " (update available)"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
|
||||||
fmt.Printf(" ID: %s\n", plugin.ID)
|
fmt.Printf(" ID: %s\n", plugin.ID)
|
||||||
fmt.Printf(" Category: %s\n", plugin.Category)
|
fmt.Printf(" Category: %s\n", plugin.Category)
|
||||||
fmt.Printf(" Author: %s\n", plugin.Author)
|
fmt.Printf(" Author: %s\n", plugin.Author)
|
||||||
@@ -510,13 +543,164 @@ func updatePluginCLI(idOrName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAllPluginsCLI() error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list installed plugins: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, _ := registry.List()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for _, pluginID := range installed {
|
||||||
|
plugin := plugins.FindByIDOrName(pluginID, pluginList)
|
||||||
|
if plugin != nil {
|
||||||
|
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
if err := manager.Update(*plugin); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "cannot update system plugin") {
|
||||||
|
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
|
||||||
|
} else {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Updating plugin: %s\n", pluginID)
|
||||||
|
if err := manager.UpdateByIDOrName(pluginID); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "cannot update system plugin") {
|
||||||
|
fmt.Printf("Skipping system plugin: %s\n", pluginID)
|
||||||
|
} else {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
for _, err := range errs {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to update some plugins")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPluginCLI(idOrName string) error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, _ := registry.List()
|
||||||
|
plugin := plugins.FindByIDOrName(idOrName, pluginList)
|
||||||
|
|
||||||
|
if plugin != nil {
|
||||||
|
installed, err := manager.IsInstalled(*plugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check install status: %w", err)
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasUpdates {
|
||||||
|
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dummyPlugin := plugins.Plugin{ID: idOrName}
|
||||||
|
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasUpdates {
|
||||||
|
fmt.Printf("Update available for plugin: %s\n", idOrName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin is up to date: %s\n", idOrName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAllPluginsCLI() error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list installed plugins: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, _ := registry.List()
|
||||||
|
|
||||||
|
var count int
|
||||||
|
for _, pluginID := range installed {
|
||||||
|
plugin := plugins.FindByIDOrName(pluginID, pluginList)
|
||||||
|
var hasUpdates bool
|
||||||
|
var name string
|
||||||
|
|
||||||
|
if plugin != nil {
|
||||||
|
name = plugin.Name
|
||||||
|
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
|
||||||
|
} else {
|
||||||
|
name = pluginID
|
||||||
|
dummyPlugin := plugins.Plugin{ID: pluginID}
|
||||||
|
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasUpdates {
|
||||||
|
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
|
||||||
|
} else {
|
||||||
|
fmt.Println("All plugins are up to date.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getCommonCommands() []*cobra.Command {
|
func getCommonCommands() []*cobra.Command {
|
||||||
return []*cobra.Command{
|
commands := shellApp.Commands()
|
||||||
|
return append(commands, []*cobra.Command{
|
||||||
versionCmd,
|
versionCmd,
|
||||||
runCmd,
|
|
||||||
restartCmd,
|
|
||||||
restartDetachedCmd,
|
|
||||||
killCmd,
|
|
||||||
ipcCmd,
|
ipcCmd,
|
||||||
debugSrvCmd,
|
debugSrvCmd,
|
||||||
pluginsCmd,
|
pluginsCmd,
|
||||||
@@ -527,6 +711,7 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
colorCmd,
|
colorCmd,
|
||||||
|
qrCmd,
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
notifyCmd,
|
notifyCmd,
|
||||||
@@ -542,5 +727,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
trashCmd,
|
trashCmd,
|
||||||
systemCmd,
|
systemCmd,
|
||||||
switchUserCmd,
|
switchUserCmd,
|
||||||
}
|
}...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
"cursor.lua",
|
"cursor.lua",
|
||||||
"windowrules.lua",
|
"windowrules.lua",
|
||||||
"cursor.kdl",
|
"cursor.kdl",
|
||||||
|
"layout.kdl",
|
||||||
"outputs.kdl",
|
"outputs.kdl",
|
||||||
"binds.kdl",
|
"binds.kdl",
|
||||||
"cursor.conf",
|
"cursor.conf",
|
||||||
|
"layout.conf",
|
||||||
"outputs.conf",
|
"outputs.conf",
|
||||||
"binds.conf",
|
"binds.conf",
|
||||||
}, cobra.ShellCompDirectiveNoFileComp
|
}, cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -54,8 +56,10 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IncludeResult struct {
|
type IncludeResult struct {
|
||||||
Exists bool `json:"exists"`
|
Exists bool `json:"exists"`
|
||||||
Included bool `json:"included"`
|
Included bool `json:"included"`
|
||||||
|
ConfigFormat string `json:"configFormat,omitempty"`
|
||||||
|
ReadOnly bool `json:"readOnly,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func runResolveInclude(cmd *cobra.Command, args []string) {
|
func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||||
@@ -70,7 +74,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
|
|||||||
result, err = checkHyprlandInclude(filename)
|
result, err = checkHyprlandInclude(filename)
|
||||||
case "niri":
|
case "niri":
|
||||||
result, err = checkNiriInclude(filename)
|
result, err = checkNiriInclude(filename)
|
||||||
case "mangowc", "dwl", "mango":
|
case "mangowc", "mango":
|
||||||
result, err = checkMangoWCInclude(filename)
|
result, err = checkMangoWCInclude(filename)
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unknown compositor: %s", compositor)
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
@@ -85,10 +89,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
||||||
if err != nil {
|
|
||||||
return IncludeResult{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(configDir, "dms", filename)
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
result := IncludeResult{}
|
result := IncludeResult{}
|
||||||
@@ -106,6 +107,8 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
|||||||
|
|
||||||
mainLua := filepath.Join(configDir, "hyprland.lua")
|
mainLua := filepath.Join(configDir, "hyprland.lua")
|
||||||
if _, err := os.Stat(mainLua); err == nil {
|
if _, err := os.Stat(mainLua); err == nil {
|
||||||
|
result.ConfigFormat = "lua"
|
||||||
|
result.ReadOnly = false
|
||||||
processedLua := make(map[string]bool)
|
processedLua := make(map[string]bool)
|
||||||
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
|
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
|
||||||
result.Included = true
|
result.Included = true
|
||||||
@@ -115,6 +118,10 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
|||||||
|
|
||||||
mainConf := filepath.Join(configDir, "hyprland.conf")
|
mainConf := filepath.Join(configDir, "hyprland.conf")
|
||||||
if _, err := os.Stat(mainConf); err == nil {
|
if _, err := os.Stat(mainConf); err == nil {
|
||||||
|
if result.ConfigFormat == "" {
|
||||||
|
result.ConfigFormat = "hyprlang"
|
||||||
|
result.ReadOnly = true
|
||||||
|
}
|
||||||
processed := make(map[string]bool)
|
processed := make(map[string]bool)
|
||||||
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
|
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
|
||||||
result.Included = true
|
result.Included = true
|
||||||
@@ -183,10 +190,7 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkNiriInclude(filename string) (IncludeResult, error) {
|
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
|
||||||
if err != nil {
|
|
||||||
return IncludeResult{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(configDir, "dms", filename)
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
result := IncludeResult{}
|
result := IncludeResult{}
|
||||||
@@ -262,10 +266,7 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
|
||||||
if err != nil {
|
|
||||||
return IncludeResult{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(configDir, "dms", filename)
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
result := IncludeResult{}
|
result := IncludeResult{}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ const (
|
|||||||
catConfigFiles
|
catConfigFiles
|
||||||
catServices
|
catServices
|
||||||
catEnvironment
|
catEnvironment
|
||||||
|
catFonts
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c category) String() string {
|
func (c category) String() string {
|
||||||
@@ -147,6 +148,8 @@ func (c category) String() string {
|
|||||||
return "Services"
|
return "Services"
|
||||||
case catEnvironment:
|
case catEnvironment:
|
||||||
return "Environment"
|
return "Environment"
|
||||||
|
case catFonts:
|
||||||
|
return "Fonts"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
@@ -213,6 +216,7 @@ func runDoctor(cmd *cobra.Command, args []string) {
|
|||||||
checkConfigurationFiles(),
|
checkConfigurationFiles(),
|
||||||
checkSystemdServices(),
|
checkSystemdServices(),
|
||||||
checkEnvironmentVars(),
|
checkEnvironmentVars(),
|
||||||
|
checkFonts(),
|
||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -316,6 +320,7 @@ func checkEnvironmentVars() []checkResult {
|
|||||||
var results []checkResult
|
var results []checkResult
|
||||||
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
|
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
|
||||||
results = append(results, checkEnvVar("QS_ICON_THEME")...)
|
results = append(results, checkEnvVar("QS_ICON_THEME")...)
|
||||||
|
results = append(results, checkXDGMenuPrefix()...)
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +335,29 @@ func checkEnvVar(name string) []checkResult {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkXDGMenuPrefix() []checkResult {
|
||||||
|
menuPrefix := os.Getenv("XDG_MENU_PREFIX")
|
||||||
|
if menuPrefix != "" {
|
||||||
|
if checkXDGMenuFile(menuPrefix) {
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, menuPrefix, "", doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, fmt.Sprintf("%s (menu file not found)", menuPrefix), fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure /etc/xdg/menus/%sapplications.menu exists.", menuPrefix), doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("keditfiletype"); err == nil {
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, "Not set", "Dolphin file associations and 'Open with…' dialog may be empty. Set XDG_MENU_PREFIX=plasma- in your compositor's environment block.", doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
if doctorVerbose {
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, "Not set", "", doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkXDGMenuFile(prefix string) bool {
|
||||||
|
menuPath := fmt.Sprintf("/etc/xdg/menus/%sapplications.menu", prefix)
|
||||||
|
_, err := os.Stat(menuPath)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func readOSRelease() map[string]string {
|
func readOSRelease() map[string]string {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
data, err := os.ReadFile("/etc/os-release")
|
data, err := os.ReadFile("/etc/os-release")
|
||||||
@@ -373,12 +401,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDMSShellVersion() (version, path string) {
|
func getDMSShellVersion() (version, path string) {
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
||||||
versionFile := filepath.Join(configPath, "VERSION")
|
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
|
||||||
if data, err := os.ReadFile(versionFile); err == nil {
|
if data, err := os.ReadFile(versionFile); err == nil {
|
||||||
return strings.TrimSpace(string(data)), configPath
|
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
|
||||||
}
|
}
|
||||||
return "installed", configPath
|
return "installed", shellApp.ConfigPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
||||||
@@ -422,8 +450,8 @@ func checkDMSInstallation() []checkResult {
|
|||||||
var results []checkResult
|
var results []checkResult
|
||||||
|
|
||||||
dmsPath := ""
|
dmsPath := ""
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
||||||
dmsPath = configPath
|
dmsPath = shellApp.ConfigPath()
|
||||||
} else if path, err := config.LocateDMSConfig(); err == nil {
|
} else if path, err := config.LocateDMSConfig(); err == nil {
|
||||||
dmsPath = path
|
dmsPath = path
|
||||||
}
|
}
|
||||||
@@ -553,6 +581,8 @@ func detectRunningWM() string {
|
|||||||
return "Hyprland"
|
return "Hyprland"
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
return "niri"
|
return "niri"
|
||||||
|
case os.Getenv("MANGO_INSTANCE_SIGNATURE") != "":
|
||||||
|
return "MangoWC"
|
||||||
case os.Getenv("MIRACLESOCK") != "":
|
case os.Getenv("MIRACLESOCK") != "":
|
||||||
return "Miracle WM"
|
return "Miracle WM"
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
@@ -803,6 +833,8 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
|
|||||||
return "systemd-networkd"
|
return "systemd-networkd"
|
||||||
case network.BackendConnMan:
|
case network.BackendConnMan:
|
||||||
return "ConnMan"
|
return "ConnMan"
|
||||||
|
case network.BackendWpaSupplicant:
|
||||||
|
return "wpa_supplicant"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -880,6 +912,7 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
{"cava", "cava", "Audio visualizer", true},
|
{"cava", "cava", "Audio visualizer", true},
|
||||||
{"khal", "khal", "Calendar events", false},
|
{"khal", "khal", "Calendar events", false},
|
||||||
{"danksearch", "dsearch", "File search", false},
|
{"danksearch", "dsearch", "File search", false},
|
||||||
|
{"dankcalendar", "dcal", "Calendar app", false},
|
||||||
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,9 +980,12 @@ func checkSystemdServices() []checkResult {
|
|||||||
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
|
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
|
case dmsState.active == "failed":
|
||||||
|
status = statusError
|
||||||
|
case dmsState.active == "active":
|
||||||
case dmsState.enabled == "disabled":
|
case dmsState.enabled == "disabled":
|
||||||
status, message = statusWarn, "Disabled"
|
status, message = statusWarn, "Disabled"
|
||||||
case dmsState.active == "failed" || dmsState.active == "inactive":
|
case dmsState.active == "inactive":
|
||||||
status = statusError
|
status = statusError
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
|
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
|
||||||
@@ -1132,3 +1168,100 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkFonts() []checkResult {
|
||||||
|
var results []checkResult
|
||||||
|
url := doctorDocsURL + "#fonts"
|
||||||
|
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
|
||||||
|
|
||||||
|
fontFamily := "Inter Variable"
|
||||||
|
monoFontFamily := "Fira Code"
|
||||||
|
|
||||||
|
if data, err := os.ReadFile(settingsPath); err == nil {
|
||||||
|
var settings struct {
|
||||||
|
FontFamily string `json:"fontFamily"`
|
||||||
|
MonoFontFamily string `json:"monoFontFamily"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &settings); err == nil {
|
||||||
|
if settings.FontFamily != "" {
|
||||||
|
fontFamily = settings.FontFamily
|
||||||
|
}
|
||||||
|
if settings.MonoFontFamily != "" {
|
||||||
|
monoFontFamily = settings.MonoFontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.CommandExists("fc-list") {
|
||||||
|
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify if fonts are cached.", url})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve font list
|
||||||
|
output, err := exec.Command("fc-list", ":", "family").Output()
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr := string(output)
|
||||||
|
if len(strings.TrimSpace(outStr)) == 0 {
|
||||||
|
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerFonts := strings.ToLower(outStr)
|
||||||
|
|
||||||
|
// Helper to check if a font exists
|
||||||
|
hasFont := func(name string) bool {
|
||||||
|
target := strings.ToLower(strings.TrimSpace(name))
|
||||||
|
if target == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(lowerFonts, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Each line can have comma-separated families
|
||||||
|
families := strings.Split(line, ",")
|
||||||
|
for _, fam := range families {
|
||||||
|
if strings.TrimSpace(fam) == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal Font Check
|
||||||
|
if hasFont(fontFamily) {
|
||||||
|
results = append(results, checkResult{catFonts, "Normal Font", statusOK, fontFamily, "Available", url})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catFonts, "Normal Font", statusWarn,
|
||||||
|
fmt.Sprintf("'%s' not found", fontFamily),
|
||||||
|
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monospace Font Check
|
||||||
|
if hasFont(monoFontFamily) {
|
||||||
|
results = append(results, checkResult{catFonts, "Monospace Font", statusOK, monoFontFamily, "Available", url})
|
||||||
|
} else {
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catFonts, "Monospace Font", statusWarn,
|
||||||
|
fmt.Sprintf("'%s' not found", monoFontFamily),
|
||||||
|
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var updateCmd = &cobra.Command{
|
|||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Update DankMaterialShell to the latest version",
|
Short: "Update DankMaterialShell to the latest version",
|
||||||
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
|
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
|
||||||
PreRunE: findConfig,
|
PreRunE: shellApp.ResolveConfig,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
runUpdate()
|
runUpdate()
|
||||||
},
|
},
|
||||||
@@ -98,7 +98,7 @@ func runUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Update complete! Restarting DMS...")
|
log.Info("Update complete! Restarting DMS...")
|
||||||
restartShell()
|
shellApp.Restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateArchLinux() error {
|
func updateArchLinux() error {
|
||||||
@@ -250,6 +250,10 @@ func updateOtherDistros() error {
|
|||||||
return fmt.Errorf("failed to fetch changes: %w", err)
|
return fmt.Errorf("failed to fetch changes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateSubmodules(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if currentTag != "" {
|
if currentTag != "" {
|
||||||
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
||||||
latestTagOutput, err := latestTagCmd.Output()
|
latestTagOutput, err := latestTagCmd.Output()
|
||||||
@@ -291,6 +295,10 @@ func updateOtherDistros() error {
|
|||||||
return fmt.Errorf("update cancelled")
|
return fmt.Errorf("update cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateSubmodules(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -320,10 +328,21 @@ func updateOtherDistros() error {
|
|||||||
return fmt.Errorf("update cancelled")
|
return fmt.Errorf("update cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateSubmodules(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nUpdate complete!")
|
fmt.Println("\nUpdate complete!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateSubmodules() error {
|
||||||
|
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||||
|
submoduleCmd.Stdout = os.Stdout
|
||||||
|
submoduleCmd.Stderr = os.Stderr
|
||||||
|
return submoduleCmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func offerReclone(dmsPath string) bool {
|
func offerReclone(dmsPath string) bool {
|
||||||
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
@@ -342,7 +361,7 @@ func offerReclone(dmsPath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Cloning fresh copy...")
|
fmt.Println("Cloning fresh copy...")
|
||||||
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||||
cloneCmd.Stdout = os.Stdout
|
cloneCmd.Stdout = os.Stdout
|
||||||
cloneCmd.Stderr = os.Stderr
|
cloneCmd.Stderr = os.Stderr
|
||||||
if err := cloneCmd.Run(); err != nil {
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
|||||||
+26
-1780
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
var calls []string
|
|
||||||
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
||||||
if dmsPath != "/tmp/dms" {
|
|
||||||
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
|
||||||
}
|
|
||||||
if compositor != "niri" {
|
|
||||||
t.Fatalf("unexpected compositor %q", compositor)
|
|
||||||
}
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
calls = append(calls, "configs")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotOptions sharedpam.SyncAuthOptions
|
|
||||||
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
gotOptions = options
|
|
||||||
calls = append(calls, "auth")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
|
||||||
ForceGreeterAuth: true,
|
|
||||||
}, func() {
|
|
||||||
calls = append(calls, "before-auth")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantCalls := []string{"configs", "before-auth", "auth"}
|
|
||||||
if !reflect.DeepEqual(calls, wantCalls) {
|
|
||||||
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
|
||||||
}
|
|
||||||
if !gotOptions.ForceGreeterAuth {
|
|
||||||
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
|
||||||
return errors.New("config sync failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
authCalled := false
|
|
||||||
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
|
||||||
authCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
|
||||||
if err == nil || err.Error() != "config sync failed" {
|
|
||||||
t.Fatalf("expected config sync error, got %v", err)
|
|
||||||
}
|
|
||||||
if authCalled {
|
|
||||||
t.Fatal("expected auth sync not to run after config sync failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,10 +38,17 @@ var matugenCheckCmd = &cobra.Command{
|
|||||||
Run: runMatugenCheck,
|
Run: runMatugenCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matugenPreviewCmd = &cobra.Command{
|
||||||
|
Use: "preview",
|
||||||
|
Short: "Preview Matugen scheme colors without applying them",
|
||||||
|
Run: runMatugenPreview,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
matugenCmd.AddCommand(matugenQueueCmd)
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
matugenCmd.AddCommand(matugenCheckCmd)
|
matugenCmd.AddCommand(matugenCheckCmd)
|
||||||
|
matugenCmd.AddCommand(matugenPreviewCmd)
|
||||||
|
|
||||||
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||||
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||||
@@ -62,6 +69,8 @@ func init() {
|
|||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
||||||
|
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
|
||||||
|
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
@@ -200,3 +209,17 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runMatugenPreview(cmd *cobra.Command, args []string) {
|
||||||
|
sourceColor, _ := cmd.Flags().GetString("source-color")
|
||||||
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
|
previews, err := matugen.PreviewSchemes(sourceColor, contrast)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate Matugen previews: %v", err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(previews)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal Matugen previews: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/qrcode"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
qrEcc string
|
||||||
|
qrVersion int
|
||||||
|
qrOutput string
|
||||||
|
qrStdout bool
|
||||||
|
qrClipboard bool
|
||||||
|
qrCopyText bool
|
||||||
|
qrRender bool
|
||||||
|
qrNoRender bool
|
||||||
|
qrInvert bool
|
||||||
|
qrQuietZone int
|
||||||
|
qrModuleSize int
|
||||||
|
qrFg string
|
||||||
|
qrBg string
|
||||||
|
qrTransparent bool
|
||||||
|
qrBorder int
|
||||||
|
qrShape string
|
||||||
|
qrLogo string
|
||||||
|
qrLogoScale int
|
||||||
|
|
||||||
|
qrWifiPassword string
|
||||||
|
qrWifiSecurity string
|
||||||
|
qrWifiHidden bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var qrCmd = &cobra.Command{
|
||||||
|
Use: "qr [text]",
|
||||||
|
Short: "Generate QR codes",
|
||||||
|
Long: `Generate a QR code from text, stdin, or a WiFi network.
|
||||||
|
|
||||||
|
By default the code is rendered to the terminal when stdout is a TTY, or
|
||||||
|
written as PNG bytes to stdout when piped. Use flags to also copy to the
|
||||||
|
clipboard, save a PNG, or tune encoding and colors.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
dms qr "https://example.com" # encode an argument
|
||||||
|
echo -n "data" | dms qr # encode stdin
|
||||||
|
dms qr - # encode stdin explicitly
|
||||||
|
|
||||||
|
Output (combine freely):
|
||||||
|
dms qr "text" --clipboard # copy PNG image to clipboard
|
||||||
|
dms qr "text" --copy-text # copy the source text to clipboard
|
||||||
|
dms qr "text" -o code.png # save a PNG file
|
||||||
|
dms qr "text" > code.png # PNG to stdout (piped)
|
||||||
|
dms qr "text" --render # force terminal render
|
||||||
|
|
||||||
|
Encoding & style:
|
||||||
|
--ecc L|M|Q|H # error correction (default M)
|
||||||
|
--qr-version 10 # force symbol version (1-40)
|
||||||
|
--module-size 12 --fg '#000' ... # PNG sizing and colors
|
||||||
|
--shape circle # round modules
|
||||||
|
--logo icon.png # center logo (bumps --ecc to H)
|
||||||
|
--invert # flip colors for light terminals
|
||||||
|
|
||||||
|
WiFi:
|
||||||
|
dms qr wifi MySSID -p secret # build from an explicit password
|
||||||
|
dms qr wifi MySSID # pull the saved secret from the shell`,
|
||||||
|
Args: cobra.ArbitraryArgs,
|
||||||
|
Run: runQR,
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrWifiCmd = &cobra.Command{
|
||||||
|
Use: "wifi <ssid>",
|
||||||
|
Short: "Generate a WiFi QR code",
|
||||||
|
Long: `Generate a QR code that joins a WiFi network when scanned.
|
||||||
|
|
||||||
|
With --password the code is built entirely offline. Without it, the saved
|
||||||
|
credentials are fetched from the running DMS shell (like the network panel).`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: runQRWifi,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrEcc, "ecc", "", "Error correction level: L, M, Q, H (default M, or H with --logo)")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrVersion, "qr-version", 0, "Force QR symbol version 1-40 (0 = auto)")
|
||||||
|
qrCmd.PersistentFlags().StringVarP(&qrOutput, "output", "o", "", "Write a PNG to this file")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrStdout, "stdout", false, "Write PNG bytes to stdout")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrClipboard, "clipboard", false, "Copy the PNG image to the clipboard")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrCopyText, "copy-text", false, "Copy the source text to the clipboard")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrRender, "render", false, "Force terminal rendering")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrNoRender, "no-render", false, "Never render to the terminal")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrInvert, "invert", false, "Swap colors (for light terminals)")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrQuietZone, "quiet-zone", 2, "Terminal margin in modules")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrModuleSize, "module-size", 0, "PNG pixels per module (0 = auto)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrFg, "fg", "", "Dark module color (#RGB or #RRGGBB)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrBg, "bg", "", "Light module color (#RGB or #RRGGBB)")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrTransparent, "transparent", false, "Transparent PNG background")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrBorder, "border", -1, "PNG border in pixels (-1 = auto)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrShape, "shape", "square", "PNG module shape (square, circle)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrLogo, "logo", "", "Center a PNG/JPEG logo on the PNG output")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrLogoScale, "logo-scale", 0, "Max logo size as 1/N of the code (0 = library default of 5)")
|
||||||
|
|
||||||
|
qrWifiCmd.Flags().StringVarP(&qrWifiPassword, "password", "p", "", "WiFi password (offline build)")
|
||||||
|
qrWifiCmd.Flags().StringVar(&qrWifiSecurity, "security", "WPA", "Security type (WPA, WEP, nopass)")
|
||||||
|
qrWifiCmd.Flags().BoolVar(&qrWifiHidden, "hidden", false, "Mark the network as hidden")
|
||||||
|
|
||||||
|
qrCmd.AddCommand(qrWifiCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQR(cmd *cobra.Command, args []string) {
|
||||||
|
text := strings.Join(args, " ")
|
||||||
|
if text == "" || text == "-" {
|
||||||
|
data, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Error reading stdin: %v", err)
|
||||||
|
}
|
||||||
|
text = strings.TrimRight(string(data), "\n")
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
fatalf("Error: no input (provide text, pipe stdin, or use a subcommand)")
|
||||||
|
}
|
||||||
|
emitQR(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQRWifi(cmd *cobra.Command, args []string) {
|
||||||
|
ssid := args[0]
|
||||||
|
if qrWifiPassword != "" || strings.EqualFold(qrWifiSecurity, "nopass") {
|
||||||
|
emitQR(qrcode.WiFiString(qrWifiSecurity, ssid, qrWifiPassword, qrWifiHidden))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := fetchWifiQRContent(ssid)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "Hint: pass --password to build the code without the shell.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if qrWifiHidden {
|
||||||
|
content = strings.TrimSuffix(content, ";") + "H:true;;"
|
||||||
|
}
|
||||||
|
emitQR(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitQR(text string) {
|
||||||
|
renderTerm := shouldRenderTerminal()
|
||||||
|
pngToStdout := qrStdout || (!renderTerm && qrOutput == "" && !qrClipboard && !qrCopyText)
|
||||||
|
|
||||||
|
if pngToStdout || qrOutput != "" || qrClipboard {
|
||||||
|
png, err := qrcode.RenderPNG(text, qrcode.ImageOptions{
|
||||||
|
ECC: effectiveEcc(),
|
||||||
|
Version: qrVersion,
|
||||||
|
ModuleSize: qrModuleSize,
|
||||||
|
Fg: qrFg,
|
||||||
|
Bg: qrBg,
|
||||||
|
Transparent: qrTransparent,
|
||||||
|
Border: qrBorder,
|
||||||
|
Shape: qrShape,
|
||||||
|
Logo: qrLogo,
|
||||||
|
LogoScale: qrLogoScale,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Error encoding QR: %v", err)
|
||||||
|
}
|
||||||
|
emitPNG(png, pngToStdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if qrCopyText {
|
||||||
|
if err := clipboard.CopyText(text); err != nil {
|
||||||
|
fatalf("Error copying text: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !renderTerm {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := qrcode.RenderTerminal(text, qrcode.TermOptions{
|
||||||
|
ECC: effectiveEcc(),
|
||||||
|
Version: qrVersion,
|
||||||
|
QuietZone: qrQuietZone,
|
||||||
|
Invert: qrInvert,
|
||||||
|
Fg: qrFg,
|
||||||
|
Bg: qrBg,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Error rendering QR: %v", err)
|
||||||
|
}
|
||||||
|
dst := os.Stdout
|
||||||
|
if pngToStdout {
|
||||||
|
dst = os.Stderr
|
||||||
|
}
|
||||||
|
fmt.Fprint(dst, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitPNG(png []byte, toStdout bool) {
|
||||||
|
if qrOutput != "" {
|
||||||
|
if err := os.WriteFile(qrOutput, png, 0o644); err != nil {
|
||||||
|
fatalf("Error writing file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, qrOutput)
|
||||||
|
}
|
||||||
|
if toStdout {
|
||||||
|
os.Stdout.Write(png)
|
||||||
|
}
|
||||||
|
if qrClipboard {
|
||||||
|
if err := clipboard.Copy(png, "image/png"); err != nil {
|
||||||
|
fatalf("Error copying image: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRenderTerminal() bool {
|
||||||
|
switch {
|
||||||
|
case qrNoRender:
|
||||||
|
return false
|
||||||
|
case qrRender:
|
||||||
|
return true
|
||||||
|
case qrStdout, qrOutput != "", qrClipboard, qrCopyText:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectiveEcc() string {
|
||||||
|
switch {
|
||||||
|
case qrEcc != "":
|
||||||
|
return qrEcc
|
||||||
|
case qrLogo != "":
|
||||||
|
return "H"
|
||||||
|
default:
|
||||||
|
return "M"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalf(format string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWifiQRContent(ssid string) (string, error) {
|
||||||
|
resp, err := sendServerRequest(models.Request{
|
||||||
|
ID: 1,
|
||||||
|
Method: "network.qrcode-content",
|
||||||
|
Params: map[string]any{"ssid": ssid},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.Error != "" {
|
||||||
|
return "", fmt.Errorf("%s", resp.Error)
|
||||||
|
}
|
||||||
|
if resp.Result == nil {
|
||||||
|
return "", fmt.Errorf("empty response")
|
||||||
|
}
|
||||||
|
content, ok := (*resp.Result).(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected response format")
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
@@ -1,19 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var customConfigPath string
|
|
||||||
var configPath string
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "dms",
|
Use: "dms",
|
||||||
Short: "dms CLI",
|
Short: "dms CLI",
|
||||||
@@ -21,54 +11,5 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
rootCmd.PersistentFlags().StringVarP(shellApp.CustomConfigVar(), "config", "c", "", "Path to a UI config dir (containing shell.qml) to use instead of the embedded UI (env: DMS_SHELL_DIR)")
|
||||||
}
|
|
||||||
|
|
||||||
func findConfig(cmd *cobra.Command, args []string) error {
|
|
||||||
if customConfigPath != "" {
|
|
||||||
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
|
|
||||||
shellPath := filepath.Join(customConfigPath, "shell.qml")
|
|
||||||
|
|
||||||
info, statErr := os.Stat(shellPath)
|
|
||||||
|
|
||||||
if statErr == nil && !info.IsDir() {
|
|
||||||
configPath = customConfigPath
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if statErr != nil {
|
|
||||||
return fmt.Errorf("custom config path error: %w", statErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
|
||||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
|
||||||
if len(getAllDMSPIDs()) == 0 {
|
|
||||||
os.Remove(configStateFile)
|
|
||||||
} else {
|
|
||||||
statePath := strings.TrimSpace(string(data))
|
|
||||||
shellPath := filepath.Join(statePath, "shell.qml")
|
|
||||||
|
|
||||||
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
|
||||||
log.Debug("Using config from active session state file: %s", statePath)
|
|
||||||
configPath = statePath
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
os.Remove(configStateFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("No custom path or active session, searching default XDG locations...")
|
|
||||||
var err error
|
|
||||||
configPath, err = config.LocateDMSConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
@@ -25,8 +28,19 @@ var (
|
|||||||
ssNoConfirm bool
|
ssNoConfirm bool
|
||||||
ssReset bool
|
ssReset bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
|
ssJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type screenshotMetadata struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Width int `json:"width,omitempty"`
|
||||||
|
Height int `json:"height,omitempty"`
|
||||||
|
Scale float64 `json:"scale,omitempty"`
|
||||||
|
Mime string `json:"mime,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
var screenshotCmd = &cobra.Command{
|
var screenshotCmd = &cobra.Command{
|
||||||
Use: "screenshot",
|
Use: "screenshot",
|
||||||
Short: "Capture screenshots",
|
Short: "Capture screenshots",
|
||||||
@@ -37,8 +51,9 @@ Modes:
|
|||||||
full - Capture the focused output
|
full - Capture the focused output
|
||||||
all - Capture all outputs combined
|
all - Capture all outputs combined
|
||||||
output - Capture a specific output by name
|
output - Capture a specific output by name
|
||||||
window - Capture the focused window (Hyprland/DWL)
|
window - Capture the focused window (Hyprland/Mango/niri)
|
||||||
last - Capture the last selected region
|
last - Capture the last selected region
|
||||||
|
scroll - Select a region, then scroll to capture a stitched tall image
|
||||||
|
|
||||||
Output format (--format):
|
Output format (--format):
|
||||||
png - PNG format (default)
|
png - PNG format (default)
|
||||||
@@ -57,7 +72,10 @@ Examples:
|
|||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
dms screenshot --no-confirm # Region capture on mouse release
|
dms screenshot --no-confirm # Region capture on mouse release
|
||||||
dms screenshot --cursor=on # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
dms screenshot -f jpg -q 85 # JPEG with quality 85
|
||||||
|
dms screenshot --json # Print capture metadata as JSON
|
||||||
|
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
|
||||||
|
dms screenshot scroll --interval 250`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssRegionCmd = &cobra.Command{
|
var ssRegionCmd = &cobra.Command{
|
||||||
@@ -95,10 +113,33 @@ If no previous region exists, falls back to interactive selection.`,
|
|||||||
var ssWindowCmd = &cobra.Command{
|
var ssWindowCmd = &cobra.Command{
|
||||||
Use: "window",
|
Use: "window",
|
||||||
Short: "Capture the focused window",
|
Short: "Capture the focused window",
|
||||||
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
|
Long: `Capture the currently focused window. Supported on Hyprland, Mango, and niri.`,
|
||||||
Run: runScreenshotWindow,
|
Run: runScreenshotWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ssScrollInterval int
|
||||||
|
|
||||||
|
var ssScrollCmd = &cobra.Command{
|
||||||
|
Use: "scroll",
|
||||||
|
Short: "Capture a scrolling region stitched into one tall image",
|
||||||
|
Long: `Select a region, then scroll the content beneath with the mouse wheel or
|
||||||
|
touchpad while frames are captured and stitched vertically. Finish with the
|
||||||
|
on-screen done button; cancel with the cancel button. Enter and Esc work
|
||||||
|
everywhere: most compositors hold the keyboard on the overlay (keyboard
|
||||||
|
scrolling does not reach the app there), while Hyprland leaves the keyboard
|
||||||
|
with the application — keyboard scrolling works, and Enter/Esc act through
|
||||||
|
temporary global binds for the session. The cursor is never included in
|
||||||
|
frames.
|
||||||
|
|
||||||
|
Frames are stitched continuously while scrolling, and revisited content is
|
||||||
|
never duplicated — scrolling up past the starting point extends the image
|
||||||
|
upward. Content jumped past faster than capture can follow is skipped rather
|
||||||
|
than stitched incorrectly.
|
||||||
|
|
||||||
|
Rotated outputs are not supported.`,
|
||||||
|
Run: runScreenshotScroll,
|
||||||
|
}
|
||||||
|
|
||||||
var ssListCmd = &cobra.Command{
|
var ssListCmd = &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List available outputs",
|
Short: "List available outputs",
|
||||||
@@ -126,8 +167,12 @@ func init() {
|
|||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
|
||||||
|
|
||||||
|
ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)")
|
||||||
|
|
||||||
screenshotCmd.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
|
screenshotCmd.AddCommand(ssScrollCmd)
|
||||||
screenshotCmd.AddCommand(ssFullCmd)
|
screenshotCmd.AddCommand(ssFullCmd)
|
||||||
screenshotCmd.AddCommand(ssAllCmd)
|
screenshotCmd.AddCommand(ssAllCmd)
|
||||||
screenshotCmd.AddCommand(ssOutputCmd)
|
screenshotCmd.AddCommand(ssOutputCmd)
|
||||||
@@ -179,15 +224,76 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. Best-effort.
|
||||||
|
func setPopoutScreenshotMode(begin bool) {
|
||||||
|
fn := "end"
|
||||||
|
if begin {
|
||||||
|
fn = "begin"
|
||||||
|
}
|
||||||
|
cmdArgs := []string{"ipc"}
|
||||||
|
if pid, ok := shellApp.SessionPID(); ok {
|
||||||
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
|
} else {
|
||||||
|
if err := shellApp.ResolveConfig(nil, nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
|
||||||
|
_ = exec.Command("qs", cmdArgs...).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeScreenshotJSON(meta screenshotMetadata) {
|
||||||
|
_ = json.NewEncoder(os.Stdout).Encode(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitScreenshotError(context string, err error) {
|
||||||
|
if ssJSON {
|
||||||
|
writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMime(format screenshot.Format) string {
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
return "image/jpeg"
|
||||||
|
case screenshot.FormatPPM:
|
||||||
|
return "image/x-portable-pixmap"
|
||||||
|
default:
|
||||||
|
return "image/png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runScreenshot(config screenshot.Config) {
|
func runScreenshot(config screenshot.Config) {
|
||||||
sc := screenshot.New(config)
|
if ssJSON && config.Stdout {
|
||||||
result, err := sc.Run()
|
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Region select needs the keyboard; drop popout grabs for its duration.
|
||||||
|
result, err := func() (*screenshot.CaptureResult, error) {
|
||||||
|
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion || config.Mode == screenshot.ModeScroll
|
||||||
|
if interactive {
|
||||||
|
setPopoutScreenshotMode(true)
|
||||||
|
defer setPopoutScreenshotMode(false)
|
||||||
|
}
|
||||||
|
return screenshot.New(config).Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
exitScreenshotError("", err)
|
||||||
|
}
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
if ssJSON {
|
||||||
|
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
|
||||||
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +305,7 @@ func runScreenshot(config screenshot.Config) {
|
|||||||
|
|
||||||
if config.Stdout {
|
if config.Stdout {
|
||||||
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
exitScreenshotError(" writing to stdout", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -220,22 +325,37 @@ func runScreenshot(config screenshot.Config) {
|
|||||||
|
|
||||||
filePath = filepath.Join(outputDir, filename)
|
filePath = filepath.Join(outputDir, filename)
|
||||||
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
exitScreenshotError(" writing file", err)
|
||||||
os.Exit(1)
|
}
|
||||||
|
if !ssJSON {
|
||||||
|
fmt.Println(filePath)
|
||||||
}
|
}
|
||||||
fmt.Println(filePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Clipboard {
|
if config.Clipboard {
|
||||||
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
exitScreenshotError(" copying to clipboard", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if !config.SaveFile {
|
if !ssJSON && !config.SaveFile {
|
||||||
fmt.Println("Copied to clipboard")
|
fmt.Println("Copied to clipboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ssJSON {
|
||||||
|
scale := result.Scale
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
writeScreenshotJSON(screenshotMetadata{
|
||||||
|
Status: "success",
|
||||||
|
Path: filePath,
|
||||||
|
Width: result.Buffer.Width,
|
||||||
|
Height: result.Buffer.Height,
|
||||||
|
Scale: scale,
|
||||||
|
Mime: formatMime(config.Format),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if config.Notify {
|
if config.Notify {
|
||||||
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||||
screenshot.SendNotification(screenshot.NotifyResult{
|
screenshot.SendNotification(screenshot.NotifyResult{
|
||||||
@@ -346,6 +466,12 @@ func runScreenshotRegion(cmd *cobra.Command, args []string) {
|
|||||||
runScreenshot(config)
|
runScreenshot(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runScreenshotScroll(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeScroll)
|
||||||
|
config.IntervalMs = min(max(ssScrollInterval, 30), 1000)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
||||||
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
||||||
runScreenshot(config)
|
runScreenshot(config)
|
||||||
|
|||||||
+100
-38
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
@@ -100,56 +100,72 @@ var setupWindowrulesCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dmsConfigSpec struct {
|
type dmsConfigSpec struct {
|
||||||
niriFile string
|
niriFile string
|
||||||
hyprFile string
|
hyprFile string
|
||||||
niriContent func(terminal string) string
|
mangoFile string
|
||||||
hyprContent func(terminal string) string
|
niriContent func(terminal string) string
|
||||||
|
hyprContent func(terminal string) string
|
||||||
|
mangoContent func(terminal string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||||
"binds": {
|
"binds": {
|
||||||
niriFile: "binds.kdl",
|
niriFile: "binds.kdl",
|
||||||
hyprFile: "binds.lua",
|
hyprFile: "binds.lua",
|
||||||
|
mangoFile: "binds.conf",
|
||||||
niriContent: func(t string) string {
|
niriContent: func(t string) string {
|
||||||
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
},
|
},
|
||||||
hyprContent: func(t string) string {
|
hyprContent: func(t string) string {
|
||||||
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
|
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
},
|
},
|
||||||
|
mangoContent: func(t string) string {
|
||||||
|
return strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
niriFile: "layout.kdl",
|
niriFile: "layout.kdl",
|
||||||
hyprFile: "layout.lua",
|
hyprFile: "layout.lua",
|
||||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
mangoFile: "layout.conf",
|
||||||
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||||
|
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||||
|
mangoContent: func(_ string) string { return config.MangoLayoutConfig },
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
niriFile: "colors.kdl",
|
niriFile: "colors.kdl",
|
||||||
hyprFile: "colors.lua",
|
hyprFile: "colors.lua",
|
||||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
mangoFile: "colors.conf",
|
||||||
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||||
|
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||||
|
mangoContent: func(_ string) string { return config.MangoColorsConfig },
|
||||||
},
|
},
|
||||||
"alttab": {
|
"alttab": {
|
||||||
niriFile: "alttab.kdl",
|
niriFile: "alttab.kdl",
|
||||||
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
niriFile: "outputs.kdl",
|
niriFile: "outputs.kdl",
|
||||||
hyprFile: "outputs.lua",
|
hyprFile: "outputs.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
mangoFile: "outputs.conf",
|
||||||
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
niriContent: func(_ string) string { return "" },
|
||||||
|
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
||||||
|
mangoContent: func(_ string) string { return "" },
|
||||||
},
|
},
|
||||||
"cursor": {
|
"cursor": {
|
||||||
niriFile: "cursor.kdl",
|
niriFile: "cursor.kdl",
|
||||||
hyprFile: "cursor.lua",
|
hyprFile: "cursor.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
mangoFile: "cursor.conf",
|
||||||
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
niriContent: func(_ string) string { return "" },
|
||||||
|
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
||||||
|
mangoContent: func(_ string) string { return "" },
|
||||||
},
|
},
|
||||||
"windowrules": {
|
"windowrules": {
|
||||||
niriFile: "windowrules.kdl",
|
niriFile: "windowrules.kdl",
|
||||||
hyprFile: "windowrules.lua",
|
hyprFile: "windowrules.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
mangoFile: "windowrules.conf",
|
||||||
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
niriContent: func(_ string) string { return "" },
|
||||||
|
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
||||||
|
mangoContent: func(_ string) string { return "" },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,20 +204,34 @@ func detectTerminal() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func detectCompositorForSetup() (string, error) {
|
func detectCompositorForSetup() (string, error) {
|
||||||
compositors := greeter.DetectCompositors()
|
var compositors []string
|
||||||
|
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
|
||||||
|
if utils.CommandExists(candidate) {
|
||||||
|
compositors = append(compositors, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch len(compositors) {
|
switch len(compositors) {
|
||||||
case 0:
|
case 0:
|
||||||
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
return "", fmt.Errorf("no supported compositors found (niri, Hyprland, or mango required)")
|
||||||
case 1:
|
case 1:
|
||||||
return strings.ToLower(compositors[0]), nil
|
return strings.ToLower(compositors[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
selected, err := greeter.PromptCompositorChoice(compositors)
|
fmt.Println("Multiple compositors detected:")
|
||||||
if err != nil {
|
for i, compositor := range compositors {
|
||||||
return "", err
|
fmt.Printf("%d) %s\n", i+1, compositor)
|
||||||
}
|
}
|
||||||
return strings.ToLower(selected), nil
|
fmt.Printf("\nChoice (1-%d): ", len(compositors))
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
choice := 0
|
||||||
|
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
|
||||||
|
if choice < 1 || choice > len(compositors) {
|
||||||
|
return "", fmt.Errorf("invalid choice")
|
||||||
|
}
|
||||||
|
return strings.ToLower(compositors[choice-1]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSetupDmsConfig(name string) error {
|
func runSetupDmsConfig(name string) error {
|
||||||
@@ -224,6 +254,9 @@ func runSetupDmsConfig(name string) error {
|
|||||||
case "hyprland":
|
case "hyprland":
|
||||||
filename = spec.hyprFile
|
filename = spec.hyprFile
|
||||||
contentFn = spec.hyprContent
|
contentFn = spec.hyprContent
|
||||||
|
case "mango", "mangowc":
|
||||||
|
filename = spec.mangoFile
|
||||||
|
contentFn = spec.mangoContent
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported compositor: %s", compositor)
|
return fmt.Errorf("unsupported compositor: %s", compositor)
|
||||||
}
|
}
|
||||||
@@ -235,9 +268,11 @@ func runSetupDmsConfig(name string) error {
|
|||||||
var dmsDir string
|
var dmsDir string
|
||||||
switch compositor {
|
switch compositor {
|
||||||
case "niri":
|
case "niri":
|
||||||
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
|
dmsDir = filepath.Join(utils.XDGConfigHome(), "niri", "dms")
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
|
dmsDir = filepath.Join(utils.XDGConfigHome(), "hypr", "dms")
|
||||||
|
case "mango", "mangowc":
|
||||||
|
dmsDir = filepath.Join(utils.XDGConfigHome(), "mango", "dms")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
@@ -273,7 +308,17 @@ func runSetup() error {
|
|||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := promptSystemd()
|
useSystemd := true
|
||||||
|
if wmSelected {
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
useSystemd = false
|
||||||
|
} else if isVoidSetup() {
|
||||||
|
useSystemd = false
|
||||||
|
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
|
||||||
|
} else {
|
||||||
|
useSystemd = promptSystemd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !wmSelected && !terminalSelected {
|
if !wmSelected && !terminalSelected {
|
||||||
fmt.Println("No configurations selected. Exiting.")
|
fmt.Println("No configurations selected. Exiting.")
|
||||||
@@ -344,6 +389,15 @@ func runSetup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isVoidSetup() bool {
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||||
|
return exists && config.Family == distros.FamilyVoid
|
||||||
|
}
|
||||||
|
|
||||||
// Add user to the input group for the evdev manager for inut state tracking.
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
func ensureInputGroup() {
|
func ensureInputGroup() {
|
||||||
@@ -379,10 +433,11 @@ func promptCompositor() (deps.WindowManager, bool) {
|
|||||||
fmt.Println("Select compositor:")
|
fmt.Println("Select compositor:")
|
||||||
fmt.Println("1) Niri")
|
fmt.Println("1) Niri")
|
||||||
fmt.Println("2) Hyprland")
|
fmt.Println("2) Hyprland")
|
||||||
fmt.Println("3) None")
|
fmt.Println("3) Mango")
|
||||||
|
fmt.Println("4) None")
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
fmt.Print("\nChoice (1-3): ")
|
fmt.Print("\nChoice (1-4): ")
|
||||||
fmt.Scanln(&response)
|
fmt.Scanln(&response)
|
||||||
response = strings.TrimSpace(response)
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
@@ -391,6 +446,8 @@ func promptCompositor() (deps.WindowManager, bool) {
|
|||||||
return deps.WindowManagerNiri, true
|
return deps.WindowManagerNiri, true
|
||||||
case "2":
|
case "2":
|
||||||
return deps.WindowManagerHyprland, true
|
return deps.WindowManagerHyprland, true
|
||||||
|
case "3":
|
||||||
|
return deps.WindowManagerMango, true
|
||||||
default:
|
default:
|
||||||
return deps.WindowManagerNiri, false
|
return deps.WindowManagerNiri, false
|
||||||
}
|
}
|
||||||
@@ -447,6 +504,11 @@ func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.
|
|||||||
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
||||||
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
||||||
}
|
}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
configPaths = []string{
|
||||||
|
filepath.Join(homeDir, ".config", "mango", "config.conf"),
|
||||||
|
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, configPath := range configPaths {
|
for _, configPath := range configPaths {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ var (
|
|||||||
sysUpdateJSON bool
|
sysUpdateJSON bool
|
||||||
sysUpdateNoFlatpak bool
|
sysUpdateNoFlatpak bool
|
||||||
sysUpdateNoAUR bool
|
sysUpdateNoAUR bool
|
||||||
|
sysUpdateIgnore []string
|
||||||
sysUpdateIntervalS int
|
sysUpdateIntervalS int
|
||||||
sysUpdateListPmTime = 5 * time.Minute
|
sysUpdateListPmTime = 5 * time.Minute
|
||||||
)
|
)
|
||||||
@@ -58,6 +59,7 @@ func init() {
|
|||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
|
||||||
|
systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
|
||||||
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
||||||
|
|
||||||
systemCmd.AddCommand(systemUpdateCmd)
|
systemCmd.AddCommand(systemUpdateCmd)
|
||||||
@@ -192,6 +194,7 @@ func runSystemUpdateApply() {
|
|||||||
Targets: pkgs,
|
Targets: pkgs,
|
||||||
IncludeFlatpak: !sysUpdateNoFlatpak,
|
IncludeFlatpak: !sysUpdateNoFlatpak,
|
||||||
IncludeAUR: !sysUpdateNoAUR,
|
IncludeAUR: !sysUpdateNoAUR,
|
||||||
|
Ignored: sysUpdateIgnore,
|
||||||
DryRun: sysUpdateDry,
|
DryRun: sysUpdateDry,
|
||||||
UseSudo: true,
|
UseSudo: true,
|
||||||
}
|
}
|
||||||
@@ -234,12 +237,19 @@ func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
||||||
if !sysUpdateNoAUR {
|
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
|
||||||
return pkgs
|
return pkgs
|
||||||
}
|
}
|
||||||
|
ignored := make(map[string]bool, len(sysUpdateIgnore))
|
||||||
|
for _, name := range sysUpdateIgnore {
|
||||||
|
ignored[name] = true
|
||||||
|
}
|
||||||
out := pkgs[:0]
|
out := pkgs[:0]
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Repo == sysupdate.RepoAUR {
|
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ignored[p.Name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, p)
|
out = append(out, p)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ var windowrulesListCmd = &cobra.Command{
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -40,7 +41,7 @@ var windowrulesAddCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -54,7 +55,7 @@ var windowrulesUpdateCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -68,7 +69,7 @@ var windowrulesRemoveCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -82,7 +83,7 @@ var windowrulesReorderCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -120,6 +121,9 @@ func getCompositor(args []string) string {
|
|||||||
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
return "hyprland"
|
return "hyprland"
|
||||||
}
|
}
|
||||||
|
if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" {
|
||||||
|
return "mango"
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,17 +143,14 @@ func writeRuleSuccess(id, path string) {
|
|||||||
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||||
compositor := getCompositor(args)
|
compositor := getCompositor(args)
|
||||||
if compositor == "" {
|
if compositor == "" {
|
||||||
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
log.Fatalf("Could not detect compositor. Please specify: hyprland, niri, or mango")
|
||||||
}
|
}
|
||||||
|
|
||||||
var result WindowRulesListResult
|
var result WindowRulesListResult
|
||||||
|
|
||||||
switch compositor {
|
switch compositor {
|
||||||
case "niri":
|
case "niri":
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to expand niri config path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,10 +183,7 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
|||||||
result.DMSStatus = parseResult.DMSStatus
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,6 +215,38 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
|||||||
result.Rules = allRules
|
result.Rules = allRules
|
||||||
result.DMSStatus = parseResult.DMSStatus
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
|
case "mango", "mangowc":
|
||||||
|
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
|
||||||
|
|
||||||
|
parseResult, err := providers.ParseMangoWindowRules(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse mango window rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allRules := providers.ConvertMangoRulesToWindowRules(parseResult.Rules)
|
||||||
|
|
||||||
|
provider := providers.NewMangoWritableProvider(configDir)
|
||||||
|
dmsRules, _ := provider.LoadDMSRules()
|
||||||
|
|
||||||
|
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||||
|
for i, dr := range dmsRules {
|
||||||
|
dmsRuleMap[i] = dr
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsIdx := 0
|
||||||
|
for i, r := range allRules {
|
||||||
|
if r.Source == "dms/windowrules.conf" {
|
||||||
|
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||||
|
allRules[i].ID = dmr.ID
|
||||||
|
allRules[i].Name = dmr.Name
|
||||||
|
}
|
||||||
|
dmsIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rules = allRules
|
||||||
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unknown compositor: %s", compositor)
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
}
|
}
|
||||||
@@ -315,17 +345,14 @@ func runWindowrulesReorder(cmd *cobra.Command, args []string) {
|
|||||||
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
||||||
switch compositor {
|
switch compositor {
|
||||||
case "niri":
|
case "niri":
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return providers.NewNiriWritableProvider(configDir)
|
return providers.NewNiriWritableProvider(configDir)
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return providers.NewHyprlandWritableProvider(configDir)
|
return providers.NewHyprlandWritableProvider(configDir)
|
||||||
|
case "mango", "mangowc":
|
||||||
|
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
|
||||||
|
return providers.NewMangoWritableProvider(configDir)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) {
|
||||||
|
var policyFile cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil {
|
||||||
|
t.Fatalf("failed to parse embedded CLI policy: %v", err)
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands == nil {
|
||||||
|
t.Fatal("embedded CLI policy has no blocked_commands")
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked := normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
if !commandBlockedByPolicy("greeter enable", blocked) {
|
||||||
|
t.Fatal("expected greeter enable to remain blocked on immutable/image-based systems")
|
||||||
|
}
|
||||||
|
if commandBlockedByPolicy("greeter sync", blocked) {
|
||||||
|
t.Fatal("expected greeter sync to remain available on immutable/image-based systems")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,15 +12,7 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
|
||||||
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
|
|
||||||
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
|
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
|
||||||
authCmd.AddCommand(authSyncCmd)
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|||||||
@@ -12,15 +12,7 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
|
||||||
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
|
|
||||||
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
|
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
|
||||||
authCmd.AddCommand(authSyncCmd)
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
|
||||||
|
// overhead in the line-delimited IPC response.
|
||||||
|
const maxIPCMessageSize = 96 * 1024 * 1024
|
||||||
|
|
||||||
func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
||||||
socketPath := getServerSocketPath()
|
socketPath := getServerSocketPath()
|
||||||
|
|
||||||
@@ -22,6 +26,7 @@ func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conn)
|
scanner := bufio.NewScanner(conn)
|
||||||
|
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
|
||||||
scanner.Scan() // discard initial capabilities message
|
scanner.Scan() // discard initial capabilities message
|
||||||
|
|
||||||
reqData, err := json.Marshal(req)
|
reqData, err := json.Marshal(req)
|
||||||
@@ -61,6 +66,7 @@ func sendServerRequestFireAndForget(req models.Request) error {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conn)
|
scanner := bufio.NewScanner(conn)
|
||||||
|
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
|
||||||
scanner.Scan() // discard initial capabilities message
|
scanner.Scan() // discard initial capabilities message
|
||||||
|
|
||||||
reqData, err := json.Marshal(req)
|
reqData, err := json.Marshal(req)
|
||||||
@@ -95,6 +101,10 @@ func getServerSocketPath() string {
|
|||||||
runtimeDir = os.TempDir()
|
runtimeDir = os.TempDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
|
||||||
|
return sessionSock
|
||||||
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(runtimeDir)
|
entries, err := os.ReadDir(runtimeDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filepath.Join(runtimeDir, "danklinux.sock")
|
return filepath.Join(runtimeDir, "danklinux.sock")
|
||||||
|
|||||||
+160
-597
@@ -2,554 +2,28 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ipcTargets map[string]map[string][]string
|
type ipcTargets map[string]map[string][]string
|
||||||
|
|
||||||
// getProcessExitCode returns the exit code from a ProcessState.
|
|
||||||
// For normal exits, returns the exit code directly.
|
|
||||||
// For signal termination, returns 128 + signal number (Unix convention).
|
|
||||||
func getProcessExitCode(state *os.ProcessState) int {
|
|
||||||
if state == nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if code := state.ExitCode(); code != -1 {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
// Process was killed by signal - extract signal number
|
|
||||||
if status, ok := state.Sys().(syscall.WaitStatus); ok {
|
|
||||||
if status.Signaled() {
|
|
||||||
return 128 + int(status.Signal())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSessionManaged bool
|
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
|
||||||
selfPath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setsid: true,
|
|
||||||
}
|
|
||||||
cmd.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runDetachedRestart(targetPIDStr string) {
|
|
||||||
targetPID, err := strconv.Atoi(targetPIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(targetPID)
|
|
||||||
if err == nil {
|
|
||||||
proc.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
killShell()
|
|
||||||
runShellDaemon(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRuntimeDir() string {
|
|
||||||
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
|
||||||
return runtime
|
|
||||||
}
|
|
||||||
return os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendLogEnv(env []string) []string {
|
|
||||||
if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
|
|
||||||
env = append(env, "DMS_LOG_LEVEL="+v)
|
|
||||||
}
|
|
||||||
if v := os.Getenv("DMS_LOG_FILE"); v != "" {
|
|
||||||
env = append(env, "DMS_LOG_FILE="+v)
|
|
||||||
}
|
|
||||||
return env
|
|
||||||
}
|
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePIDFile(childPID int) error {
|
|
||||||
pidFile := getPIDFilePath()
|
|
||||||
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removePIDFile() {
|
|
||||||
pidFile := getPIDFilePath()
|
|
||||||
os.Remove(pidFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllDMSPIDs() []int {
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var pids []int
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pidFile := filepath.Join(dir, entry.Name())
|
|
||||||
data, err := os.ReadFile(pidFile)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(pidFile)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(childPID)
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(pidFile)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
|
||||||
os.Remove(pidFile)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pids = append(pids, childPID)
|
|
||||||
|
|
||||||
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
|
||||||
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
|
||||||
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
|
||||||
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
|
||||||
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
|
||||||
pids = append(pids, parentPID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pids
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellInteractive(session bool) {
|
|
||||||
isSessionManaged = session
|
|
||||||
go printASCII()
|
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
socketPath := server.GetSocketPath()
|
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(configStateFile)
|
|
||||||
|
|
||||||
errChan := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
server.CLIVersion = Version
|
|
||||||
if err := server.Start(false); err != nil {
|
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
|
||||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
|
||||||
if !strings.HasPrefix(configPath, homeDir) {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = appendLogEnv(cmd.Env)
|
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Fatalf("Error starting quickshell: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write PID file for the quickshell child process
|
|
||||||
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
|
||||||
log.Warnf("Failed to write PID file: %v", err)
|
|
||||||
}
|
|
||||||
defer removePIDFile()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
|
||||||
} else {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sig := <-sigChan:
|
|
||||||
if sig == syscall.SIGUSR1 {
|
|
||||||
if isSessionManaged {
|
|
||||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
|
||||||
execDetachedRestart(os.Getpid())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
|
||||||
select {
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
return
|
|
||||||
|
|
||||||
case err := <-errChan:
|
|
||||||
log.Error(err)
|
|
||||||
cancel()
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartShell() {
|
|
||||||
pids := getAllDMSPIDs()
|
|
||||||
|
|
||||||
if len(pids) == 0 {
|
|
||||||
log.Info("No running DMS shell instances found. Starting daemon...")
|
|
||||||
runShellDaemon(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPid := os.Getpid()
|
|
||||||
uniquePids := make(map[int]bool)
|
|
||||||
|
|
||||||
for _, pid := range pids {
|
|
||||||
if pid != currentPid {
|
|
||||||
uniquePids[pid] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for pid := range uniquePids {
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error finding process %d: %v", pid, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.SIGUSR1); err != nil {
|
|
||||||
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
|
|
||||||
} else {
|
|
||||||
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func killShell() {
|
|
||||||
pids := getAllDMSPIDs()
|
|
||||||
|
|
||||||
if len(pids) == 0 {
|
|
||||||
log.Info("No running DMS shell instances found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPid := os.Getpid()
|
|
||||||
uniquePids := make(map[int]bool)
|
|
||||||
|
|
||||||
for _, pid := range pids {
|
|
||||||
if pid != currentPid {
|
|
||||||
uniquePids[pid] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for pid := range uniquePids {
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error finding process %d: %v", pid, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Kill(); err != nil {
|
|
||||||
log.Errorf("Error killing process %d: %v", pid, err)
|
|
||||||
} else {
|
|
||||||
log.Infof("Killed DMS process with PID %d", pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
pidFile := filepath.Join(dir, entry.Name())
|
|
||||||
os.Remove(pidFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellDaemon(session bool) {
|
|
||||||
isSessionManaged = session
|
|
||||||
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
|
|
||||||
|
|
||||||
if !isDaemonChild {
|
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
|
||||||
|
|
||||||
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setsid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Fatalf("Error starting daemon: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
socketPath := server.GetSocketPath()
|
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(configStateFile)
|
|
||||||
|
|
||||||
errChan := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
server.CLIVersion = Version
|
|
||||||
if err := server.Start(false); err != nil {
|
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
|
||||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
|
||||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
|
||||||
|
|
||||||
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) {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = appendLogEnv(cmd.Env)
|
|
||||||
|
|
||||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error opening /dev/null: %v", err)
|
|
||||||
}
|
|
||||||
defer devNull.Close()
|
|
||||||
|
|
||||||
cmd.Stdin = devNull
|
|
||||||
cmd.Stdout = devNull
|
|
||||||
cmd.Stderr = devNull
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Fatalf("Error starting daemon: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write PID file for the quickshell child process
|
|
||||||
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
|
||||||
log.Warnf("Failed to write PID file: %v", err)
|
|
||||||
}
|
|
||||||
defer removePIDFile()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
|
||||||
} else {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sig := <-sigChan:
|
|
||||||
if sig == syscall.SIGUSR1 {
|
|
||||||
if isSessionManaged {
|
|
||||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
|
||||||
execDetachedRestart(os.Getpid())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
|
||||||
select {
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
||||||
out, err := exec.Command("qs", "ipc", "--help").Output()
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "qs", "ipc", "--help")
|
||||||
|
cmd.WaitDelay = 500 * time.Millisecond
|
||||||
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -585,13 +59,34 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
|||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
func getShellIPCCompletions(args []string, _ string) []string {
|
func buildQsIPCBaseArgs() ([]string, error) {
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
if qsHasAnyDisplay() {
|
switch pid, ok := shellApp.SessionPID(); {
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
case ok:
|
||||||
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
|
default:
|
||||||
|
if err := shellApp.ResolveConfig(nil, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
return cmdArgs, nil
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
}
|
||||||
|
|
||||||
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
|
baseArgs, err := buildQsIPCBaseArgs()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Error building IPC args for completions: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmdArgs := append(baseArgs, "show")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "qs", cmdArgs...)
|
||||||
|
cmd.WaitDelay = 500 * time.Millisecond
|
||||||
var targets ipcTargets
|
var targets ipcTargets
|
||||||
|
|
||||||
if output, err := cmd.Output(); err == nil {
|
if output, err := cmd.Output(); err == nil {
|
||||||
@@ -607,7 +102,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
targetNames := make([]string, 0)
|
targetNames := make([]string, 0)
|
||||||
targetNames = append(targetNames, "call")
|
targetNames = append(targetNames, "call", "list")
|
||||||
for k := range targets {
|
for k := range targets {
|
||||||
targetNames = append(targetNames, k)
|
targetNames = append(targetNames, k)
|
||||||
}
|
}
|
||||||
@@ -633,43 +128,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstDMSPID() (int, bool) {
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if proc.Signal(syscall.Signal(0)) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -680,23 +138,11 @@ func runShellIPCCommand(args []string) {
|
|||||||
args = append([]string{"call"}, args...)
|
args = append([]string{"call"}, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
baseArgs, err := buildQsIPCBaseArgs()
|
||||||
|
if err != nil {
|
||||||
switch pid, ok := getFirstDMSPID(); {
|
log.Fatalf("Error finding config: %v", err)
|
||||||
case ok:
|
|
||||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
|
||||||
default:
|
|
||||||
if err := findConfig(nil, nil); err != nil {
|
|
||||||
log.Fatalf("Error finding config: %v", err)
|
|
||||||
}
|
|
||||||
// ! TODO - remove check when QS 0.3 is released
|
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
|
||||||
}
|
}
|
||||||
|
cmdArgs := append(baseArgs, args...)
|
||||||
cmdArgs = append(cmdArgs, args...)
|
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@@ -708,19 +154,20 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printIPCHelp() {
|
func printIPCHelp() {
|
||||||
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
baseArgs, err := buildQsIPCBaseArgs()
|
||||||
if qsHasAnyDisplay() {
|
if err != nil {
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
printIPCHelpFailure(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
cmdArgs := append(baseArgs, "show")
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
printIPCHelpFailure(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,3 +195,119 @@ func printIPCHelp() {
|
|||||||
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printIPCHelpFailure(err error) {
|
||||||
|
fmt.Println("Could not retrieve IPC targets.")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" Full docs: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc")
|
||||||
|
fmt.Println(" Try: dms ipc call <target> <function>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
|
||||||
|
func ensureFontCache() {
|
||||||
|
if _, err := exec.LookPath("fc-list"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("fc-cache"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fontsToCheck []string
|
||||||
|
|
||||||
|
if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
|
||||||
|
if data, err := os.ReadFile(settingsPath); err == nil {
|
||||||
|
var settings struct {
|
||||||
|
FontFamily string `json:"fontFamily"`
|
||||||
|
MonoFontFamily string `json:"monoFontFamily"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &settings); err == nil {
|
||||||
|
if settings.FontFamily != "" && settings.FontFamily != "Inter Variable" {
|
||||||
|
fontsToCheck = append(fontsToCheck, settings.FontFamily)
|
||||||
|
}
|
||||||
|
if settings.MonoFontFamily != "" && settings.MonoFontFamily != "Fira Code" {
|
||||||
|
fontsToCheck = append(fontsToCheck, settings.MonoFontFamily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fontsToCheck) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := exec.Command("fc-list", ":", "family").Output()
|
||||||
|
if err != nil || len(strings.TrimSpace(string(output))) == 0 {
|
||||||
|
log.Warnf("Font cache appears empty or corrupt, rebuilding...")
|
||||||
|
rebuildFontCache()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFonts := strings.ToLower(string(output))
|
||||||
|
var missing []string
|
||||||
|
for _, font := range fontsToCheck {
|
||||||
|
if !fontInCache(strings.ToLower(font), cacheFonts) {
|
||||||
|
missing = append(missing, font)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) > 0 {
|
||||||
|
log.Warnf("Font(s) not found in cache: %s — rebuilding...", strings.Join(missing, ", "))
|
||||||
|
rebuildFontCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fontInCache(target, cache string) bool {
|
||||||
|
for _, line := range strings.Split(cache, "\n") {
|
||||||
|
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
|
||||||
|
if strings.TrimSpace(fam) == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebuildFontCache() {
|
||||||
|
cmd := exec.Command("fc-cache", "-f")
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Warnf("Failed to rebuild font cache: %v\n%s", err, string(output))
|
||||||
|
} else {
|
||||||
|
log.Infof("Font cache rebuilt successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
|
||||||
|
func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) {
|
||||||
|
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if containsFontCrashSignature(stderrTail) {
|
||||||
|
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
|
||||||
|
} else {
|
||||||
|
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsFontCrashSignature(logStr string) bool {
|
||||||
|
logStr = strings.ToLower(logStr)
|
||||||
|
signatures := []string{
|
||||||
|
"fontconfig",
|
||||||
|
"freetype",
|
||||||
|
"ft_load_glyph",
|
||||||
|
"ft_face",
|
||||||
|
"fc-list",
|
||||||
|
"fc-cache",
|
||||||
|
"glyph",
|
||||||
|
"typeface",
|
||||||
|
}
|
||||||
|
for _, sig := range signatures {
|
||||||
|
if strings.Contains(logStr, sig) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
|
||||||
|
"github.com/AvengeMedia/dankgo/shellapp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var shellApp = shellapp.New(shellapp.Config{
|
||||||
|
ID: "danklinux",
|
||||||
|
EnvPrefix: "DMS",
|
||||||
|
QSAppID: "com.danklinux.dms",
|
||||||
|
Version: Version,
|
||||||
|
Embedded: embeddedShell{},
|
||||||
|
Boot: bootBackend,
|
||||||
|
PreLaunch: preLaunch,
|
||||||
|
ExtraEnv: dmsExtraEnv,
|
||||||
|
OnUIExit: logStartupFailure,
|
||||||
|
})
|
||||||
|
|
||||||
|
type embeddedShell struct{}
|
||||||
|
|
||||||
|
func (embeddedShell) Available() bool { return shellembed.Available() }
|
||||||
|
|
||||||
|
func (embeddedShell) Extract(baseDir string) (string, error) { return shellembed.Extract(baseDir) }
|
||||||
|
|
||||||
|
func (embeddedShell) Prune(baseDir, keep string) { shellembed.Prune(baseDir, keep) }
|
||||||
|
|
||||||
|
type dmsBackend struct {
|
||||||
|
srv *server.Server
|
||||||
|
done chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *dmsBackend) SocketPath() string { return b.srv.SocketPath() }
|
||||||
|
|
||||||
|
func (b *dmsBackend) Close() { b.srv.Close() }
|
||||||
|
|
||||||
|
func (b *dmsBackend) Done() <-chan error { return b.done }
|
||||||
|
|
||||||
|
func bootBackend(ctx context.Context) (shellapp.Backend, error) {
|
||||||
|
config.CleanupStrayHyprlandConfFile(log.Infof)
|
||||||
|
server.CLIVersion = Version
|
||||||
|
|
||||||
|
srv := server.New()
|
||||||
|
if err := srv.Listen(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
backend := &dmsBackend{srv: srv, done: make(chan error, 1)}
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
backend.done <- fmt.Errorf("server panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
backend.done <- srv.Serve(false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func preLaunch() {
|
||||||
|
go printASCII()
|
||||||
|
ensureFontCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
func dmsExtraEnv(string) []string {
|
||||||
|
var env []string
|
||||||
|
if selfPath, err := os.Executable(); err == nil {
|
||||||
|
env = append(env, "DMS_EXECUTABLE="+selfPath)
|
||||||
|
}
|
||||||
|
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
|
||||||
|
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,62 +26,3 @@ func isArchPackageInstalled(packageName string) bool {
|
|||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type systemdServiceState struct {
|
|
||||||
Name string
|
|
||||||
EnabledState string
|
|
||||||
NeedsDisable bool
|
|
||||||
Exists bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
|
|
||||||
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
|
|
||||||
cmd := exec.Command("systemctl", "is-enabled", serviceName)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
|
|
||||||
stateStr := strings.TrimSpace(string(output))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
|
||||||
isKnownState := slices.Contains(knownStates, stateStr)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
+27
-27
@@ -1,14 +1,13 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1
|
github.com/alecthomas/chroma/v2 v2.27.0
|
||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v1.0.0
|
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
|
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
|
||||||
@@ -18,13 +17,12 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||||
github.com/yuin/goldmark v1.8.2
|
github.com/yuin/goldmark v1.8.4
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.5.0
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
golang.org/x/image v0.44.0
|
||||||
golang.org/x/image v0.39.0
|
tailscale.com v1.100.0
|
||||||
tailscale.com v1.96.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -32,28 +30,28 @@ require (
|
|||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
|
github.com/charmbracelet/log v1.0.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.4 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.15 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
|
github.com/dlclark/regexp2/v2 v2.5.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fogleman/gg v1.3.0 // indirect
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 // indirect
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||||
github.com/mdlayher/netlink v1.11.1 // indirect
|
github.com/mdlayher/netlink v1.11.2 // indirect
|
||||||
github.com/mdlayher/socket v0.6.0 // indirect
|
github.com/mdlayher/socket v0.6.1 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
@@ -63,13 +61,15 @@ require (
|
|||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/net v0.57.0 // indirect
|
||||||
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
@@ -79,22 +79,22 @@ require (
|
|||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||||
github.com/mattn/go-isatty v0.0.22
|
github.com/mattn/go-isatty v0.0.23
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/text v0.36.0
|
golang.org/x/text v0.40.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+56
-56
@@ -1,5 +1,7 @@
|
|||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d h1:EZN2x2uAX975Uvw4MByqWaaVTp2fOJrNTl4V4EQbSn8=
|
||||||
|
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d/go.mod h1:7p7cfydr4WM1G6eOPFlANXF3IV5du3FoA4CbDPprHAo=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||||
@@ -11,8 +13,8 @@ github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukD
|
|||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
@@ -48,25 +50,23 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
|
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
|
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
|
||||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
@@ -85,14 +85,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 h1:wH21vHuv323v9x78JNFNJ6P7HEAsdwr9yq2k9/o4zEE=
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E=
|
||||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
|
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA=
|
||||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
|
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
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/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
@@ -117,8 +117,8 @@ github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5G
|
|||||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -128,16 +128,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
|
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
||||||
github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
|
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
||||||
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
|
||||||
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
@@ -180,8 +180,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
|
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
@@ -193,36 +193,36 @@ github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR
|
|||||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
|
||||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
||||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||||
@@ -235,5 +235,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
|
||||||
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
@@ -17,6 +16,37 @@ const envMime = "_DMS_CLIPBOARD_MIME"
|
|||||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
|
type Offer struct {
|
||||||
|
MimeType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// textMimeAliases are offered alongside plain-text content so legacy X11
|
||||||
|
// clients bridged through XWayland find a target they can convert.
|
||||||
|
var textMimeAliases = []string{
|
||||||
|
"text/plain",
|
||||||
|
"text/plain;charset=utf-8",
|
||||||
|
"UTF8_STRING",
|
||||||
|
"STRING",
|
||||||
|
"TEXT",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandOffers turns raw clipboard data into the full offer list to serve,
|
||||||
|
// adding the standard alias set for text content.
|
||||||
|
func ExpandOffers(data []byte, mimeType string) []Offer {
|
||||||
|
offers := []Offer{{MimeType: mimeType, Data: data}}
|
||||||
|
if mimeType != "text/plain" && mimeType != "text/plain;charset=utf-8" {
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
for _, alias := range textMimeAliases {
|
||||||
|
if alias == mimeType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
offers = append(offers, Offer{MimeType: alias, Data: data})
|
||||||
|
}
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
|
||||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
func MaybeServeAndExit() {
|
func MaybeServeAndExit() {
|
||||||
@@ -44,7 +74,7 @@ func MaybeServeAndExit() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -55,22 +85,33 @@ func Copy(data []byte, mimeType string) error {
|
|||||||
return copyForkCached(data, mimeType, false)
|
return copyForkCached(data, mimeType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CopyText(text string) error {
|
||||||
|
return Copy([]byte(text), "text/plain;charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return serveClipboard(data, mimeType, pasteOnce)
|
return serveOffers(ExpandOffers(data, mimeType), pasteOnce)
|
||||||
}
|
}
|
||||||
return copyForkCached(data, mimeType, pasteOnce)
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if !foreground {
|
||||||
buf, err := io.ReadAll(data)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read source: %w", err)
|
|
||||||
}
|
|
||||||
return serveClipboard(buf, mimeType, pasteOnce)
|
|
||||||
}
|
}
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||||
|
if foreground {
|
||||||
|
return serveOffers(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyMultiFork(offers, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
@@ -132,39 +173,70 @@ func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
cmd := newForkCmd(mimeType, pasteOnce)
|
cmd := newForkCmd(mimeType, pasteOnce)
|
||||||
|
|
||||||
switch src := data.(type) {
|
if src, ok := data.(*os.File); ok {
|
||||||
case *os.File:
|
|
||||||
cmd.Stdin = src
|
cmd.Stdin = src
|
||||||
return waitReady(cmd)
|
return waitReady(cmd)
|
||||||
|
|
||||||
default:
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(stdin, data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
|
||||||
if err := stdin.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf [1]byte
|
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
|
}
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||||
|
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||||
|
if pasteOnce {
|
||||||
|
args = append(args, "--paste-once")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offer := range offers {
|
||||||
|
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||||
|
if _, err := stdin.Write(offer.Data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write offer data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
@@ -194,57 +266,25 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
// serveOffers owns the Wayland selection until cancelled (or first paste when
|
||||||
display, err := wlclient.Connect("")
|
// pasteOnce is set), answering every offered mime type with its data.
|
||||||
|
func serveOffers(offers []Offer, pasteOnce bool) error {
|
||||||
|
if len(offers) == 0 {
|
||||||
|
return fmt.Errorf("no offers to serve")
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := connectSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
defer display.Destroy()
|
defer s.Close()
|
||||||
|
|
||||||
ctx := display.Context()
|
dataControlMgr, err := s.requireDataControl()
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get registry: %w", err)
|
return err
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
device, err := dataControlMgr.GetDataDevice(s.seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get data device: %w", err)
|
return fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -255,25 +295,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("create data source: %w", err)
|
return fmt.Errorf("create data source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := source.Offer(mimeType); err != nil {
|
offerData := make(map[string][]byte, len(offers))
|
||||||
return fmt.Errorf("offer mime type: %w", err)
|
for _, offer := range offers {
|
||||||
}
|
if err := source.Offer(offer.MimeType); err != nil {
|
||||||
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||||
if err := source.Offer("text/plain"); err != nil {
|
|
||||||
return fmt.Errorf("offer text/plain: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
|
||||||
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("UTF8_STRING"); err != nil {
|
|
||||||
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("STRING"); err != nil {
|
|
||||||
return fmt.Errorf("offer STRING: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("TEXT"); err != nil {
|
|
||||||
return fmt.Errorf("offer TEXT: %w", err)
|
|
||||||
}
|
}
|
||||||
|
offerData[offer.MimeType] = offer.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
@@ -283,7 +310,11 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
_ = syscall.SetNonblock(e.Fd, false)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
_, _ = file.Write(data)
|
|
||||||
|
if data, ok := offerData[e.MimeType]; ok {
|
||||||
|
_, _ = file.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -298,7 +329,7 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("set selection: %w", err)
|
return fmt.Errorf("set selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
signalReady()
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -310,70 +341,26 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := s.ctx.Dispatch(); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyText(text string) error {
|
|
||||||
return Copy([]byte(text), "text/plain;charset=utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paste() ([]byte, string, error) {
|
func Paste() ([]byte, string, error) {
|
||||||
display, err := wlclient.Connect("")
|
s, err := connectSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
return nil, "", err
|
||||||
}
|
}
|
||||||
defer display.Destroy()
|
defer s.Close()
|
||||||
|
|
||||||
ctx := display.Context()
|
dataControlMgr, err := s.requireDataControl()
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get registry: %w", err)
|
return nil, "", err
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
device, err := dataControlMgr.GetDataDevice(s.seat)
|
||||||
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
|
|
||||||
if seat == nil {
|
|
||||||
return nil, "", fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
return nil, "", fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -399,15 +386,14 @@ func Paste() ([]byte, string, error) {
|
|||||||
gotSelection = true
|
gotSelection = true
|
||||||
})
|
})
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
if !gotSelection || selectionOffer == nil {
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
return nil, "", fmt.Errorf("no clipboard data")
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeTypes := offerMimeTypes[selectionOffer]
|
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
|
||||||
selectedMime := selectPreferredMimeType(mimeTypes)
|
|
||||||
if selectedMime == "" {
|
if selectedMime == "" {
|
||||||
return nil, "", fmt.Errorf("no supported mime type")
|
return nil, "", fmt.Errorf("no supported mime type")
|
||||||
}
|
}
|
||||||
@@ -424,7 +410,7 @@ func Paste() ([]byte, string, error) {
|
|||||||
}
|
}
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
data, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -470,161 +456,3 @@ func selectPreferredMimeType(mimes []string) string {
|
|||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Offer struct {
|
|
||||||
MimeType string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
|
||||||
return copyMultiFork(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
return copyMultiServe(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, offer := range offers {
|
|
||||||
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
|
||||||
if _, err := stdin.Write(offer.Data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write offer data: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
ctx := display.Context()
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get registry: %w", err)
|
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
source, err := dataControlMgr.CreateDataSource()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create data source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
offerMap := make(map[string][]byte)
|
|
||||||
for _, offer := range offers {
|
|
||||||
if err := source.Offer(offer.MimeType); err != nil {
|
|
||||||
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
|
||||||
}
|
|
||||||
offerMap[offer.MimeType] = offer.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
|
||||||
pasted := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
_ = syscall.SetNonblock(e.Fd, false)
|
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
|
||||||
_, _ = file.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case pasted <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
|
||||||
close(cancelled)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := device.SetSelection(source); err != nil {
|
|
||||||
return fmt.Errorf("set selection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-cancelled:
|
|
||||||
return nil
|
|
||||||
case <-pasted:
|
|
||||||
if pasteOnce {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := ctx.Dispatch(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xkbKeymapFormatV1 = 1
|
||||||
|
|
||||||
|
keyStateReleased = 0
|
||||||
|
keyStatePressed = 1
|
||||||
|
|
||||||
|
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
|
||||||
|
shiftModMask = 1 << 0
|
||||||
|
ctrlModMask = 1 << 2
|
||||||
|
|
||||||
|
// evdev fallbacks for a standard pc105 map
|
||||||
|
fallbackCtrlKey = 29 // KEY_LEFTCTRL
|
||||||
|
fallbackShiftKey = 42 // KEY_LEFTSHIFT
|
||||||
|
fallbackVKey = 47 // KEY_V
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
|
||||||
|
// using the seat's own keymap, so keycodes stay valid for XWayland clients
|
||||||
|
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
|
||||||
|
// selects ctrl+shift+v for terminal targets.
|
||||||
|
func SendPasteKeystroke(withShift bool) error {
|
||||||
|
s, err := connectSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if s.virtualKeyboardMgr == nil {
|
||||||
|
return fmt.Errorf("compositor does not support zwp_virtual_keyboard_manager_v1")
|
||||||
|
}
|
||||||
|
if s.seat == nil {
|
||||||
|
return fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard, err := s.seat.GetKeyboard()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get keyboard: %w", err)
|
||||||
|
}
|
||||||
|
defer keyboard.Release()
|
||||||
|
|
||||||
|
var keymap *wlclient.KeyboardKeymapEvent
|
||||||
|
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||||
|
if keymap == nil {
|
||||||
|
keymap = &e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.display.Roundtrip()
|
||||||
|
|
||||||
|
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
|
||||||
|
return fmt.Errorf("no xkb keymap from seat")
|
||||||
|
}
|
||||||
|
defer unix.Close(keymap.Fd)
|
||||||
|
|
||||||
|
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read keymap: %w", err)
|
||||||
|
}
|
||||||
|
keys := resolveKeycodes(keymapText)
|
||||||
|
|
||||||
|
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create virtual keyboard: %w", err)
|
||||||
|
}
|
||||||
|
defer vk.Destroy()
|
||||||
|
|
||||||
|
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
|
||||||
|
return fmt.Errorf("set keymap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mods := uint32(ctrlModMask)
|
||||||
|
held := []uint32{keys.ctrl}
|
||||||
|
if withShift {
|
||||||
|
mods |= shiftModMask
|
||||||
|
held = append(held, keys.shift)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := uint32(0)
|
||||||
|
press := func(key, state uint32) error {
|
||||||
|
t++
|
||||||
|
return vk.Key(t, key, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range held {
|
||||||
|
if err := press(key, keyStatePressed); err != nil {
|
||||||
|
return fmt.Errorf("key press: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
|
||||||
|
return fmt.Errorf("set modifiers: %w", err)
|
||||||
|
}
|
||||||
|
if err := press(keys.v, keyStatePressed); err != nil {
|
||||||
|
return fmt.Errorf("key press: %w", err)
|
||||||
|
}
|
||||||
|
if err := press(keys.v, keyStateReleased); err != nil {
|
||||||
|
return fmt.Errorf("key release: %w", err)
|
||||||
|
}
|
||||||
|
for i := len(held) - 1; i >= 0; i-- {
|
||||||
|
if err := press(held[i], keyStateReleased); err != nil {
|
||||||
|
return fmt.Errorf("key release: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
|
||||||
|
return fmt.Errorf("clear modifiers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.display.Roundtrip()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKeymap(fd int, size uint32) (string, error) {
|
||||||
|
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
text := strings.TrimRight(string(data), "\x00")
|
||||||
|
return text, unix.Munmap(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pasteKeycodes struct {
|
||||||
|
ctrl uint32
|
||||||
|
shift uint32
|
||||||
|
v uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
|
||||||
|
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
|
||||||
|
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
|
||||||
|
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// xkbcommon may serialize keysyms as hex escapes instead of names
|
||||||
|
// (e.g. "0x76" for v, "0xffe3" for Control_L).
|
||||||
|
var keysymNames = map[uint32]string{
|
||||||
|
0x76: "v",
|
||||||
|
0xffe3: "Control_L",
|
||||||
|
0xffe1: "Shift_L",
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalKeysym(sym string) string {
|
||||||
|
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(sym[2:], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
if name, ok := keysymNames[uint32(value)]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
|
||||||
|
// the seat keymap's first group, falling back to pc105 positions.
|
||||||
|
func resolveKeycodes(keymap string) pasteKeycodes {
|
||||||
|
codes := map[string]uint32{}
|
||||||
|
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
|
||||||
|
if code, err := strconv.Atoi(m[2]); err == nil {
|
||||||
|
codes[m[1]] = uint32(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
|
||||||
|
want := map[string]*uint32{
|
||||||
|
"Control_L": &keys.ctrl,
|
||||||
|
"Shift_L": &keys.shift,
|
||||||
|
"v": &keys.v,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
|
||||||
|
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
|
||||||
|
if group == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xkbCode, ok := codes[m[1]]
|
||||||
|
if !ok || xkbCode < 8 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
|
||||||
|
target, wanted := want[level1]
|
||||||
|
if !wanted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*target = xkbCode - 8
|
||||||
|
delete(want, level1)
|
||||||
|
if len(want) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLiveSeatKeymapResolution(t *testing.T) {
|
||||||
|
if os.Getenv("DMS_LIVE_TEST") == "" {
|
||||||
|
t.Skip("set DMS_LIVE_TEST=1 to run against the live compositor")
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := connectSession()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("connect: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if s.virtualKeyboardMgr == nil {
|
||||||
|
t.Fatal("compositor does not advertise zwp_virtual_keyboard_manager_v1")
|
||||||
|
}
|
||||||
|
if s.seat == nil {
|
||||||
|
t.Fatal("no seat")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard, err := s.seat.GetKeyboard()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get keyboard: %v", err)
|
||||||
|
}
|
||||||
|
defer keyboard.Release()
|
||||||
|
|
||||||
|
var keymap *wlclient.KeyboardKeymapEvent
|
||||||
|
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||||
|
if keymap == nil {
|
||||||
|
keymap = &e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.display.Roundtrip()
|
||||||
|
|
||||||
|
if keymap == nil {
|
||||||
|
t.Fatal("no keymap event")
|
||||||
|
}
|
||||||
|
defer unix.Close(keymap.Fd)
|
||||||
|
|
||||||
|
text, err := readKeymap(keymap.Fd, keymap.Size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read keymap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dump := os.Getenv("DMS_LIVE_DUMP"); dump != "" {
|
||||||
|
if err := os.WriteFile(dump, []byte(text), 0o644); err != nil {
|
||||||
|
t.Fatalf("dump keymap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := resolveKeycodes(text)
|
||||||
|
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
|
||||||
|
|
||||||
|
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
|
||||||
|
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
const qwertyKeymap = `xkb_keymap {
|
||||||
|
xkb_keycodes "(unnamed)" {
|
||||||
|
minimum = 8;
|
||||||
|
maximum = 708;
|
||||||
|
<ESC> = 9;
|
||||||
|
<AB04> = 55;
|
||||||
|
<LCTL> = 37;
|
||||||
|
<LFSH> = 50;
|
||||||
|
alias <AL01> = <AC01>;
|
||||||
|
indicator 1 = "Caps Lock";
|
||||||
|
};
|
||||||
|
xkb_types "(unnamed)" {
|
||||||
|
type "ALPHABETIC" {
|
||||||
|
modifiers = Shift+Lock;
|
||||||
|
map[Shift] = Level2;
|
||||||
|
level_name[Level1] = "Base";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
xkb_symbols "(unnamed)" {
|
||||||
|
key <ESC> { [ Escape ] };
|
||||||
|
key <AB04> { type= "ALPHABETIC", [ v, V ] };
|
||||||
|
key <LCTL> { [ Control_L ] };
|
||||||
|
key <LFSH> { [ Shift_L ] };
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
const hexKeymap = `xkb_keymap {
|
||||||
|
xkb_keycodes "(unnamed)" {
|
||||||
|
<AB04> = 56;
|
||||||
|
<LCTL> = 38;
|
||||||
|
<LFSH> = 51;
|
||||||
|
};
|
||||||
|
xkb_symbols "(unnamed)" {
|
||||||
|
key <LCTL> { [ 0xffe3 ] };
|
||||||
|
key <LFSH> {
|
||||||
|
type= "PC_ALT_LEVEL2",
|
||||||
|
symbols[1]= [ 0xffe1, 0xfe08 ]
|
||||||
|
};
|
||||||
|
key <AB04> { [ 0x76, 0x56 ] };
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
const dvorakKeymap = `xkb_keymap {
|
||||||
|
xkb_keycodes "(unnamed)" {
|
||||||
|
<AB09> = 60;
|
||||||
|
<LCTL> = 37;
|
||||||
|
<LFSH> = 50;
|
||||||
|
};
|
||||||
|
xkb_symbols "(unnamed)" {
|
||||||
|
key <AB09> { [ v, V ] };
|
||||||
|
key <LCTL> { [ Control_L ] };
|
||||||
|
key <LFSH> { [ Shift_L ] };
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
func TestResolveKeycodes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
keymap string
|
||||||
|
want pasteKeycodes
|
||||||
|
}{
|
||||||
|
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
|
||||||
|
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
|
||||||
|
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
|
||||||
|
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := resolveKeycodes(tt.keymap)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandOffers(t *testing.T) {
|
||||||
|
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
|
||||||
|
if len(text) != 5 {
|
||||||
|
t.Fatalf("expected 5 text offers, got %d", len(text))
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, o := range text {
|
||||||
|
if string(o.Data) != "hi" {
|
||||||
|
t.Errorf("offer %s has wrong data", o.MimeType)
|
||||||
|
}
|
||||||
|
if seen[o.MimeType] {
|
||||||
|
t.Errorf("duplicate offer %s", o.MimeType)
|
||||||
|
}
|
||||||
|
seen[o.MimeType] = true
|
||||||
|
}
|
||||||
|
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
|
||||||
|
t.Errorf("missing X11 alias offers: %v", seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
img := ExpandOffers([]byte{1}, "image/png")
|
||||||
|
if len(img) != 1 || img[0].MimeType != "image/png" {
|
||||||
|
t.Errorf("non-text mime should not expand, got %+v", img)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
display *wlclient.Display
|
||||||
|
ctx *wlclient.Context
|
||||||
|
registry *wlclient.Registry
|
||||||
|
seat *wlclient.Seat
|
||||||
|
dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||||
|
virtualKeyboardMgr *virtual_keyboard.ZwpVirtualKeyboardManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectSession opens a short-lived Wayland connection and binds the seat
|
||||||
|
// plus whichever clipboard-related globals the compositor advertises.
|
||||||
|
func connectSession() (*session, error) {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &session{display: display, ctx: display.Context()}
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
display.Destroy()
|
||||||
|
return nil, fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
s.registry = registry
|
||||||
|
|
||||||
|
var bindErr error
|
||||||
|
bind := func(name uint32, iface string, version uint32, proxy wlclient.Proxy) {
|
||||||
|
if err := registry.Bind(name, iface, version, proxy); err != nil {
|
||||||
|
bindErr = fmt.Errorf("bind %s: %w", iface, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case ext_data_control.ExtDataControlManagerV1InterfaceName:
|
||||||
|
mgr := ext_data_control.NewExtDataControlManagerV1(s.ctx)
|
||||||
|
bind(e.Name, e.Interface, e.Version, mgr)
|
||||||
|
s.dataControlMgr = mgr
|
||||||
|
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
|
||||||
|
mgr := virtual_keyboard.NewZwpVirtualKeyboardManagerV1(s.ctx)
|
||||||
|
bind(e.Name, e.Interface, e.Version, mgr)
|
||||||
|
s.virtualKeyboardMgr = mgr
|
||||||
|
case "wl_seat":
|
||||||
|
if s.seat != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seat := wlclient.NewSeat(s.ctx)
|
||||||
|
bind(e.Name, e.Interface, e.Version, seat)
|
||||||
|
s.seat = seat
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, bindErr
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) requireDataControl() (*ext_data_control.ExtDataControlManagerV1, error) {
|
||||||
|
switch {
|
||||||
|
case s.dataControlMgr == nil:
|
||||||
|
return nil, fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
case s.seat == nil:
|
||||||
|
return nil, fmt.Errorf("no seat available")
|
||||||
|
default:
|
||||||
|
return s.dataControlMgr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Close() {
|
||||||
|
if s.dataControlMgr != nil {
|
||||||
|
s.dataControlMgr.Destroy()
|
||||||
|
}
|
||||||
|
if s.registry != nil {
|
||||||
|
s.registry.Destroy()
|
||||||
|
}
|
||||||
|
s.display.Destroy()
|
||||||
|
}
|
||||||
@@ -61,6 +61,10 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
@@ -73,6 +77,10 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
||||||
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
},
|
},
|
||||||
|
"Mango": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "mango", "mango.conf"),
|
||||||
|
},
|
||||||
"Ghostty": {
|
"Ghostty": {
|
||||||
filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||||
},
|
},
|
||||||
@@ -126,6 +134,14 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
if shouldReplaceConfig("Mango") {
|
||||||
|
result, err := cd.deployMangoConfig(terminal, useSystemd)
|
||||||
|
results = append(results, result)
|
||||||
|
if err != nil {
|
||||||
|
return results, fmt.Errorf("failed to deploy Mango config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch terminal {
|
switch terminal {
|
||||||
@@ -269,6 +285,96 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployMangoConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
|
result := DeploymentResult{
|
||||||
|
ConfigType: "Mango",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(result.Path)
|
||||||
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
var terminalCommand string
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
terminalCommand = "ghostty"
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
terminalCommand = "kitty"
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
terminalCommand = "alacritty"
|
||||||
|
default:
|
||||||
|
terminalCommand = "ghostty"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DMS owns config.conf for mango (like niri/hyprland): back up and replace.
|
||||||
|
if existingData, err := os.ReadFile(result.Path); err == nil {
|
||||||
|
cd.log("Found existing Mango configuration")
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
|
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := strings.ReplaceAll(MangoConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cd.deployMangoDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Deployed = true
|
||||||
|
cd.log("Successfully deployed Mango configuration")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string) error {
|
||||||
|
configs := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
overwrite bool
|
||||||
|
}{
|
||||||
|
// binds.conf is DMS-owned (overwrite); the rest are runtime/user-managed.
|
||||||
|
{"binds.conf", strings.ReplaceAll(MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand), true},
|
||||||
|
{"colors.conf", MangoColorsConfig, false},
|
||||||
|
{"layout.conf", MangoLayoutConfig, false},
|
||||||
|
{"outputs.conf", "", false},
|
||||||
|
{"cursor.conf", "", false},
|
||||||
|
{"windowrules.conf", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
|
if !cfg.overwrite {
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
@@ -600,6 +706,10 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupStrayHyprlandConfFile(func(format string, v ...any) {
|
||||||
|
cd.log(fmt.Sprintf(format, v...))
|
||||||
|
})
|
||||||
|
|
||||||
result.Deployed = true
|
result.Deployed = true
|
||||||
cd.log("Successfully deployed Hyprland configuration")
|
cd.log("Successfully deployed Hyprland configuration")
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -20,13 +20,17 @@ func TestCleanupStrayHyprlandConfFile(t *testing.T) {
|
|||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
t.Setenv("HOME", td)
|
t.Setenv("HOME", td)
|
||||||
configDir := filepath.Join(td, ".config", "hypr")
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
confPath := filepath.Join(configDir, "hyprland.conf")
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
dmsConfPath := filepath.Join(dmsDir, "colors.conf")
|
||||||
require.NoError(t, os.WriteFile(confPath, []byte("# legacy user config\n"), 0o644))
|
require.NoError(t, os.WriteFile(confPath, []byte("# legacy user config\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(dmsConfPath, []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
|
||||||
|
|
||||||
CleanupStrayHyprlandConfFile(nil)
|
CleanupStrayHyprlandConfFile(nil)
|
||||||
|
|
||||||
assert.FileExists(t, confPath, "must not touch hyprland.conf when user has not migrated")
|
assert.FileExists(t, confPath, "must not touch hyprland.conf when user has not migrated")
|
||||||
|
assert.FileExists(t, dmsConfPath, "must not touch dms/*.conf when user has not migrated")
|
||||||
assert.NoDirExists(t, filepath.Join(configDir, hyprlandBackupDirName))
|
assert.NoDirExists(t, filepath.Join(configDir, hyprlandBackupDirName))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,20 +38,25 @@ func TestCleanupStrayHyprlandConfFile(t *testing.T) {
|
|||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
t.Setenv("HOME", td)
|
t.Setenv("HOME", td)
|
||||||
configDir := filepath.Join(td, ".config", "hypr")
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
luaPath := filepath.Join(configDir, "hyprland.lua")
|
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||||
require.NoError(t, os.WriteFile(luaPath, []byte("-- dms managed\n"), 0o644))
|
require.NoError(t, os.WriteFile(luaPath, []byte("-- dms managed\n"), 0o644))
|
||||||
confPath := filepath.Join(configDir, "hyprland.conf")
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
dmsConfPath := filepath.Join(dmsDir, "colors.conf")
|
||||||
require.NoError(t, os.WriteFile(confPath, []byte("# autogen\n"), 0o644))
|
require.NoError(t, os.WriteFile(confPath, []byte("# autogen\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(dmsConfPath, []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
|
||||||
|
|
||||||
CleanupStrayHyprlandConfFile(nil)
|
CleanupStrayHyprlandConfFile(nil)
|
||||||
|
|
||||||
assert.NoFileExists(t, confPath)
|
assert.NoFileExists(t, confPath)
|
||||||
|
assert.NoFileExists(t, dmsConfPath)
|
||||||
assert.FileExists(t, luaPath)
|
assert.FileExists(t, luaPath)
|
||||||
entries, err := os.ReadDir(filepath.Join(configDir, hyprlandBackupDirName))
|
entries, err := os.ReadDir(filepath.Join(configDir, hyprlandBackupDirName))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, entries, 1)
|
require.Len(t, entries, 1)
|
||||||
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "hyprland.conf"))
|
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "hyprland.conf"))
|
||||||
|
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "dms", "colors.conf"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +413,7 @@ general {
|
|||||||
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
||||||
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf"), []byte("bind = SUPER, T, exec, foot\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf"), []byte("bind = SUPER, T, exec, foot\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "colors.conf"), []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "cursor.conf"), []byte("env = XCURSOR_SIZE,24\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "cursor.conf"), []byte("env = XCURSOR_SIZE,24\n"), 0o644))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"), []byte("old backup\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"), []byte("old backup\n"), 0o644))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf.backup.old"), []byte("old dms backup\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf.backup.old"), []byte("old dms backup\n"), 0o644))
|
||||||
@@ -423,10 +433,12 @@ general {
|
|||||||
assert.Contains(t, result.BackupPath, hyprlandBackupDirName)
|
assert.Contains(t, result.BackupPath, hyprlandBackupDirName)
|
||||||
assert.NoFileExists(t, hyprPath)
|
assert.NoFileExists(t, hyprPath)
|
||||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf"))
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf"))
|
||||||
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "colors.conf"))
|
||||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "cursor.conf"))
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "cursor.conf"))
|
||||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf.backup.old"))
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf.backup.old"))
|
||||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf.backup.old"))
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf.backup.old"))
|
||||||
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf"))
|
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(dmsDir, "colors.conf"))
|
||||||
assert.NoFileExists(t, filepath.Join(dmsDir, "cursor.conf"))
|
assert.NoFileExists(t, filepath.Join(dmsDir, "cursor.conf"))
|
||||||
assert.NoFileExists(t, filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
|
assert.NoFileExists(t, filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
|
||||||
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
|
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
|
||||||
@@ -485,7 +497,7 @@ general {
|
|||||||
managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua"))
|
managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(managed), `hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))`)
|
assert.Contains(t, string(managed), `hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))`)
|
||||||
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })`)
|
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })`)
|
||||||
|
|
||||||
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -508,6 +520,18 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandLuaConfig, "input =")
|
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMangoConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, MangoConfig, "exec-once=dms run")
|
||||||
|
assert.NotContains(t, MangoConfig, "exec_once=dms run")
|
||||||
|
assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,K,focusdir,up")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,L,focusdir,right")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "gesturebind=none,right,3,viewtoleft_have_client")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "gesturebind=none,left,3,viewtoright_have_client")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
|
|||||||
copy-on-select = false
|
copy-on-select = false
|
||||||
confirm-close-surface = false
|
confirm-close-surface = false
|
||||||
|
|
||||||
# Disable annoying copied to clipboard
|
# Disable in-app Ghostty toast notifications
|
||||||
app-notifications = no-clipboard-copy,no-config-reload
|
app-notifications = false
|
||||||
|
|
||||||
# Key bindings for common actions
|
# Key bindings for common actions
|
||||||
#keybind = ctrl+c=copy_to_clipboard
|
#keybind = ctrl+c=copy_to_clipboard
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
|||||||
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
||||||
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
||||||
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
|
||||||
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
|
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
||||||
|
|
||||||
-- === Cheat sheet
|
-- === Cheat sheet
|
||||||
@@ -38,7 +39,7 @@ hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd([[dms ipc call brightness increme
|
|||||||
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd([[dms ipc call brightness decrement 5 ""]]), { locked = true, repeating = true })
|
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd([[dms ipc call brightness decrement 5 ""]]), { locked = true, repeating = true })
|
||||||
|
|
||||||
-- === Window Management ===
|
-- === Window Management ===
|
||||||
hl.bind("SUPER + Q", hl.dsp.window.kill())
|
hl.bind("SUPER + Q", hl.dsp.window.close())
|
||||||
hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))
|
hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))
|
||||||
hl.bind("SUPER + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" }))
|
hl.bind("SUPER + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" }))
|
||||||
hl.bind("SUPER + SHIFT + T", hl.dsp.window.float({ action = "toggle" }))
|
hl.bind("SUPER + SHIFT + T", hl.dsp.window.float({ action = "toggle" }))
|
||||||
@@ -112,6 +113,9 @@ hl.bind("SUPER + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
|||||||
hl.bind("SUPER + CTRL + mouse_down", hl.dsp.window.move({ workspace = "e+1" }))
|
hl.bind("SUPER + CTRL + mouse_down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
hl.bind("SUPER + CTRL + mouse_up", hl.dsp.window.move({ workspace = "e-1" }))
|
hl.bind("SUPER + CTRL + mouse_up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
|
||||||
|
-- === Touchpad Gestures ===
|
||||||
|
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||||
|
|
||||||
-- === Numbered Workspaces ===
|
-- === Numbered Workspaces ===
|
||||||
hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))
|
hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))
|
||||||
hl.bind("SUPER + 2", hl.dsp.focus({ workspace = "2" }))
|
hl.bind("SUPER + 2", hl.dsp.focus({ workspace = "2" }))
|
||||||
@@ -140,7 +144,7 @@ hl.bind("SUPER + bracketright", hl.dsp.layout("preselect r"))
|
|||||||
|
|
||||||
-- === Sizing & Layout ===
|
-- === Sizing & Layout ===
|
||||||
hl.bind("SUPER + R", hl.dsp.layout("togglesplit"))
|
hl.bind("SUPER + R", hl.dsp.layout("togglesplit"))
|
||||||
hl.bind("SUPER + CTRL + F", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive exact 100% 100%]]))
|
hl.bind("SUPER + CTRL + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "set" }))
|
||||||
|
|
||||||
-- === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
-- === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window" })
|
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window" })
|
||||||
@@ -150,10 +154,10 @@ hl.bind("SUPER + code:20", hl.dsp.window.resize({ x = -100, y = 0, relative = tr
|
|||||||
hl.bind("SUPER + code:21", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { description = "Shrink window left" })
|
hl.bind("SUPER + code:21", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { description = "Shrink window left" })
|
||||||
|
|
||||||
-- === Manual Sizing ===
|
-- === Manual Sizing ===
|
||||||
hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })
|
hl.bind("SUPER + minus", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })
|
||||||
hl.bind("SUPER + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 10% 0]]), { repeating = true })
|
hl.bind("SUPER + equal", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { repeating = true })
|
||||||
hl.bind("SUPER + SHIFT + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 -10%]]), { repeating = true })
|
hl.bind("SUPER + SHIFT + minus", hl.dsp.window.resize({ x = 0, y = -100, relative = true }), { repeating = true })
|
||||||
hl.bind("SUPER + SHIFT + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 10%]]), { repeating = true })
|
hl.bind("SUPER + SHIFT + equal", hl.dsp.window.resize({ x = 0, y = 100, relative = true }), { repeating = true })
|
||||||
|
|
||||||
-- === Screenshots ===
|
-- === Screenshots ===
|
||||||
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
|
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ end)
|
|||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
input = {
|
input = {
|
||||||
kb_layout = "us",
|
-- empty inherits XKB_DEFAULT_LAYOUT (libxkbcommon), falls back to "us"
|
||||||
|
kb_layout = "",
|
||||||
numlock_by_default = true,
|
numlock_by_default = true,
|
||||||
|
follow_mouse = 0,
|
||||||
|
touchpad = {
|
||||||
|
tap_to_click = true,
|
||||||
|
natural_scroll = true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
general = {
|
general = {
|
||||||
gaps_in = 5,
|
gaps_in = 5,
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
|
||||||
|
# Format: bind=MODS,key,action[,args]
|
||||||
|
# Put bind descriptions above bind lines; inline # comments break Mango spawn args.
|
||||||
|
|
||||||
|
# === Application Launchers ===
|
||||||
|
# Open Terminal
|
||||||
|
bind=SUPER,t,spawn,{{TERMINAL_COMMAND}}
|
||||||
|
# Open Terminal
|
||||||
|
bind=SUPER,Return,spawn,{{TERMINAL_COMMAND}}
|
||||||
|
# Application Launcher
|
||||||
|
bind=SUPER,space,spawn,dms ipc call spotlight toggle
|
||||||
|
# Spotlight Bar
|
||||||
|
bind=ALT,space,spawn,dms ipc call spotlight-bar toggle
|
||||||
|
# Clipboard Manager
|
||||||
|
bind=SUPER,v,spawn,dms ipc call clipboard toggle
|
||||||
|
# Task Manager
|
||||||
|
bind=SUPER,m,spawn,dms ipc call processlist focusOrToggle
|
||||||
|
# Settings
|
||||||
|
bind=SUPER,comma,spawn,dms ipc call settings focusOrToggle
|
||||||
|
# Notification Center
|
||||||
|
bind=SUPER,n,spawn,dms ipc call notifications toggle
|
||||||
|
# Notepad
|
||||||
|
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||||
|
# Browse Wallpapers
|
||||||
|
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
|
||||||
|
# Power Menu
|
||||||
|
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||||
|
# Cycle Display Profile
|
||||||
|
bind=SUPER,p,spawn,dms ipc outputs cycleProfile
|
||||||
|
|
||||||
|
# === Cheat sheet ===
|
||||||
|
# Keyboard Shortcuts
|
||||||
|
bind=SUPER+SHIFT,slash,spawn,dms ipc call keybinds toggle mangowc
|
||||||
|
|
||||||
|
# === Security ===
|
||||||
|
# Lock Screen
|
||||||
|
bind=SUPER+ALT,l,spawn,dms ipc call lock lock
|
||||||
|
# Task Manager
|
||||||
|
bind=CTRL+ALT,Delete,spawn,dms ipc call processlist focusOrToggle
|
||||||
|
|
||||||
|
# === Window Rules ===
|
||||||
|
# Create Window Rule
|
||||||
|
bind=SUPER+SHIFT,w,spawn,dms ipc call window-rules toggle
|
||||||
|
|
||||||
|
# === Screenshots ===
|
||||||
|
# Screenshot: Interactive
|
||||||
|
bind=none,Print,spawn,dms screenshot
|
||||||
|
# Screenshot: Full Screen
|
||||||
|
bind=CTRL,Print,spawn,dms screenshot full
|
||||||
|
# Screenshot: Window
|
||||||
|
bind=ALT,Print,spawn,dms screenshot window
|
||||||
|
|
||||||
|
# === Audio Controls ===
|
||||||
|
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
|
||||||
|
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
|
||||||
|
bind=none,XF86AudioMute,spawn,dms ipc call audio mute
|
||||||
|
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
|
||||||
|
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
|
||||||
|
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
|
||||||
|
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
|
||||||
|
bind=none,XF86AudioNext,spawn,dms ipc call mpris next
|
||||||
|
|
||||||
|
# === Brightness Controls ===
|
||||||
|
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
|
||||||
|
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
|
||||||
|
|
||||||
|
# === Window Management ===
|
||||||
|
# Close Window
|
||||||
|
bind=SUPER,q,killclient,
|
||||||
|
bind=SUPER,f,togglefullscreen,
|
||||||
|
bind=SUPER,a,togglemaximizescreen,
|
||||||
|
bind=SUPER+SHIFT,space,togglefloating,
|
||||||
|
bind=SUPER,o,toggleoverview
|
||||||
|
bind=ALT,Tab,toggleoverview
|
||||||
|
# Exit Compositor
|
||||||
|
bind=SUPER+SHIFT,e,quit,
|
||||||
|
|
||||||
|
# === Focus Navigation ===
|
||||||
|
bind=SUPER,Tab,focusstack,next
|
||||||
|
bind=SUPER+SHIFT,Tab,focusstack,prev
|
||||||
|
bind=SUPER,Left,focusdir,left
|
||||||
|
bind=SUPER,H,focusdir,left
|
||||||
|
bind=SUPER,Right,focusdir,right
|
||||||
|
bind=SUPER,L,focusdir,right
|
||||||
|
bind=SUPER,Up,focusdir,up
|
||||||
|
bind=SUPER,K,focusdir,up
|
||||||
|
bind=SUPER,Down,focusdir,down
|
||||||
|
bind=SUPER,J,focusdir,down
|
||||||
|
|
||||||
|
# === Window Movement ===
|
||||||
|
bind=SUPER+SHIFT,Left,exchange_client,left
|
||||||
|
bind=SUPER+SHIFT,Right,exchange_client,right
|
||||||
|
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||||
|
bind=SUPER+SHIFT,Down,exchange_client,down
|
||||||
|
bind=SUPER+SHIFT,H,exchange_client,left
|
||||||
|
bind=SUPER+SHIFT,L,exchange_client,right
|
||||||
|
bind=SUPER+SHIFT,K,exchange_client,up
|
||||||
|
bind=SUPER+SHIFT,J,exchange_client,down
|
||||||
|
|
||||||
|
# === Monitor Navigation ===
|
||||||
|
bind=SUPER+ALT,Left,focusmon,left
|
||||||
|
bind=SUPER+ALT,Right,focusmon,right
|
||||||
|
bind=SUPER+ALT+SHIFT,Left,tagmon,left
|
||||||
|
bind=SUPER+ALT+SHIFT,Right,tagmon,right
|
||||||
|
|
||||||
|
# === Layout ===
|
||||||
|
# Cycle Layout - Gaps, Floating, Tiling
|
||||||
|
bind=SUPER+ALT,j,switch_layout
|
||||||
|
bind=SUPER+SHIFT,equal,incgaps,1
|
||||||
|
bind=SUPER+SHIFT,minus,incgaps,-1
|
||||||
|
|
||||||
|
# === Tags (1-9): view tag ===
|
||||||
|
bind=SUPER,1,view,1
|
||||||
|
bind=SUPER,2,view,2
|
||||||
|
bind=SUPER,3,view,3
|
||||||
|
bind=SUPER,4,view,4
|
||||||
|
bind=SUPER,5,view,5
|
||||||
|
bind=SUPER,6,view,6
|
||||||
|
bind=SUPER,7,view,7
|
||||||
|
bind=SUPER,8,view,8
|
||||||
|
bind=SUPER,9,view,9
|
||||||
|
|
||||||
|
# === Tags (1-9): move focused window to tag ===
|
||||||
|
bind=SUPER+SHIFT,1,tag,1
|
||||||
|
bind=SUPER+SHIFT,2,tag,2
|
||||||
|
bind=SUPER+SHIFT,3,tag,3
|
||||||
|
bind=SUPER+SHIFT,4,tag,4
|
||||||
|
bind=SUPER+SHIFT,5,tag,5
|
||||||
|
bind=SUPER+SHIFT,6,tag,6
|
||||||
|
bind=SUPER+SHIFT,7,tag,7
|
||||||
|
bind=SUPER+SHIFT,8,tag,8
|
||||||
|
bind=SUPER+SHIFT,9,tag,9
|
||||||
|
|
||||||
|
# === Touchpad Gestures ===
|
||||||
|
# 3-finger horizontal swipe: switch between occupied workspaces
|
||||||
|
gesturebind=none,right,3,viewtoleft_have_client
|
||||||
|
gesturebind=none,left,3,viewtoright_have_client
|
||||||
|
# 4-finger vertical swipe: toggle the overview
|
||||||
|
gesturebind=none,up,4,toggleoverview
|
||||||
|
gesturebind=none,down,4,toggleoverview
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Auto-generated by DMS. Overwritten by matugen (dms/colors.conf).
|
||||||
|
# Remove `source=./dms/colors.conf` from config.conf to override manually.
|
||||||
|
|
||||||
|
bordercolor = 0x595959ff
|
||||||
|
focuscolor = 0x8ab4f8ff
|
||||||
|
urgentcolor = 0xff5555ff
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Auto-generated by DMS. Regenerated from DMS settings (dms/layout.conf).
|
||||||
|
|
||||||
|
border_radius=12
|
||||||
|
gappih=5
|
||||||
|
gappiv=5
|
||||||
|
gappoh=5
|
||||||
|
gappov=5
|
||||||
|
borderpx=2
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# DankMaterialShell — MangoWM configuration (managed by `dms setup`)
|
||||||
|
# Keybinds, colors, layout, outputs, cursor and window rules are pulled from the
|
||||||
|
# ./dms fragments below. Add your own binds/rules here; they sit alongside DMS's.
|
||||||
|
|
||||||
|
env=XDG_CURRENT_DESKTOP,mango
|
||||||
|
env=XDG_SESSION_TYPE,wayland
|
||||||
|
|
||||||
|
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
|
||||||
|
# every exec= on each config reload, and DMS reloads the config, which would
|
||||||
|
# spawn a new shell on every reload.
|
||||||
|
exec-once=dms run
|
||||||
|
|
||||||
|
source=./dms/colors.conf
|
||||||
|
source=./dms/layout.conf
|
||||||
|
source=./dms/cursor.conf
|
||||||
|
source=./dms/outputs.conf
|
||||||
|
source=./dms/windowrules.conf
|
||||||
|
source=./dms/binds.conf
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
binds {
|
binds {
|
||||||
// === System & Overview ===
|
// === System & Overview ===
|
||||||
Mod+D repeat=false { toggle-overview; }
|
Mod+O repeat=false { toggle-overview; }
|
||||||
Mod+Tab repeat=false { toggle-overview; }
|
Mod+Tab repeat=false { toggle-overview; }
|
||||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ binds {
|
|||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
|
||||||
}
|
}
|
||||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
@@ -51,24 +51,24 @@ binds {
|
|||||||
XF86AudioMicMute allow-when-locked=true {
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||||
}
|
}
|
||||||
XF86AudioPause allow-when-locked=true {
|
XF86AudioPause allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
}
|
}
|
||||||
XF86AudioPlay allow-when-locked=true {
|
XF86AudioPlay allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
}
|
}
|
||||||
XF86AudioPrev allow-when-locked=true {
|
XF86AudioPrev allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "previous";
|
spawn "dms" "ipc" "call" "mpris" "previous";
|
||||||
}
|
}
|
||||||
XF86AudioNext allow-when-locked=true {
|
XF86AudioNext allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "next";
|
spawn "dms" "ipc" "call" "mpris" "next";
|
||||||
}
|
}
|
||||||
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
||||||
}
|
}
|
||||||
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Brightness Controls ===
|
// === Brightness Controls ===
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
@@ -20,3 +20,10 @@ window-rule {
|
|||||||
tiled-state true
|
tiled-state true
|
||||||
draw-border-with-background false
|
draw-border-with-background false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layer-rule {
|
||||||
|
exclude namespace="^dms:bar$"
|
||||||
|
background-effect {
|
||||||
|
xray false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -265,9 +265,9 @@ recent-windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Include dms files
|
// Include dms files
|
||||||
include "dms/colors.kdl"
|
include optional=true "dms/colors.kdl"
|
||||||
include "dms/layout.kdl"
|
include optional=true "dms/layout.kdl"
|
||||||
include "dms/alttab.kdl"
|
include optional=true "dms/alttab.kdl"
|
||||||
include "dms/binds.kdl"
|
include optional=true "dms/binds.kdl"
|
||||||
include "dms/outputs.kdl"
|
include optional=true "dms/outputs.kdl"
|
||||||
include "dms/cursor.kdl"
|
include optional=true "dms/cursor.kdl"
|
||||||
|
|||||||
@@ -138,11 +138,9 @@ func readExistingHyprlandConfig(configDir string) (data string, sourcePath strin
|
|||||||
return "", "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanupStrayHyprlandConfFile moves a stray ~/.config/hypr/hyprland.conf
|
// CleanupStrayHyprlandConfFile moves stray ~/.config/hypr/hyprland.conf and
|
||||||
// into .dms-backups/<timestamp>/ only when hyprland.lua also exists, which
|
// top-level ~/.config/hypr/dms/*.conf files into .dms-backups/<timestamp>/ only
|
||||||
// proves Lua is the live config and the .conf is an autogen Hyprland 0.55
|
// when hyprland.lua also exists as the live config.
|
||||||
// produced when launched without -c. If only hyprland.conf exists, the user
|
|
||||||
// has not migrated and we must leave their config alone.
|
|
||||||
func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
|
func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
|
||||||
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" {
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" {
|
||||||
return
|
return
|
||||||
@@ -156,19 +154,44 @@ func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
|
|||||||
if _, err := os.Stat(luaPath); err != nil {
|
if _, err := os.Stat(luaPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var strayPaths []string
|
||||||
confPath := filepath.Join(configDir, "hyprland.conf")
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
if _, err := os.Stat(confPath); err != nil {
|
if info, err := os.Lstat(confPath); err == nil && !info.IsDir() {
|
||||||
return
|
strayPaths = append(strayPaths, confPath)
|
||||||
}
|
}
|
||||||
ts := time.Now().Format("2006-01-02_15-04-05")
|
dmsConfPaths, err := filepath.Glob(filepath.Join(configDir, "dms", "*.conf"))
|
||||||
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, "hyprland.conf")
|
if err == nil {
|
||||||
if err := moveHyprlandConfigFile(confPath, dst); err != nil {
|
for _, p := range dmsConfPaths {
|
||||||
if logFn != nil {
|
if info, err := os.Lstat(p); err == nil && !info.IsDir() {
|
||||||
logFn("Could not move stray hyprland.conf: %v", err)
|
strayPaths = append(strayPaths, p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if len(strayPaths) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if logFn != nil {
|
|
||||||
logFn("Moved stray hyprland.conf to %s", dst)
|
ts := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
moved := 0
|
||||||
|
for _, src := range strayPaths {
|
||||||
|
rel, err := filepath.Rel(configDir, src)
|
||||||
|
if err != nil {
|
||||||
|
rel = filepath.Base(src)
|
||||||
|
}
|
||||||
|
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, rel)
|
||||||
|
if err := moveHyprlandConfigFile(src, dst); err != nil {
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Could not move stray Hyprland conf file %s: %v", src, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
moved++
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Moved stray Hyprland conf file to %s", dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if moved > 0 && logFn != nil {
|
||||||
|
logFn("Moved %d stray Hyprland conf file(s) out of the active Lua config tree", moved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed embedded/mango.conf
|
||||||
|
var MangoConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/mango-colors.conf
|
||||||
|
var MangoColorsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/mango-layout.conf
|
||||||
|
var MangoLayoutConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/mango-binds.conf
|
||||||
|
var MangoBindsConfig string
|
||||||
@@ -16,6 +16,3 @@ var NiriAlttabConfig string
|
|||||||
|
|
||||||
//go:embed embedded/niri-binds.kdl
|
//go:embed embedded/niri-binds.kdl
|
||||||
var NiriBindsConfig string
|
var NiriBindsConfig string
|
||||||
|
|
||||||
//go:embed embedded/niri-greeter.kdl
|
|
||||||
var NiriGreeterConfig string
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type WindowManager int
|
|||||||
const (
|
const (
|
||||||
WindowManagerHyprland WindowManager = iota
|
WindowManagerHyprland WindowManager = iota
|
||||||
WindowManagerNiri
|
WindowManagerNiri
|
||||||
|
WindowManagerMango
|
||||||
)
|
)
|
||||||
|
|
||||||
type Terminal int
|
type Terminal int
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ func mergedAssociations() *MimeAssociations {
|
|||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSafeIniField rejects values that would corrupt a key=value line in mimeapps.list
|
||||||
|
func isSafeIniField(s string) bool {
|
||||||
|
return !strings.ContainsAny(s, "\n\r[]")
|
||||||
|
}
|
||||||
|
|
||||||
func writeUserMimeapps(update func(*MimeAssociations)) error {
|
func writeUserMimeapps(update func(*MimeAssociations)) error {
|
||||||
mimeappsWriteMu.Lock()
|
mimeappsWriteMu.Lock()
|
||||||
defer mimeappsWriteMu.Unlock()
|
defer mimeappsWriteMu.Unlock()
|
||||||
@@ -152,6 +157,7 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
w := bufio.NewWriter(&buf)
|
w := bufio.NewWriter(&buf)
|
||||||
|
|
||||||
|
var writeErr error
|
||||||
writeSection := func(name string, entries map[string]string) {
|
writeSection := func(name string, entries map[string]string) {
|
||||||
fmt.Fprintf(w, "[%s]\n", name)
|
fmt.Fprintf(w, "[%s]\n", name)
|
||||||
keys := make([]string, 0, len(entries))
|
keys := make([]string, 0, len(entries))
|
||||||
@@ -160,7 +166,14 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
|
|||||||
}
|
}
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
fmt.Fprintf(w, "%s=%s\n", k, entries[k])
|
v := entries[k]
|
||||||
|
if !isSafeIniField(k) || !isSafeIniField(v) {
|
||||||
|
if writeErr == nil {
|
||||||
|
writeErr = fmt.Errorf("invalid mimeapps.list field %q=%q", k, v)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s=%s\n", k, v)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
}
|
}
|
||||||
@@ -177,6 +190,10 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
|
|||||||
writeSection(groupAdded, flatten(assoc.Added))
|
writeSection(groupAdded, flatten(assoc.Added))
|
||||||
writeSection(groupRemoved, flatten(assoc.Removed))
|
writeSection(groupRemoved, flatten(assoc.Removed))
|
||||||
|
|
||||||
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
|
||||||
if err := w.Flush(); err != nil {
|
if err := w.Flush(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,12 +112,37 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mango-specific tools (dwl-based, uses xwayland-satellite like niri)
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
|
dependencies = append(dependencies, a.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, a.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectDanksearch() deps.Dependency {
|
||||||
|
dep := a.BaseDistribution.detectDanksearch()
|
||||||
|
dep.CanToggle = true
|
||||||
|
if a.packageInstalled("dsearch-git") {
|
||||||
|
dep.Variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
return dep
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectDankCalendar() deps.Dependency {
|
||||||
|
dep := a.BaseDistribution.detectDankCalendar()
|
||||||
|
dep.CanToggle = true
|
||||||
|
if a.packageInstalled("dankcalendar-git") {
|
||||||
|
dep.Variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
return dep
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||||
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
|
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
}
|
}
|
||||||
@@ -127,7 +152,13 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
installed := a.packageInstalled("greetd-dms-greeter-git") || a.packageInstalled("greetd-dms-greeter-bin")
|
||||||
|
dep := a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", installed)
|
||||||
|
dep.CanToggle = true
|
||||||
|
if a.packageInstalled("greetd-dms-greeter-git") {
|
||||||
|
dep.Variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
return dep
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -172,6 +203,11 @@ func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
|||||||
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSonameProvides reports whether dep is a shared-library soname
|
||||||
|
func isSonameProvides(dep string) bool {
|
||||||
|
return strings.HasSuffix(dep, ".so") || strings.Contains(dep, ".so.")
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -181,7 +217,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||||
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
"dms-greeter": a.getDMSGreeterMapping(variants["dms-greeter"]),
|
||||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
@@ -189,6 +225,8 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"danksearch": a.getDanksearchMapping(variants["danksearch"]),
|
||||||
|
"dankcalendar": a.getDankCalendarMapping(variants["dankcalendar"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
@@ -199,6 +237,9 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = a.getNiriMapping(variants["niri"])
|
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
packages["mango"] = a.getMangoMapping(variants["mango"])
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
@@ -222,6 +263,13 @@ func (a *ArchDistribution) getNiriMapping(variant deps.PackageVariant) PackageMa
|
|||||||
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getMangoMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "mangowm-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "mangowm", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping {
|
func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if runtime.GOARCH == "arm64" {
|
if runtime.GOARCH == "arm64" {
|
||||||
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
||||||
@@ -233,6 +281,27 @@ func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) Packag
|
|||||||
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
|
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getDanksearchMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dsearch-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dsearch-bin", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getDankCalendarMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dankcalendar-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dankcalendar-bin", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getDMSGreeterMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "greetd-dms-greeter-bin", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
|
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if forceDMSGit || variant == deps.VariantGit {
|
if forceDMSGit || variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
@@ -724,7 +793,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[dep] = true
|
seen[dep] = true
|
||||||
if a.isInSystemRepo(dep) {
|
if isSonameProvides(dep) || a.isInSystemRepo(dep) {
|
||||||
systemPkgs = append(systemPkgs, dep)
|
systemPkgs = append(systemPkgs, dep)
|
||||||
} else {
|
} else {
|
||||||
aurPkgs = append(aurPkgs, dep)
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ func (b *BaseDistribution) detectDgop() deps.Dependency {
|
|||||||
return b.detectCommand("dgop", "Desktop portal management tool")
|
return b.detectCommand("dgop", "Desktop portal management tool")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectDanksearch() deps.Dependency {
|
||||||
|
return b.detectOptionalPackage("danksearch", "File indexing and search service", b.commandExists("dsearch") || b.commandExists("danksearch"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectDankCalendar() deps.Dependency {
|
||||||
|
return b.detectOptionalPackage("dankcalendar", "Calendar application", b.commandExists("dcal") || b.commandExists("dankcalendar"))
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||||
|
|
||||||
@@ -337,6 +345,36 @@ func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Depen
|
|||||||
Variant: variant,
|
Variant: variant,
|
||||||
CanToggle: true,
|
CanToggle: true,
|
||||||
}
|
}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
variant := deps.VariantStable
|
||||||
|
version := ""
|
||||||
|
|
||||||
|
if b.commandExists("mango") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
cmd := exec.Command("mango", "-v")
|
||||||
|
if output, err := cmd.Output(); err == nil {
|
||||||
|
outStr := string(output)
|
||||||
|
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||||
|
variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
if versionRegex := regexp.MustCompile(`(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||||
|
matches := versionRegex.FindStringSubmatch(outStr)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mango",
|
||||||
|
Status: status,
|
||||||
|
Version: version,
|
||||||
|
Description: "dwl-based dynamic tiling Wayland compositor",
|
||||||
|
Required: true,
|
||||||
|
Variant: variant,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return deps.Dependency{
|
return deps.Dependency{
|
||||||
Name: "unknown-wm",
|
Name: "unknown-wm",
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, d.detectMatugen())
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
dependencies = append(dependencies, d.detectDgop())
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
|
dependencies = append(dependencies, d.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, d.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,8 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
@@ -157,7 +161,60 @@ func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) P
|
|||||||
if forceQuickshellGit || variant == deps.VariantGit {
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
// Debian 13 ships stable quickshell in trixie-backports only.
|
||||||
|
if d.debianNeedsQuickshellBackports() {
|
||||||
|
return PackageMapping{Name: "quickshell/trixie-backports", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) debianNeedsQuickshellBackports() bool {
|
||||||
|
osInfo, err := GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return osInfo.VersionID == "13" || strings.EqualFold(osInfo.VersionCodename, "trixie")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) ensureQuickshellBackports(ctx context.Context, systemPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
needsBackports := false
|
||||||
|
for _, pkg := range systemPkgs {
|
||||||
|
if strings.Contains(pkg, "trixie-backports") {
|
||||||
|
needsBackports = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !needsBackports {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
policyOut, err := exec.CommandContext(ctx, "apt-cache", "policy").Output()
|
||||||
|
if err == nil && strings.Contains(string(policyOut), "trixie-backports") {
|
||||||
|
d.log("trixie-backports already configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
listFile := "/etc/apt/sources.list.d/trixie-backports.list"
|
||||||
|
repoLine := "deb http://deb.debian.org/debian trixie-backports main contrib non-free non-free-firmware"
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.30,
|
||||||
|
Step: "Enabling trixie-backports for quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
|
LogOutput: "Debian 13 ships quickshell in trixie-backports",
|
||||||
|
}
|
||||||
|
|
||||||
|
addCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
|
if err := d.runWithProgress(addCmd, progressChan, PhaseSystemPackages, 0.30, 0.31); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
return d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.31, 0.33)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
@@ -262,6 +319,10 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := d.ensureQuickshellBackports(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable trixie-backports for quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// System Packages
|
// System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupDsearchService enables the dsearch.service user unit. Enablement failures
|
||||||
|
// are returned for the caller to surface as a non-fatal warning.
|
||||||
|
func SetupDsearchService(ctx context.Context, logf func(string)) error {
|
||||||
|
if logf == nil {
|
||||||
|
logf = func(string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSystemctlUser(ctx, "daemon-reload"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSystemctlUser(ctx, "enable", "--now", "dsearch.service"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logf("Enabled dsearch.service")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSystemctlUser(ctx context.Context, args ...string) error {
|
||||||
|
cmd := exec.CommandContext(ctx, "systemctl", append([]string{"--user"}, args...)...)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("systemctl --user %v failed: %w: %s", args, err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -77,7 +77,11 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
// Common detections using base methods
|
// Common detections using base methods
|
||||||
dependencies = append(dependencies, f.detectGit())
|
dependencies = append(dependencies, f.detectGit())
|
||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
wmDep := f.detectWindowManager(wm)
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
wmDep.Description = "MangoWM (Wayland compositor) — the Terra repo will be enabled automatically to install it"
|
||||||
|
}
|
||||||
|
dependencies = append(dependencies, wmDep)
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
dependencies = append(dependencies, f.detectDMSGreeter())
|
dependencies = append(dependencies, f.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
@@ -93,8 +97,15 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mango-specific tools (dwl-based, uses xwayland-satellite like niri)
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
|
dependencies = append(dependencies, f.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, f.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -129,6 +140,8 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
@@ -139,6 +152,10 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
// mangowm resolves via Terra, enabled automatically by enableTerraRepo.
|
||||||
|
packages["mango"] = PackageMapping{Name: "mangowm", Repository: RepoTypeSystem}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
@@ -159,7 +176,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "lionheartp/Hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
@@ -297,6 +314,22 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2b: Enable Terra repo for MangoWM (not in Fedora's repos). Must run
|
||||||
|
// before the DNF phase so `mangowm` resolves.
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Enabling Terra repository for MangoWM...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: "Setting up the Terra repo (fyralabs) to provide mango",
|
||||||
|
}
|
||||||
|
if err := f.enableTerraRepo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable Terra repository: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: System Packages (DNF)
|
// Phase 3: System Packages (DNF)
|
||||||
if len(dnfPkgs) > 0 {
|
if len(dnfPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -423,6 +456,30 @@ func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enableTerraRepo registers the persistent Terra repo (via terra-release) so
|
||||||
|
// `mangowm` resolves in the DNF phase. $releasever is single-quoted so dnf, not
|
||||||
|
// the shell, expands it.
|
||||||
|
func (f *FedoraDistribution) enableTerraRepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// Skip if Terra is already configured
|
||||||
|
if exec.CommandContext(ctx, "sh", "-c",
|
||||||
|
"rpm -q terra-release >/dev/null 2>&1 || test -f /etc/yum.repos.d/terra.repo").Run() == nil {
|
||||||
|
f.log("Terra repository already configured, skipping enable")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.log("Enabling Terra repository (fyralabs) for mango...")
|
||||||
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
|
`dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release 2>&1`)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logError("failed to enable Terra repository", err)
|
||||||
|
f.log(fmt.Sprintf("Terra enable output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to enable Terra repository: %w", err)
|
||||||
|
}
|
||||||
|
f.log(fmt.Sprintf("Terra repository enabled: %s", string(output)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
|||||||
@@ -106,8 +106,14 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mango-specific tools (dwl-based, uses xwayland-satellite like niri)
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
|
dependencies = append(dependencies, g.detectDanksearch())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -166,6 +172,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
"dms (DankMaterialShell)": g.getDmsMapping(),
|
"dms (DankMaterialShell)": g.getDmsMapping(),
|
||||||
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
|
"danksearch": {Name: "gui-apps/danksearch", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
@@ -176,6 +183,10 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
packages["mango"] = g.getMangoMapping(variants["mango"])
|
||||||
|
packages["scenefx"] = PackageMapping{Name: "gui-libs/scenefx", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
@@ -197,6 +208,10 @@ func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMappin
|
|||||||
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getMangoMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
|
return PackageMapping{Name: "gui-wm/mangowm", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getPrerequisites() []string {
|
func (g *GentooDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"app-eselect/eselect-repository",
|
"app-eselect/eselect-repository",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
FamilyDebian DistroFamily = "debian"
|
FamilyDebian DistroFamily = "debian"
|
||||||
FamilyNix DistroFamily = "nix"
|
FamilyNix DistroFamily = "nix"
|
||||||
FamilyGentoo DistroFamily = "gentoo"
|
FamilyGentoo DistroFamily = "gentoo"
|
||||||
|
FamilyVoid DistroFamily = "void"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PackageManagerType defines the package manager a distro uses
|
// PackageManagerType defines the package manager a distro uses
|
||||||
@@ -29,6 +30,7 @@ const (
|
|||||||
PackageManagerZypper PackageManagerType = "zypper"
|
PackageManagerZypper PackageManagerType = "zypper"
|
||||||
PackageManagerNix PackageManagerType = "nix"
|
PackageManagerNix PackageManagerType = "nix"
|
||||||
PackageManagerPortage PackageManagerType = "portage"
|
PackageManagerPortage PackageManagerType = "portage"
|
||||||
|
PackageManagerXBPS PackageManagerType = "xbps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepositoryType defines the type of repository for a package
|
// RepositoryType defines the type of repository for a package
|
||||||
@@ -42,6 +44,7 @@ const (
|
|||||||
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
||||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||||
|
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
|
||||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
Progress: 0.90,
|
Progress: 0.90,
|
||||||
Step: "Cloning DankMaterialShell...",
|
Step: "Cloning DankMaterialShell...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
|
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(dmsPath)
|
configDir := filepath.Dir(dmsPath)
|
||||||
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules",
|
||||||
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||||
if err := cloneCmd.Run(); err != nil {
|
if err := cloneCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||||
@@ -553,6 +553,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.syncDMSSubmodules(ctx, dmsPath)
|
||||||
|
|
||||||
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||||
m.log("DankMaterialShell cloned successfully")
|
m.log("DankMaterialShell cloned successfully")
|
||||||
return nil
|
return nil
|
||||||
@@ -591,6 +593,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.syncDMSSubmodules(ctx, dmsPath)
|
||||||
|
|
||||||
m.log("DankMaterialShell updated successfully (git variant)")
|
m.log("DankMaterialShell updated successfully (git variant)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -609,10 +613,19 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.syncDMSSubmodules(ctx, dmsPath)
|
||||||
|
|
||||||
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
|
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) syncDMSSubmodules(ctx context.Context, dmsPath string) {
|
||||||
|
submoduleCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "submodule", "update", "--init", "--recursive")
|
||||||
|
if err := submoduleCmd.Run(); err != nil {
|
||||||
|
m.logError("Failed to update submodules", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing xwayland-satellite from source...")
|
m.log("Installing xwayland-satellite from source...")
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type minimalInstallGroup struct {
|
|||||||
|
|
||||||
func shouldPreferMinimalInstall(pkg string) bool {
|
func shouldPreferMinimalInstall(pkg string) bool {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "niri", "niri-git":
|
case "niri", "niri-git", "hyprland", "hyprland-git":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
|
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
|
dependencies = append(dependencies, o.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, o.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -129,6 +131,8 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
|
dependencies = append(dependencies, u.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, u.detectDankCalendar())
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -124,6 +126,8 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
|
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
|
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -0,0 +1,541 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
VoidDMSRepo = "https://void.danklinux.com/dms/current"
|
||||||
|
VoidDankLinuxRepo = "https://void.danklinux.com/danklinux/current"
|
||||||
|
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
|
||||||
|
|
||||||
|
voidRunitSvDir = "/etc/sv"
|
||||||
|
voidRunitServiceDir = "/var/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewVoidDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
|
||||||
|
return &VoidDistribution{
|
||||||
|
BaseDistribution: NewBaseDistribution(logChan),
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) GetID() string {
|
||||||
|
return v.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) GetColorHex() string {
|
||||||
|
return v.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) GetFamily() DistroFamily {
|
||||||
|
return v.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerXBPS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
dependencies = append(dependencies, v.detectDMS())
|
||||||
|
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
|
||||||
|
dependencies = append(dependencies, v.detectGit())
|
||||||
|
dependencies = append(dependencies, v.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, v.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, v.detectDMSGreeter())
|
||||||
|
dependencies = append(dependencies, v.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, v.detectAccountsService())
|
||||||
|
dependencies = append(dependencies, v.detectDBus())
|
||||||
|
dependencies = append(dependencies, v.detectElogind())
|
||||||
|
dependencies = append(dependencies, v.detectMesaDri())
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, v.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
|
||||||
|
dependencies = append(dependencies, v.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies, v.detectMatugen())
|
||||||
|
dependencies = append(dependencies, v.detectDgop())
|
||||||
|
dependencies = append(dependencies, v.detectDanksearch())
|
||||||
|
dependencies = append(dependencies, v.detectDankCalendar())
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectDMS() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
version := ""
|
||||||
|
variant := deps.VariantStable
|
||||||
|
|
||||||
|
if v.packageInstalled("dms-git") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
version = v.packageVersion("dms-git")
|
||||||
|
variant = deps.VariantGit
|
||||||
|
} else if v.packageInstalled("dms") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
version = v.packageVersion("dms")
|
||||||
|
} else if v.commandExists("dms") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "dms (DankMaterialShell)",
|
||||||
|
Status: status,
|
||||||
|
Version: version,
|
||||||
|
Description: "Desktop Management System package",
|
||||||
|
Required: true,
|
||||||
|
Variant: variant,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
|
||||||
|
dep := v.BaseDistribution.detectQuickshell()
|
||||||
|
dep.CanToggle = false
|
||||||
|
return dep
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectDBus() deps.Dependency {
|
||||||
|
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectElogind() deps.Dependency {
|
||||||
|
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectMesaDri() deps.Dependency {
|
||||||
|
return v.detectPackage("mesa-dri", "Mesa DRI/EGL drivers (GPU rendering; compositors find no outputs without it)", v.packageInstalled("mesa-dri"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) packageInstalled(pkg string) bool {
|
||||||
|
return exec.Command("xbps-query", pkg).Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) packageVersion(pkg string) string {
|
||||||
|
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
|
||||||
|
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
|
||||||
|
"mesa-dri": {Name: "mesa-dri", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
|
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
|
||||||
|
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
|
||||||
|
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||||
|
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||||
|
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
|
||||||
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
|
||||||
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantStable {
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Checking XBPS...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Checking for xbps-install",
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := exec.LookPath("xbps-install"); err != nil {
|
||||||
|
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
arch, err := v.xbpsArch(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
|
||||||
|
}
|
||||||
|
if arch != "x86_64" {
|
||||||
|
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
if len(xbpsPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling DMS XBPS repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: "Setting up custom XBPS repositories for DMS packages",
|
||||||
|
}
|
||||||
|
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
|
||||||
|
if len(allPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install XBPS packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
|
||||||
|
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable Void session services: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if !v.isRunitSystem() {
|
||||||
|
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// D-Bus activation alone starts elogind without its wrapper mounts; the runit service is required.
|
||||||
|
for _, service := range []string{"dbus", "elogind"} {
|
||||||
|
if !v.runitServiceInstalled(service) {
|
||||||
|
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.runitServiceEnabled(service) {
|
||||||
|
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.92,
|
||||||
|
Step: fmt.Sprintf("Enabling %s runit service...", service),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
|
||||||
|
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
|
||||||
|
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
|
||||||
|
}
|
||||||
|
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) isRunitSystem() bool {
|
||||||
|
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
|
||||||
|
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
|
||||||
|
return err == nil && fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
|
||||||
|
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
|
||||||
|
systemPkgs := []string{}
|
||||||
|
xbpsPkgs := []PackageMapping{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeXBPS:
|
||||||
|
xbpsPkgs = append(xbpsPkgs, pkgInfo)
|
||||||
|
case RepoTypeSystem:
|
||||||
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs, xbpsPkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
enabledRepoURLs := []string{}
|
||||||
|
|
||||||
|
for _, pkg := range xbpsPkgs {
|
||||||
|
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repoName := v.xbpsRepoName(pkg.RepoURL)
|
||||||
|
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
|
||||||
|
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
|
||||||
|
repoFileContent := repoLine + "\n"
|
||||||
|
|
||||||
|
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
|
||||||
|
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
|
||||||
|
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
|
||||||
|
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
|
||||||
|
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
|
||||||
|
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
|
||||||
|
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(enabledRepos) > 0 {
|
||||||
|
syncArgs := []string{"xbps-install", "-Sy", "-i"}
|
||||||
|
for _, repoURL := range enabledRepoURLs {
|
||||||
|
syncArgs = append(syncArgs, "--repository", repoURL)
|
||||||
|
}
|
||||||
|
syncCommand := strings.Join(syncArgs, " ")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Synchronizing XBPS repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
|
||||||
|
LogOutput: "Synchronizing XBPS repository indexes",
|
||||||
|
}
|
||||||
|
|
||||||
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
|
||||||
|
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
|
||||||
|
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
|
||||||
|
switch repoURL {
|
||||||
|
case VoidDMSRepo:
|
||||||
|
return "dms"
|
||||||
|
case VoidDankLinuxRepo:
|
||||||
|
return "danklinux"
|
||||||
|
case VoidHyprlandRepo:
|
||||||
|
return "hyprland"
|
||||||
|
default:
|
||||||
|
name := strings.TrimPrefix(repoURL, "https://")
|
||||||
|
name = strings.TrimPrefix(name, "http://")
|
||||||
|
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
|
||||||
|
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
args := append([]string{"xbps-install", "-Sy"}, packages...)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing XBPS packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var unique []string
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, pkg := range group {
|
||||||
|
if pkg == "" || seen[pkg] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pkg] = true
|
||||||
|
unique = append(unique, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unique
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package errdefs
|
package errdefs
|
||||||
|
|
||||||
type ErrorType int
|
import (
|
||||||
|
dankerrdefs "github.com/AvengeMedia/dankgo/errdefs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorType = dankerrdefs.ErrorType
|
||||||
|
|
||||||
|
type CustomError = dankerrdefs.CustomError
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ErrTypeNotLinux ErrorType = iota
|
ErrTypeNotLinux ErrorType = dankerrdefs.AppErrorBase + iota
|
||||||
ErrTypeInvalidArchitecture
|
ErrTypeInvalidArchitecture
|
||||||
ErrTypeUnsupportedDistribution
|
ErrTypeUnsupportedDistribution
|
||||||
ErrTypeUnsupportedVersion
|
ErrTypeUnsupportedVersion
|
||||||
@@ -22,20 +28,8 @@ const (
|
|||||||
ErrTypeGeneric
|
ErrTypeGeneric
|
||||||
)
|
)
|
||||||
|
|
||||||
type CustomError struct {
|
|
||||||
Type ErrorType
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *CustomError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomError(errType ErrorType, message string) error {
|
func NewCustomError(errType ErrorType, message string) error {
|
||||||
return &CustomError{
|
return dankerrdefs.NewCustomError(errType, message)
|
||||||
Type: errType,
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -47,6 +41,10 @@ const (
|
|||||||
ErrWifiDisabled = "wifi-disabled"
|
ErrWifiDisabled = "wifi-disabled"
|
||||||
ErrAlreadyConnected = "already-connected"
|
ErrAlreadyConnected = "already-connected"
|
||||||
ErrConnectionFailed = "connection-failed"
|
ErrConnectionFailed = "connection-failed"
|
||||||
|
|
||||||
|
ErrHotspotIPConfigFailed = "hotspot-ip-config-failed"
|
||||||
|
ErrHotspotSupplicantFailed = "hotspot-supplicant-failed"
|
||||||
|
ErrHotspotFailed = "hotspot-failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
"github.com/AvengeMedia/dankgo/dbusutil"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/dankgo/syncmap"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ func (c *GeoClueClient) startSignalPump() error {
|
|||||||
if err := c.dbusConn.AddMatchSignal(
|
if err := c.dbusConn.AddMatchSignal(
|
||||||
dbus.WithMatchObjectPath(c.clientPath),
|
dbus.WithMatchObjectPath(c.clientPath),
|
||||||
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
||||||
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
dbus.WithMatchMember("LocationUpdated"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
# AppArmor profile for dms-greeter
|
|
||||||
#
|
|
||||||
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
|
||||||
# Manual edits will be overwritten on next sync.
|
|
||||||
#
|
|
||||||
# Mode: complain (denials are logged, nothing is blocked)
|
|
||||||
# To switch to enforce after validating with `aa-logprof`:
|
|
||||||
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
|
||||||
#
|
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
|
||||||
#include <abstractions/base>
|
|
||||||
#include <abstractions/bash>
|
|
||||||
|
|
||||||
# The launcher script itself
|
|
||||||
/usr/bin/dms-greeter r,
|
|
||||||
|
|
||||||
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
|
||||||
/var/cache/dms-greeter/ rw,
|
|
||||||
/var/cache/dms-greeter/** rwlk,
|
|
||||||
|
|
||||||
# DMS config — packaged path
|
|
||||||
/usr/share/quickshell/dms-greeter/ r,
|
|
||||||
/usr/share/quickshell/dms-greeter/** r,
|
|
||||||
/usr/share/quickshell/ r,
|
|
||||||
/usr/share/quickshell/** r,
|
|
||||||
|
|
||||||
# DMS config — system and user overrides
|
|
||||||
/etc/dms/ r,
|
|
||||||
/etc/dms/** r,
|
|
||||||
/usr/share/dms/ r,
|
|
||||||
/usr/share/dms/** r,
|
|
||||||
/home/*/.config/quickshell/ r,
|
|
||||||
/home/*/.config/quickshell/** r,
|
|
||||||
/root/.config/quickshell/ r,
|
|
||||||
/root/.config/quickshell/** r,
|
|
||||||
|
|
||||||
# greetd / PAM — read-only for session setup
|
|
||||||
/etc/greetd/ r,
|
|
||||||
/etc/greetd/** r,
|
|
||||||
/etc/pam.d/ r,
|
|
||||||
/etc/pam.d/** r,
|
|
||||||
/usr/lib/pam.d/ r,
|
|
||||||
/usr/lib/pam.d/** r,
|
|
||||||
|
|
||||||
# Compositor binaries — run unconfined so each compositor uses its own profile
|
|
||||||
/usr/bin/niri Ux,
|
|
||||||
/usr/bin/hyprland Ux,
|
|
||||||
/usr/bin/Hyprland Ux,
|
|
||||||
/usr/bin/sway Ux,
|
|
||||||
/usr/bin/labwc Ux,
|
|
||||||
/usr/bin/scroll Ux,
|
|
||||||
/usr/bin/miracle-wm Ux,
|
|
||||||
/usr/bin/mango Ux,
|
|
||||||
|
|
||||||
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
|
||||||
/usr/bin/qs Ux,
|
|
||||||
/usr/bin/quickshell Ux,
|
|
||||||
|
|
||||||
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
|
||||||
/run/user/[0-9]*/ rw,
|
|
||||||
/run/user/[0-9]*/** rw,
|
|
||||||
|
|
||||||
# DRM / GPU devices (required for Wayland compositor startup)
|
|
||||||
/dev/dri/ r,
|
|
||||||
/dev/dri/* rw,
|
|
||||||
/dev/udmabuf rw,
|
|
||||||
|
|
||||||
# Input devices
|
|
||||||
/dev/input/ r,
|
|
||||||
/dev/input/* r,
|
|
||||||
|
|
||||||
# Systemd journal / logging
|
|
||||||
/run/systemd/journal/socket rw,
|
|
||||||
/dev/log rw,
|
|
||||||
|
|
||||||
# Shell helper binaries invoked by the launcher script
|
|
||||||
/usr/bin/env ix,
|
|
||||||
/usr/bin/mkdir ix,
|
|
||||||
/usr/bin/cat ix,
|
|
||||||
/usr/bin/grep ix,
|
|
||||||
/usr/bin/dirname ix,
|
|
||||||
/usr/bin/basename ix,
|
|
||||||
/usr/bin/command ix,
|
|
||||||
/bin/env ix,
|
|
||||||
/bin/mkdir ix,
|
|
||||||
|
|
||||||
# Signal management (compositor lifecycle)
|
|
||||||
signal (send, receive) set=("term", "int", "hup", "kill"),
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user