mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-03 12:08:31 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aed731efb0 | |||
| cf0632c077 | |||
| e92da4a15f | |||
| 8abdff3220 | |||
| 584d57a8de | |||
| afb5e59c29 |
@@ -2,16 +2,15 @@
|
|||||||
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 five plugin types: widget (bar + Control Center), daemon (background
|
Quickshell. Supports four plugin types: widget (bar + Control Center), daemon (background
|
||||||
service), launcher (search + actions), desktop (draggable desktop widgets), and composite
|
service), launcher (search + actions), and desktop (draggable desktop widgets). Covers
|
||||||
(multi-surface). Covers manifest creation, QML component development, startup checks,
|
manifest creation, QML component development, settings UI, data persistence, theme
|
||||||
settings UI, data persistence, theme integration, PopoutService usage, IPC runtime
|
integration, PopoutService usage, and external command execution. Use when the user wants
|
||||||
discovery, and external command execution. Use when the user wants to create, modify,
|
to create, modify, or debug a DMS plugin, or asks about the DMS plugin API.
|
||||||
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.1"
|
version: "1.0"
|
||||||
domain: qml-desktop-development
|
domain: qml-desktop-development
|
||||||
framework: DankMaterialShell
|
framework: DankMaterialShell
|
||||||
languages: qml, javascript
|
languages: qml, javascript
|
||||||
@@ -38,15 +37,14 @@ 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/
|
||||||
|
|
||||||
**Five plugin types:**
|
**Four 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
|
||||||
|
|
||||||
@@ -60,9 +58,6 @@ 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
|
||||||
|
|
||||||
@@ -83,7 +78,7 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**With settings, startup check, and permissions:**
|
**With settings and permissions:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -97,31 +92,7 @@ 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"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -129,11 +100,9 @@ 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`)
|
||||||
- Provide either `component` (single-surface) or `components` (multi-surface), not both
|
- `component` must start with `./` and end with `.qml`
|
||||||
- `component` / component paths must start with `./` and end with `.qml`
|
- `type: "launcher"` requires a `trigger` field
|
||||||
- `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
|
||||||
|
|
||||||
@@ -280,69 +249,7 @@ 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.
|
||||||
|
|
||||||
### Composite
|
## Step 4: Add Settings (Optional)
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -386,7 +293,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 6: Use Data Persistence
|
## Step 5: Use Data Persistence
|
||||||
|
|
||||||
Three tiers of persistence:
|
Three tiers of persistence:
|
||||||
|
|
||||||
@@ -395,7 +302,6 @@ 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) { ... } }`
|
||||||
@@ -403,7 +309,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 7: Theme Integration
|
## Step 6: 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.
|
||||||
|
|
||||||
@@ -418,7 +324,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 8: Add Popout Content (Widgets Only)
|
## Step 7: 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:
|
||||||
|
|
||||||
@@ -454,7 +360,7 @@ PluginComponent {
|
|||||||
|
|
||||||
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
||||||
|
|
||||||
## Step 9: Control Center Integration (Widgets Only)
|
## Step 8: Control Center Integration (Widgets Only)
|
||||||
|
|
||||||
Add your widget to the Control Center grid:
|
Add your widget to the Control Center grid:
|
||||||
|
|
||||||
@@ -483,7 +389,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 10: External Commands and Clipboard
|
## Step 9: External Commands and Clipboard
|
||||||
|
|
||||||
**Run commands and capture output:**
|
**Run commands and capture output:**
|
||||||
|
|
||||||
@@ -514,33 +420,18 @@ 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 11: Validate and Test
|
## Step 10: 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
|
||||||
|
|
||||||
@@ -554,9 +445,6 @@ Plugin IDs are validated against `^[a-zA-Z0-9_\-:]{1,64}$`.
|
|||||||
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,7 +11,8 @@
|
|||||||
"version",
|
"version",
|
||||||
"author",
|
"author",
|
||||||
"type",
|
"type",
|
||||||
"capabilities"
|
"capabilities",
|
||||||
|
"component"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -41,8 +42,8 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
|
"description": "Plugin type",
|
||||||
"enum": ["widget", "daemon", "launcher", "desktop", "composite"]
|
"enum": ["widget", "daemon", "launcher", "desktop"]
|
||||||
},
|
},
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -54,37 +55,9 @@
|
|||||||
},
|
},
|
||||||
"component": {
|
"component": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Relative path to main QML component file. Required unless 'components' is provided.",
|
"description": "Relative path to main QML component file",
|
||||||
"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)"
|
||||||
@@ -98,26 +71,14 @@
|
|||||||
"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": "Deprecated alias for 'dependencies'.",
|
"description": "Array of required system tools/dependencies",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -148,29 +109,6 @@
|
|||||||
"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,16 +166,6 @@ 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,22 +9,15 @@
|
|||||||
| `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`, `composite` |
|
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop` |
|
||||||
| `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
|
||||||
|
|
||||||
@@ -32,10 +25,8 @@ One of `component` or `components` is required (not both):
|
|||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `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+$` |
|
||||||
| `dependencies` | array | System tool dependencies (e.g., `["curl", "jq"]`). Registry metadata. |
|
| `requires` | array | System tool dependencies (e.g., `["curl", "jq"]`) |
|
||||||
| `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) |
|
||||||
|
|
||||||
@@ -62,65 +53,6 @@ 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
|
||||||
@@ -135,9 +67,8 @@ Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`. Provide any subse
|
|||||||
"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",
|
||||||
"dependencies": ["curl", "jq"],
|
"requires": ["curl", "jq"],
|
||||||
"permissions": ["settings_read", "settings_write", "process", "network"]
|
"permissions": ["settings_read", "settings_write", "process", "network"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -235,12 +235,10 @@ Conditionally show/hide the bar pill:
|
|||||||
```qml
|
```qml
|
||||||
PluginComponent {
|
PluginComponent {
|
||||||
visibilityCommand: "pgrep -x myapp"
|
visibilityCommand: "pgrep -x myapp"
|
||||||
visibilityInterval: 5 // seconds between checks
|
visibilityInterval: 5000 // check every 5 seconds
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**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:
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
## 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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: ["*"]
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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,17 +7,14 @@ 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@v3
|
uses: actions/create-github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
@@ -31,54 +28,4 @@ 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: |
|
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
|
||||||
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, "stable-*"]
|
branches: [master, main]
|
||||||
paths:
|
paths:
|
||||||
- "core/**"
|
- "core/**"
|
||||||
- ".github/workflows/go-ci.yml"
|
- ".github/workflows/go-ci.yml"
|
||||||
@@ -27,8 +27,6 @@ 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
|
||||||
@@ -50,39 +48,8 @@ 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 withshell' ./cmd/dms
|
run: go build -v -tags distro_binary ./cmd/dms
|
||||||
|
|
||||||
- name: Build dankinstall
|
- name: Build dankinstall
|
||||||
run: go build -v ./cmd/dankinstall
|
run: go build -v ./cmd/dankinstall
|
||||||
|
|
||||||
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, "stable-*"]
|
branches: [master, main]
|
||||||
paths:
|
paths:
|
||||||
- "flake.*"
|
- "flake.*"
|
||||||
- "distro/nix/**"
|
- "distro/nix/**"
|
||||||
@@ -33,6 +33,8 @@ 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
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
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,15 +3,13 @@ name: Pre-commit Checks
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main, "stable-*"]
|
branches: [master, main]
|
||||||
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
|
||||||
@@ -28,4 +26,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@v2
|
uses: j178/prek-action@v1
|
||||||
|
|||||||
+50
-130
@@ -7,16 +7,10 @@ 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 }}
|
||||||
@@ -27,11 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
arch: [amd64, arm64]
|
||||||
- { goos: linux, arch: amd64 }
|
|
||||||
- { goos: linux, arch: arm64 }
|
|
||||||
- { goos: freebsd, arch: amd64 }
|
|
||||||
- { goos: freebsd, arch: arm64 }
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -39,8 +29,6 @@ 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
|
||||||
@@ -48,7 +36,6 @@ 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
|
||||||
@@ -56,7 +43,6 @@ 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:"
|
||||||
@@ -65,43 +51,38 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
if: matrix.goos == 'linux'
|
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
- name: Build dankinstall (${{ env.ASSET }})
|
- name: Build dankinstall (${{ matrix.arch }})
|
||||||
if: matrix.goos == 'linux'
|
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: linux
|
||||||
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-${{ env.ASSET }}
|
-o ../../dankinstall-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dankinstall-${{ env.ASSET }}
|
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||||
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
|
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
|
|
||||||
- name: Sync embedded shell
|
- name: Build dms (${{ matrix.arch }})
|
||||||
run: make sync-shell
|
|
||||||
|
|
||||||
- name: Build dms (${{ env.ASSET }})
|
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: linux
|
||||||
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 withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-${{ env.ASSET }}
|
-o ../../dms-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-${{ env.ASSET }}
|
gzip -9 -k dms-${{ matrix.arch }}
|
||||||
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
|
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
||||||
|
|
||||||
- name: Generate shell completions
|
- name: Generate shell completions
|
||||||
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
chmod +x dms-amd64
|
chmod +x dms-amd64
|
||||||
@@ -109,58 +90,46 @@ 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 (${{ env.ASSET }})
|
- name: Build dms-distropkg (${{ matrix.arch }})
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: linux
|
||||||
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 withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-distropkg-${{ env.ASSET }}
|
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-distropkg-${{ env.ASSET }}
|
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||||
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
|
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
|
|
||||||
- name: Upload artifacts (${{ env.ASSET }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ env.ASSET }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ env.ASSET }}.gz
|
core/dankinstall-${{ matrix.arch }}.gz
|
||||||
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
core/dms-${{ env.ASSET }}.gz
|
core/dms-${{ matrix.arch }}.gz
|
||||||
core/dms-${{ env.ASSET }}.gz.sha256
|
core/dms-${{ matrix.arch }}.gz.sha256
|
||||||
core/dms-distropkg-${{ env.ASSET }}.gz
|
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
core/dms-distropkg-${{ matrix.arch }}.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.goos == 'linux' && matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ env.ASSET }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ env.ASSET }}.gz
|
core/dankinstall-${{ matrix.arch }}.gz
|
||||||
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
core/dms-${{ env.ASSET }}.gz
|
core/dms-${{ matrix.arch }}.gz
|
||||||
core/dms-${{ env.ASSET }}.gz.sha256
|
core/dms-${{ matrix.arch }}.gz.sha256
|
||||||
core/dms-distropkg-${{ env.ASSET }}.gz
|
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
core/completion.bash
|
core/completion.bash
|
||||||
core/completion.fish
|
core/completion.fish
|
||||||
core/completion.zsh
|
core/completion.zsh
|
||||||
@@ -216,13 +185,11 @@ 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
|
||||||
@@ -238,23 +205,13 @@ 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 "")
|
||||||
CHANGELOG=""
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
if [ -n "$PREVIOUS_TAG" ]; then
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
||||||
# PR-based notes with author credits; falls back to raw git log below
|
else
|
||||||
CHANGELOG=$(python3 scripts/release-notes.py "${PREVIOUS_TAG}..${TAG}" --format github --bare 2>/dev/null || true)
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
||||||
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'
|
||||||
@@ -269,23 +226,16 @@ 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
|
||||||
@@ -333,22 +283,6 @@ 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/
|
||||||
@@ -365,8 +299,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 os/architecture
|
# Create full packages for each architecture
|
||||||
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
|
for arch in amd64 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
|
||||||
@@ -409,16 +343,18 @@ jobs:
|
|||||||
|
|
||||||
## Installation Steps
|
## Installation Steps
|
||||||
|
|
||||||
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/`
|
1. **Install quickshell assets:**
|
||||||
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a
|
```bash
|
||||||
modified copy instead of the embedded UI.
|
mkdir -p ~/.config/quickshell
|
||||||
|
cp -r dms ~/.config/quickshell/
|
||||||
|
```
|
||||||
|
|
||||||
1. **Install the DMS CLI binaries:**
|
2. **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
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install shell completions (optional):**
|
3. **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
|
||||||
@@ -430,7 +366,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
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the shell:**
|
4. **Start the shell:**
|
||||||
```bash
|
```bash
|
||||||
dms run
|
dms run
|
||||||
```
|
```
|
||||||
@@ -481,19 +417,3 @@ 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,12 +4,14 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: 'Package to build'
|
description: 'Package to build (dms, dms-greeter, or both)'
|
||||||
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
|
||||||
@@ -29,7 +31,11 @@ jobs:
|
|||||||
id: set-packages
|
id: set-packages
|
||||||
run: |
|
run: |
|
||||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
if [ "$PACKAGE_INPUT" = "both" ]; then
|
||||||
|
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
|
||||||
@@ -157,6 +163,8 @@ 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,6 +9,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
|
- dms-greeter
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -23,7 +24,6 @@ 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,7 +35,6 @@ 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
|
||||||
@@ -74,12 +73,27 @@ 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 stable package
|
# Run from tag with no package specified - update both stable packages
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms dms-greeter" >> $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
|
||||||
@@ -105,7 +119,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 the stable package and build list of those needing updates
|
# Check each 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")
|
||||||
@@ -113,6 +127,10 @@ 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
|
||||||
@@ -121,7 +139,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 "✓ All packages up to date"
|
echo "✓ Both packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -145,6 +163,18 @@ 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
|
||||||
@@ -168,7 +198,6 @@ 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
|
||||||
@@ -189,9 +218,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 using the API
|
# Determine version for dms stable and dms-greeter using the API
|
||||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
# 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 }}" == "all" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
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
|
||||||
@@ -257,7 +286,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)
|
- name: Update stable version (dms + dms-greeter)
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
@@ -293,12 +322,25 @@ 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 stable)
|
# Update download_url paths (for dms, dms-greeter 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
|
||||||
@@ -359,7 +401,7 @@ jobs:
|
|||||||
UPLOADED_PACKAGES=()
|
UPLOADED_PACKAGES=()
|
||||||
SKIPPED_PACKAGES=()
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
# PACKAGES can be a space-separated list (from the "all" check)
|
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "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 ""
|
||||||
@@ -462,6 +504,9 @@ 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,6 +9,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
|
- dms-greeter
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -23,7 +24,6 @@ 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,7 +35,6 @@ 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: |
|
||||||
@@ -94,7 +93,6 @@ 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
|
||||||
@@ -134,6 +132,7 @@ 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
|
||||||
|
|
||||||
@@ -148,3 +147,4 @@ 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"
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
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@v3
|
uses: actions/create-github-app-token@v1
|
||||||
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,24 +110,8 @@ 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
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
[submodule "dank-qml-common"]
|
|
||||||
path = dank-qml-common
|
|
||||||
url = https://github.com/AvengeMedia/dank-qml-common.git
|
|
||||||
branch = master
|
|
||||||
@@ -20,30 +20,6 @@ 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,13 +1,5 @@
|
|||||||
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,20 +6,6 @@ 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
|
||||||
@@ -42,47 +28,6 @@ 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.
|
||||||
@@ -159,8 +104,6 @@ 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=$(DESTDIR)$(PREFIX)/bin
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
DATA_DIR=$(DESTDIR)$(PREFIX)/share
|
DATA_DIR=$(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 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
|
.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
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -27,12 +27,6 @@ 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
|
||||||
@@ -41,12 +35,6 @@ 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)..."
|
||||||
@@ -55,9 +43,8 @@ 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 -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
@cp -r $(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"
|
||||||
|
|
||||||
@@ -72,17 +59,13 @@ 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|$(PREFIX)/bin/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
@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..."
|
||||||
@@ -91,12 +74,10 @@ install-icon:
|
|||||||
@echo "Icon installed"
|
@echo "Icon installed"
|
||||||
|
|
||||||
install-desktop:
|
install-desktop:
|
||||||
@echo "Installing desktop entries..."
|
@echo "Installing desktop entry..."
|
||||||
@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 entries installed"
|
@echo "Desktop entry 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 ""
|
||||||
@@ -135,12 +116,10 @@ uninstall-icon:
|
|||||||
@echo "Icon removed"
|
@echo "Icon removed"
|
||||||
|
|
||||||
uninstall-desktop:
|
uninstall-desktop:
|
||||||
@echo "Removing desktop entries..."
|
@echo "Removing desktop entry..."
|
||||||
@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 entries removed"
|
@echo "Desktop entry 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 a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
||||||
|
|
||||||
**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,17 +132,6 @@ 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:
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[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;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[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
|
|
||||||
+77
-80
@@ -1,85 +1,82 @@
|
|||||||
dir: internal/mocks/{{.InterfaceDirRelative}}
|
with-expecter: true
|
||||||
structname: Mock{{.InterfaceName}}
|
dir: "internal/mocks/{{.InterfaceDirRelative}}"
|
||||||
pkgname: '{{.SrcPackageName}}'
|
mockname: "Mock{{.InterfaceName}}"
|
||||||
filename: 'mock_{{.InterfaceName}}.go'
|
outpkg: "{{.PackageName}}"
|
||||||
template: testify
|
|
||||||
template-data:
|
|
||||||
unroll-variadic: true
|
|
||||||
packages:
|
packages:
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/geolocation
|
|
||||||
pkgname: mocks_geolocation
|
|
||||||
interfaces:
|
|
||||||
Client: {}
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/cups
|
|
||||||
pkgname: mocks_cups
|
|
||||||
interfaces:
|
|
||||||
CUPSClientInterface: {}
|
|
||||||
PkHelper:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/cups_pkhelper
|
|
||||||
pkgname: mocks_cups_pkhelper
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/evdev
|
|
||||||
pkgname: mocks_evdev
|
|
||||||
interfaces:
|
|
||||||
EvdevDevice: {}
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/network
|
|
||||||
pkgname: mocks_network
|
|
||||||
interfaces:
|
|
||||||
Backend: {}
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/wlcontext
|
|
||||||
pkgname: mocks_wlcontext
|
|
||||||
interfaces:
|
|
||||||
WaylandContext: {}
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
|
||||||
config:
|
|
||||||
dir: internal/mocks/utils
|
|
||||||
pkgname: mocks_utils
|
|
||||||
interfaces:
|
|
||||||
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:
|
github.com/Wifx/gonetworkmanager/v2:
|
||||||
interfaces:
|
interfaces:
|
||||||
AccessPoint: {}
|
NetworkManager:
|
||||||
ActiveConnection: {}
|
Device:
|
||||||
Connection: {}
|
DeviceWireless:
|
||||||
Device: {}
|
AccessPoint:
|
||||||
DeviceWireless: {}
|
Connection:
|
||||||
IP4Config: {}
|
Settings:
|
||||||
NetworkManager: {}
|
ActiveConnection:
|
||||||
Settings: {}
|
IP4Config:
|
||||||
github.com/godbus/dbus/v5:
|
|
||||||
interfaces:
|
|
||||||
BusObject: {}
|
|
||||||
net:
|
net:
|
||||||
interfaces:
|
interfaces:
|
||||||
Conn: {}
|
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:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/geolocation"
|
||||||
|
outpkg: mocks_geolocation
|
||||||
|
interfaces:
|
||||||
|
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:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/cups"
|
||||||
|
outpkg: mocks_cups
|
||||||
|
interfaces:
|
||||||
|
CUPSClientInterface:
|
||||||
|
PkHelper:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/cups_pkhelper"
|
||||||
|
outpkg: mocks_cups_pkhelper
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/evdev"
|
||||||
|
outpkg: mocks_evdev
|
||||||
|
interfaces:
|
||||||
|
EvdevDevice:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/version"
|
||||||
|
outpkg: mocks_version
|
||||||
|
interfaces:
|
||||||
|
VersionFetcher:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/wlcontext"
|
||||||
|
outpkg: mocks_wlcontext
|
||||||
|
interfaces:
|
||||||
|
WaylandContext:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/wlclient"
|
||||||
|
outpkg: mocks_wlclient
|
||||||
|
interfaces:
|
||||||
|
WaylandDisplay:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/utils"
|
||||||
|
outpkg: mocks_utils
|
||||||
|
interfaces:
|
||||||
|
AppChecker:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.12.2
|
rev: v2.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
+26
-50
@@ -3,8 +3,6 @@ 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
|
||||||
|
|
||||||
@@ -23,46 +21,17 @@ 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
|
|
||||||
|
|
||||||
ifeq ($(ARCH),all)
|
.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
|
||||||
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: sync-shell
|
build:
|
||||||
@echo "Building $(BINARY_NAME)..."
|
@echo "Building $(BINARY_NAME)..."
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
CGO_ENABLED=0 $(GO) build $(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:
|
||||||
@@ -71,18 +40,26 @@ 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 (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
|
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
||||||
dist: sync-shell
|
dist:
|
||||||
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
|
ifeq ($(ARCH),all)
|
||||||
|
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@for os in $(DIST_OSES); do \
|
@echo "Building for linux/amd64..."
|
||||||
for arch in $(DIST_ARCHS); do \
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
||||||
echo "Building for $$os/$$arch..."; \
|
@echo "Building for linux/arm64..."
|
||||||
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; \
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
||||||
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
|
@echo "Distribution builds complete:"
|
||||||
done; \
|
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
||||||
done
|
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
||||||
@echo "Distribution builds complete"
|
else
|
||||||
|
@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
|
||||||
|
|
||||||
@@ -122,7 +99,7 @@ uninstall-dankinstall:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
|
@rm -rf $(BUILD_DIR)
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -164,11 +141,10 @@ 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) with the embedded UI"
|
@echo " build - Build the main binary (dms)"
|
||||||
@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/freebsd amd64/arm64 (no update/greeter)"
|
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
||||||
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
|
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
||||||
@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)"
|
||||||
|
|||||||
+6
-9
@@ -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, Gentoo, and Void. 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, and Gentoo. 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` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
|
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
||||||
|
|
||||||
### Color Picker
|
### Color Picker
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ make test # Run tests
|
|||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update features
|
make dist # Build without update/greeter 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, Void (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (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), testing, and sid. Stable quickshell from Debian (trixie-backports on 13). Other companions via OBS.
|
Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
|
||||||
|
|
||||||
**openSUSE**
|
**openSUSE**
|
||||||
Most packages available in standard repos. Minimal building required.
|
Most packages available in standard repos. Minimal building required.
|
||||||
@@ -214,7 +214,4 @@ 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,8 +23,6 @@ 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{
|
||||||
@@ -44,15 +42,13 @@ configure passwordless sudo for your user.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
|
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri or hyprland (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() {
|
||||||
@@ -78,8 +74,6 @@ 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 {
|
||||||
@@ -101,7 +95,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, hyprland, or mango)")
|
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
|
||||||
}
|
}
|
||||||
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)")
|
||||||
@@ -115,8 +109,6 @@ 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,11 +1,7 @@
|
|||||||
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"
|
||||||
@@ -16,7 +12,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 PAM/authentication setup for the DMS lock screen",
|
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
||||||
}
|
}
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
var authSyncCmd = &cobra.Command{
|
||||||
@@ -39,142 +35,9 @@ 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 {
|
||||||
@@ -212,31 +75,3 @@ 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,15 +63,6 @@ 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",
|
||||||
@@ -225,10 +216,8 @@ 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, clipSendPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
@@ -325,12 +314,6 @@ 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,12 +19,7 @@ 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{
|
||||||
@@ -34,9 +29,6 @@ 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)
|
||||||
|
|||||||
+86
-271
@@ -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,16 +19,68 @@ 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",
|
Use: "ipc [target] [function] [args...]",
|
||||||
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) {
|
||||||
@@ -36,21 +88,11 @@ Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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{
|
||||||
@@ -129,22 +171,10 @@ 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, or all plugins",
|
Short: "Update a plugin by ID",
|
||||||
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
|
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: cobra.ExactArgs(1),
|
||||||
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
|
||||||
@@ -152,26 +182,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -207,41 +217,29 @@ 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 {
|
||||||
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
|
paths := []string{
|
||||||
return ver
|
"/usr/share/quickshell/dms/VERSION",
|
||||||
|
"/usr/local/share/quickshell/dms/VERSION",
|
||||||
|
"/etc/xdg/quickshell/dms/VERSION",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range shellVersionPaths {
|
for _, path := range paths {
|
||||||
content, err := os.ReadFile(path)
|
if content, err := os.ReadFile(path); err == nil {
|
||||||
if err != nil {
|
ver := strings.TrimSpace(string(content))
|
||||||
continue
|
ver = strings.TrimPrefix(ver, "v")
|
||||||
}
|
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
|
||||||
if ver := parseBaseVersion(string(content)); ver != "" {
|
if matches := re.FindStringSubmatch(ver); matches != nil {
|
||||||
return ver
|
return matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
@@ -269,13 +267,6 @@ 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)
|
||||||
@@ -299,26 +290,6 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,11 +330,7 @@ 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 {
|
||||||
hasUpdateStr := ""
|
fmt.Printf(" %s\n", plugin.Name)
|
||||||
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)
|
||||||
@@ -543,164 +510,13 @@ 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 {
|
||||||
commands := shellApp.Commands()
|
return []*cobra.Command{
|
||||||
return append(commands, []*cobra.Command{
|
|
||||||
versionCmd,
|
versionCmd,
|
||||||
|
runCmd,
|
||||||
|
restartCmd,
|
||||||
|
restartDetachedCmd,
|
||||||
|
killCmd,
|
||||||
ipcCmd,
|
ipcCmd,
|
||||||
debugSrvCmd,
|
debugSrvCmd,
|
||||||
pluginsCmd,
|
pluginsCmd,
|
||||||
@@ -711,7 +527,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
colorCmd,
|
colorCmd,
|
||||||
qrCmd,
|
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
notifyCmd,
|
notifyCmd,
|
||||||
@@ -727,5 +542,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
trashCmd,
|
trashCmd,
|
||||||
systemCmd,
|
systemCmd,
|
||||||
switchUserCmd,
|
switchUserCmd,
|
||||||
}...)
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,9 @@ 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",
|
||||||
"input.kdl",
|
|
||||||
"cursor.conf",
|
"cursor.conf",
|
||||||
"layout.conf",
|
|
||||||
"outputs.conf",
|
"outputs.conf",
|
||||||
"binds.conf",
|
"binds.conf",
|
||||||
}, cobra.ShellCompDirectiveNoFileComp
|
}, cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -57,10 +54,8 @@ 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) {
|
||||||
@@ -75,7 +70,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", "mango":
|
case "mangowc", "dwl", "mango":
|
||||||
result, err = checkMangoWCInclude(filename)
|
result, err = checkMangoWCInclude(filename)
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unknown compositor: %s", compositor)
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
@@ -90,7 +85,10 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||||
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
targetPath := filepath.Join(configDir, "dms", filename)
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
result := IncludeResult{}
|
result := IncludeResult{}
|
||||||
@@ -108,8 +106,6 @@ 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
|
||||||
@@ -119,10 +115,6 @@ 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
|
||||||
@@ -191,7 +183,10 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkNiriInclude(filename string) (IncludeResult, error) {
|
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||||
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
targetPath := filepath.Join(configDir, "dms", filename)
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
result := IncludeResult{}
|
result := IncludeResult{}
|
||||||
@@ -267,7 +262,10 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||||
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
|
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
targetPath := filepath.Join(configDir, "dms", filename)
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
result := IncludeResult{}
|
result := IncludeResult{}
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ const (
|
|||||||
catConfigFiles
|
catConfigFiles
|
||||||
catServices
|
catServices
|
||||||
catEnvironment
|
catEnvironment
|
||||||
catFonts
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c category) String() string {
|
func (c category) String() string {
|
||||||
@@ -148,8 +147,6 @@ 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"
|
||||||
}
|
}
|
||||||
@@ -216,7 +213,6 @@ func runDoctor(cmd *cobra.Command, args []string) {
|
|||||||
checkConfigurationFiles(),
|
checkConfigurationFiles(),
|
||||||
checkSystemdServices(),
|
checkSystemdServices(),
|
||||||
checkEnvironmentVars(),
|
checkEnvironmentVars(),
|
||||||
checkFonts(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -320,7 +316,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,29 +330,6 @@ 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")
|
||||||
@@ -401,12 +373,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDMSShellVersion() (version, path string) {
|
func getDMSShellVersion() (version, path string) {
|
||||||
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||||
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
|
versionFile := filepath.Join(configPath, "VERSION")
|
||||||
if data, err := os.ReadFile(versionFile); err == nil {
|
if data, err := os.ReadFile(versionFile); err == nil {
|
||||||
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
|
return strings.TrimSpace(string(data)), configPath
|
||||||
}
|
}
|
||||||
return "installed", shellApp.ConfigPath()
|
return "installed", configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
||||||
@@ -450,8 +422,8 @@ func checkDMSInstallation() []checkResult {
|
|||||||
var results []checkResult
|
var results []checkResult
|
||||||
|
|
||||||
dmsPath := ""
|
dmsPath := ""
|
||||||
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||||
dmsPath = shellApp.ConfigPath()
|
dmsPath = configPath
|
||||||
} else if path, err := config.LocateDMSConfig(); err == nil {
|
} else if path, err := config.LocateDMSConfig(); err == nil {
|
||||||
dmsPath = path
|
dmsPath = path
|
||||||
}
|
}
|
||||||
@@ -581,8 +553,6 @@ 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") != "":
|
||||||
@@ -833,8 +803,6 @@ 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 ""
|
||||||
}
|
}
|
||||||
@@ -912,7 +880,6 @@ 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},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,12 +947,9 @@ 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 == "inactive":
|
case dmsState.active == "failed" || 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"})
|
||||||
@@ -1168,100 +1132,3 @@ 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: shellApp.ResolveConfig,
|
PreRunE: findConfig,
|
||||||
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...")
|
||||||
shellApp.Restart()
|
restartShell()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateArchLinux() error {
|
func updateArchLinux() error {
|
||||||
@@ -250,10 +250,6 @@ 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()
|
||||||
@@ -295,10 +291,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -328,21 +320,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.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)
|
||||||
@@ -361,7 +342,7 @@ func offerReclone(dmsPath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Cloning fresh copy...")
|
fmt.Println("Cloning fresh copy...")
|
||||||
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
cloneCmd := exec.Command("git", "clone", "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 {
|
||||||
|
|||||||
+1783
-29
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
|||||||
|
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,17 +38,10 @@ 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")
|
||||||
@@ -69,8 +62,6 @@ 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 {
|
||||||
@@ -209,17 +200,3 @@ 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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
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,9 +1,19 @@
|
|||||||
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",
|
||||||
@@ -11,5 +21,54 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
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)")
|
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,12 +2,9 @@ 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"
|
||||||
@@ -28,19 +25,8 @@ var (
|
|||||||
ssNoConfirm bool
|
ssNoConfirm bool
|
||||||
ssReset bool
|
ssReset bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
ssJSON bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type screenshotMetadata struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
Width int `json:"width,omitempty"`
|
|
||||||
Height int `json:"height,omitempty"`
|
|
||||||
Scale float64 `json:"scale,omitempty"`
|
|
||||||
Mime string `json:"mime,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var screenshotCmd = &cobra.Command{
|
var screenshotCmd = &cobra.Command{
|
||||||
Use: "screenshot",
|
Use: "screenshot",
|
||||||
Short: "Capture screenshots",
|
Short: "Capture screenshots",
|
||||||
@@ -51,9 +37,8 @@ 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/Mango/niri)
|
window - Capture the focused window (Hyprland/DWL)
|
||||||
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)
|
||||||
@@ -72,10 +57,7 @@ Examples:
|
|||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
dms screenshot --no-confirm # Region capture on mouse release
|
dms screenshot --no-confirm # Region capture on mouse release
|
||||||
dms screenshot --cursor=on # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
dms screenshot --json # Print capture metadata as JSON
|
|
||||||
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
|
|
||||||
dms screenshot scroll --interval 250`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssRegionCmd = &cobra.Command{
|
var ssRegionCmd = &cobra.Command{
|
||||||
@@ -113,33 +95,10 @@ 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, Mango, and niri.`,
|
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
|
||||||
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",
|
||||||
@@ -167,12 +126,8 @@ 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)
|
||||||
@@ -224,76 +179,15 @@ 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) {
|
||||||
if ssJSON && config.Stdout {
|
sc := screenshot.New(config)
|
||||||
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
|
result, err := sc.Run()
|
||||||
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 {
|
if err != nil {
|
||||||
exitScreenshotError("", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
if ssJSON {
|
|
||||||
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +199,8 @@ func runScreenshot(config screenshot.Config) {
|
|||||||
|
|
||||||
if config.Stdout {
|
if config.Stdout {
|
||||||
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
exitScreenshotError(" writing to stdout", err)
|
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -325,37 +220,22 @@ func runScreenshot(config screenshot.Config) {
|
|||||||
|
|
||||||
filePath = filepath.Join(outputDir, filename)
|
filePath = filepath.Join(outputDir, filename)
|
||||||
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
||||||
exitScreenshotError(" writing file", err)
|
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
||||||
}
|
os.Exit(1)
|
||||||
if !ssJSON {
|
|
||||||
fmt.Println(filePath)
|
|
||||||
}
|
}
|
||||||
|
fmt.Println(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Clipboard {
|
if config.Clipboard {
|
||||||
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
exitScreenshotError(" copying to clipboard", err)
|
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if !ssJSON && !config.SaveFile {
|
if !config.SaveFile {
|
||||||
fmt.Println("Copied to clipboard")
|
fmt.Println("Copied to clipboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ssJSON {
|
|
||||||
scale := result.Scale
|
|
||||||
if scale <= 0 {
|
|
||||||
scale = 1.0
|
|
||||||
}
|
|
||||||
writeScreenshotJSON(screenshotMetadata{
|
|
||||||
Status: "success",
|
|
||||||
Path: filePath,
|
|
||||||
Width: result.Buffer.Width,
|
|
||||||
Height: result.Buffer.Height,
|
|
||||||
Scale: scale,
|
|
||||||
Mime: formatMime(config.Format),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Notify {
|
if config.Notify {
|
||||||
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||||
screenshot.SendNotification(screenshot.NotifyResult{
|
screenshot.SendNotification(screenshot.NotifyResult{
|
||||||
@@ -466,12 +346,6 @@ 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)
|
||||||
|
|||||||
+38
-104
@@ -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/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"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,76 +100,56 @@ var setupWindowrulesCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dmsConfigSpec struct {
|
type dmsConfigSpec struct {
|
||||||
niriFile string
|
niriFile string
|
||||||
hyprFile string
|
hyprFile string
|
||||||
mangoFile string
|
niriContent func(terminal string) string
|
||||||
niriContent func(terminal string) string
|
hyprContent 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",
|
||||||
mangoFile: "layout.conf",
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||||
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",
|
||||||
mangoFile: "colors.conf",
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||||
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 },
|
||||||
},
|
},
|
||||||
"input": {
|
|
||||||
niriFile: "input.kdl",
|
|
||||||
niriContent: func(_ string) string { return config.NiriInputConfig },
|
|
||||||
},
|
|
||||||
"outputs": {
|
"outputs": {
|
||||||
niriFile: "outputs.kdl",
|
niriFile: "outputs.kdl",
|
||||||
hyprFile: "outputs.lua",
|
hyprFile: "outputs.lua",
|
||||||
mangoFile: "outputs.conf",
|
niriContent: func(_ string) string { return "" },
|
||||||
niriContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
||||||
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",
|
||||||
mangoFile: "cursor.conf",
|
niriContent: func(_ string) string { return "" },
|
||||||
niriContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
||||||
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",
|
||||||
mangoFile: "windowrules.conf",
|
niriContent: func(_ string) string { return "" },
|
||||||
niriContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
||||||
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
|
||||||
mangoContent: func(_ string) string { return "" },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,34 +188,20 @@ func detectTerminal() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func detectCompositorForSetup() (string, error) {
|
func detectCompositorForSetup() (string, error) {
|
||||||
var compositors []string
|
compositors := greeter.DetectCompositors()
|
||||||
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, Hyprland, or mango required)")
|
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
||||||
case 1:
|
case 1:
|
||||||
return strings.ToLower(compositors[0]), nil
|
return strings.ToLower(compositors[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Multiple compositors detected:")
|
selected, err := greeter.PromptCompositorChoice(compositors)
|
||||||
for i, compositor := range compositors {
|
if err != nil {
|
||||||
fmt.Printf("%d) %s\n", i+1, compositor)
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Printf("\nChoice (1-%d): ", len(compositors))
|
return strings.ToLower(selected), nil
|
||||||
|
|
||||||
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 {
|
||||||
@@ -258,9 +224,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -272,11 +235,9 @@ func runSetupDmsConfig(name string) error {
|
|||||||
var dmsDir string
|
var dmsDir string
|
||||||
switch compositor {
|
switch compositor {
|
||||||
case "niri":
|
case "niri":
|
||||||
dmsDir = filepath.Join(utils.XDGConfigHome(), "niri", "dms")
|
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
dmsDir = filepath.Join(utils.XDGConfigHome(), "hypr", "dms")
|
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "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 {
|
||||||
@@ -312,17 +273,7 @@ func runSetup() error {
|
|||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := true
|
useSystemd := promptSystemd()
|
||||||
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.")
|
||||||
@@ -393,15 +344,6 @@ 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() {
|
||||||
@@ -437,11 +379,10 @@ 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) Mango")
|
fmt.Println("3) None")
|
||||||
fmt.Println("4) None")
|
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
fmt.Print("\nChoice (1-4): ")
|
fmt.Print("\nChoice (1-3): ")
|
||||||
fmt.Scanln(&response)
|
fmt.Scanln(&response)
|
||||||
response = strings.TrimSpace(response)
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
@@ -450,8 +391,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -508,11 +447,6 @@ 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,7 +47,6 @@ 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
|
||||||
)
|
)
|
||||||
@@ -59,7 +58,6 @@ 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)
|
||||||
@@ -194,7 +192,6 @@ func runSystemUpdateApply() {
|
|||||||
Targets: pkgs,
|
Targets: pkgs,
|
||||||
IncludeFlatpak: !sysUpdateNoFlatpak,
|
IncludeFlatpak: !sysUpdateNoFlatpak,
|
||||||
IncludeAUR: !sysUpdateNoAUR,
|
IncludeAUR: !sysUpdateNoAUR,
|
||||||
Ignored: sysUpdateIgnore,
|
|
||||||
DryRun: sysUpdateDry,
|
DryRun: sysUpdateDry,
|
||||||
UseSudo: true,
|
UseSudo: true,
|
||||||
}
|
}
|
||||||
@@ -237,19 +234,12 @@ 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 && len(sysUpdateIgnore) == 0 {
|
if !sysUpdateNoAUR {
|
||||||
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 sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
|
if p.Repo == sysupdate.RepoAUR {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ignored[p.Name] {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, p)
|
out = append(out, p)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,7 +26,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", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -41,7 +40,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", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -55,7 +54,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", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -69,7 +68,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", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -83,7 +82,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", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -121,9 +120,6 @@ 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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,14 +139,17 @@ 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, niri, or mango")
|
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
||||||
}
|
}
|
||||||
|
|
||||||
var result WindowRulesListResult
|
var result WindowRulesListResult
|
||||||
|
|
||||||
switch compositor {
|
switch compositor {
|
||||||
case "niri":
|
case "niri":
|
||||||
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to expand niri config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,7 +182,10 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
|||||||
result.DMSStatus = parseResult.DMSStatus
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -215,38 +217,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -345,14 +315,17 @@ 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 := filepath.Join(utils.XDGConfigHome(), "niri")
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return providers.NewNiriWritableProvider(configDir)
|
return providers.NewNiriWritableProvider(configDir)
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
configDir, err := utils.ExpandPath("$HOME/.config/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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
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,7 +12,15 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
|
runCmd.Flags().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,7 +12,15 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
|
runCmd.Flags().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,10 +12,6 @@ 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()
|
||||||
|
|
||||||
@@ -26,7 +22,6 @@ 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)
|
||||||
@@ -66,7 +61,6 @@ 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)
|
||||||
@@ -101,10 +95,6 @@ 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")
|
||||||
|
|||||||
+597
-160
@@ -2,28 +2,554 @@ 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
|
||||||
|
|
||||||
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
// getProcessExitCode returns the exit code from a ProcessState.
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
// 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()
|
defer cancel()
|
||||||
cmd := exec.CommandContext(ctx, "qs", "ipc", "--help")
|
|
||||||
cmd.WaitDelay = 500 * time.Millisecond
|
socketPath := server.GetSocketPath()
|
||||||
out, err := cmd.Output()
|
|
||||||
|
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 {
|
||||||
|
out, err := exec.Command("qs", "ipc", "--help").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -59,34 +585,13 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
|||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildQsIPCBaseArgs() ([]string, error) {
|
|
||||||
cmdArgs := []string{"ipc"}
|
|
||||||
switch pid, ok := shellApp.SessionPID(); {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
return cmdArgs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getShellIPCCompletions(args []string, _ string) []string {
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
baseArgs, err := buildQsIPCBaseArgs()
|
cmdArgs := []string{"ipc"}
|
||||||
if err != nil {
|
if qsHasAnyDisplay() {
|
||||||
log.Debugf("Error building IPC args for completions: %v", err)
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
cmdArgs := append(baseArgs, "show")
|
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
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 {
|
||||||
@@ -102,7 +607,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", "list")
|
targetNames = append(targetNames, "call")
|
||||||
for k := range targets {
|
for k := range targets {
|
||||||
targetNames = append(targetNames, k)
|
targetNames = append(targetNames, k)
|
||||||
}
|
}
|
||||||
@@ -128,6 +633,43 @@ 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()
|
||||||
@@ -138,11 +680,23 @@ func runShellIPCCommand(args []string) {
|
|||||||
args = append([]string{"call"}, args...)
|
args = append([]string{"call"}, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseArgs, err := buildQsIPCBaseArgs()
|
cmdArgs := []string{"ipc"}
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error finding config: %v", err)
|
switch pid, ok := getFirstDMSPID(); {
|
||||||
|
case ok:
|
||||||
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
|
default:
|
||||||
|
if err := findConfig(nil, nil); err != nil {
|
||||||
|
log.Fatalf("Error finding config: %v", err)
|
||||||
|
}
|
||||||
|
// ! TODO - remove check when QS 0.3 is released
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
}
|
}
|
||||||
cmdArgs := append(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
|
||||||
@@ -154,20 +708,19 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printIPCHelp() {
|
func printIPCHelp() {
|
||||||
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
|
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
baseArgs, err := buildQsIPCBaseArgs()
|
cmdArgs := []string{"ipc"}
|
||||||
if err != nil {
|
if qsHasAnyDisplay() {
|
||||||
printIPCHelpFailure(err)
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
cmdArgs := append(baseArgs, "show")
|
cmdArgs = append(cmdArgs, "-p", configPath, "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 {
|
||||||
printIPCHelpFailure(err)
|
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,119 +748,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
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,7 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,3 +28,62 @@ 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,13 +1,14 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.27.0
|
github.com/alecthomas/chroma/v2 v2.24.1
|
||||||
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
|
||||||
@@ -17,12 +18,13 @@ 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.4
|
github.com/yuin/goldmark v1.8.2
|
||||||
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.5.0
|
go.etcd.io/bbolt v1.4.3
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||||
golang.org/x/image v0.44.0
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||||
tailscale.com v1.100.0
|
golang.org/x/image v0.39.0
|
||||||
|
tailscale.com v1.96.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -30,28 +32,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.4 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/coder/websocket v1.8.15 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/dlclark/regexp2/v2 v2.5.1 // indirect
|
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // 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-alpha.1 // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
|
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 // 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.4.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/mdlayher/netlink v1.11.2 // indirect
|
github.com/mdlayher/netlink v1.11.1 // indirect
|
||||||
github.com/mdlayher/socket v0.6.1 // indirect
|
github.com/mdlayher/socket v0.6.0 // 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
|
||||||
@@ -61,15 +63,13 @@ 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.54.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/net v0.57.0 // indirect
|
golang.org/x/sync v0.20.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-20260730184236-239485829b0b
|
|
||||||
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.4
|
github.com/go-git/go-git/v6 v6.0.0-alpha.2
|
||||||
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.23
|
github.com/mattn/go-isatty v0.0.22
|
||||||
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.24 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0
|
||||||
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.47.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.org/x/text v0.40.0
|
golang.org/x/text v0.36.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+56
-56
@@ -1,7 +1,5 @@
|
|||||||
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-20260730184236-239485829b0b h1:UwX1H4BkzazL7ips9ljnHzBXGttYARcIi5Njj9aqIt4=
|
|
||||||
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
|
|
||||||
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=
|
||||||
@@ -13,8 +11,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.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
||||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
||||||
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=
|
||||||
@@ -50,23 +48,25 @@ 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.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.14/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-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
|
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
|
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
|
||||||
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/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
|
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||||
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
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-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
|
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/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E=
|
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
|
||||||
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 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
|
||||||
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-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
|
||||||
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 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
|
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
|
||||||
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 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4/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.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
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.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
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.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
|
||||||
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
|
||||||
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
|
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
||||||
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
|
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
||||||
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-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
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.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.8.2/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.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
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.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
|
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
||||||
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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"
|
||||||
@@ -16,37 +17,6 @@ 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() {
|
||||||
@@ -74,7 +44,7 @@ func MaybeServeAndExit() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil {
|
if err := serveClipboard(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)
|
||||||
}
|
}
|
||||||
@@ -85,33 +55,22 @@ 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 serveOffers(ExpandOffers(data, mimeType), pasteOnce)
|
return serveClipboard(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 {
|
|
||||||
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 {
|
if foreground {
|
||||||
return serveOffers(offers, pasteOnce)
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyMultiFork(offers, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
@@ -173,70 +132,39 @@ 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)
|
||||||
|
|
||||||
if src, ok := data.(*os.File); ok {
|
switch src := data.(type) {
|
||||||
|
case *os.File:
|
||||||
cmd.Stdin = src
|
cmd.Stdin = src
|
||||||
return waitReady(cmd)
|
return waitReady(cmd)
|
||||||
}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
default:
|
||||||
if err != nil {
|
stdin, err := cmd.StdinPipe()
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
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
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
|
}
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
@@ -266,25 +194,57 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveOffers owns the Wayland selection until cancelled (or first paste when
|
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
// pasteOnce is set), answering every offered mime type with its data.
|
display, err := wlclient.Connect("")
|
||||||
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 err
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer display.Destroy()
|
||||||
|
|
||||||
dataControlMgr, err := s.requireDataControl()
|
ctx := display.Context()
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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(s.seat)
|
device, err := dataControlMgr.GetDataDevice(seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get data device: %w", err)
|
return fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -295,12 +255,25 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("create data source: %w", err)
|
return fmt.Errorf("create data source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
offerData := make(map[string][]byte, len(offers))
|
if err := source.Offer(mimeType); err != nil {
|
||||||
for _, offer := range offers {
|
return fmt.Errorf("offer mime type: %w", err)
|
||||||
if err := source.Offer(offer.MimeType); err != nil {
|
}
|
||||||
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
||||||
|
if err := source.Offer("text/plain"); err != nil {
|
||||||
|
return fmt.Errorf("offer text/plain: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
||||||
|
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("UTF8_STRING"); err != nil {
|
||||||
|
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("STRING"); err != nil {
|
||||||
|
return fmt.Errorf("offer STRING: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("TEXT"); err != nil {
|
||||||
|
return fmt.Errorf("offer TEXT: %w", err)
|
||||||
}
|
}
|
||||||
offerData[offer.MimeType] = offer.Data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
@@ -310,11 +283,7 @@ func serveOffers(offers []Offer, 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:
|
||||||
@@ -329,7 +298,7 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("set selection: %w", err)
|
return fmt.Errorf("set selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
signalReady()
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -341,26 +310,70 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err := s.ctx.Dispatch(); err != nil {
|
if err := 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) {
|
||||||
s, err := connectSession()
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer display.Destroy()
|
||||||
|
|
||||||
dataControlMgr, err := s.requireDataControl()
|
ctx := display.Context()
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
defer registry.Destroy()
|
||||||
|
|
||||||
|
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||||
|
var seat *wlclient.Seat
|
||||||
|
var bindErr error
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case "ext_data_control_manager_v1":
|
||||||
|
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
case "wl_seat":
|
||||||
|
if seat != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seat = wlclient.NewSeat(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(s.seat)
|
if dataControlMgr == nil {
|
||||||
|
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
}
|
||||||
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
|
if seat == nil {
|
||||||
|
return nil, "", fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := dataControlMgr.GetDataDevice(seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
return nil, "", fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -386,14 +399,15 @@ func Paste() ([]byte, string, error) {
|
|||||||
gotSelection = true
|
gotSelection = true
|
||||||
})
|
})
|
||||||
|
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
if !gotSelection || selectionOffer == nil {
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
return nil, "", fmt.Errorf("no clipboard data")
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
|
mimeTypes := 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")
|
||||||
}
|
}
|
||||||
@@ -410,7 +424,7 @@ func Paste() ([]byte, string, error) {
|
|||||||
}
|
}
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
data, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -456,3 +470,161 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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,10 +61,6 @@ 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
|
||||||
|
|
||||||
@@ -77,10 +73,6 @@ 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"),
|
||||||
},
|
},
|
||||||
@@ -134,14 +126,6 @@ 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 {
|
||||||
@@ -264,7 +248,6 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
{"layout.kdl", NiriLayoutConfig},
|
{"layout.kdl", NiriLayoutConfig},
|
||||||
{"alttab.kdl", NiriAlttabConfig},
|
{"alttab.kdl", NiriAlttabConfig},
|
||||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"input.kdl", NiriInputConfig},
|
|
||||||
{"outputs.kdl", ""},
|
{"outputs.kdl", ""},
|
||||||
{"cursor.kdl", ""},
|
{"cursor.kdl", ""},
|
||||||
{"windowrules.kdl", ""},
|
{"windowrules.kdl", ""},
|
||||||
@@ -286,96 +269,6 @@ 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
|
||||||
|
|
||||||
@@ -707,10 +600,6 @@ 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,17 +20,13 @@ 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")
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||||
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))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -38,25 +34,20 @@ 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")
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||||
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"))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +404,6 @@ 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))
|
||||||
@@ -433,12 +423,10 @@ 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"))
|
||||||
@@ -497,7 +485,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.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })`)
|
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { 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)
|
||||||
@@ -520,18 +508,6 @@ 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 in-app Ghostty toast notifications
|
# Disable annoying copied to clipboard
|
||||||
app-notifications = false
|
app-notifications = no-clipboard-copy,no-config-reload
|
||||||
|
|
||||||
# Key bindings for common actions
|
# Key bindings for common actions
|
||||||
#keybind = ctrl+c=copy_to_clipboard
|
#keybind = ctrl+c=copy_to_clipboard
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ 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 dash toggle wallpaper"))
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash 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
|
||||||
@@ -39,7 +38,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.close())
|
hl.bind("SUPER + Q", hl.dsp.window.kill())
|
||||||
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" }))
|
||||||
@@ -113,9 +112,6 @@ 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" }))
|
||||||
@@ -144,7 +140,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.window.fullscreen({ mode = "maximized", action = "set" }))
|
hl.bind("SUPER + CTRL + F", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive exact 100% 100%]]))
|
||||||
|
|
||||||
-- === 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" })
|
||||||
@@ -154,10 +150,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.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })
|
hl.bind("SUPER + minus", 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 + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 10% 0]]), { repeating = true })
|
||||||
hl.bind("SUPER + SHIFT + minus", hl.dsp.window.resize({ x = 0, y = -100, relative = true }), { repeating = true })
|
hl.bind("SUPER + SHIFT + minus", 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 })
|
hl.bind("SUPER + SHIFT + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 10%]]), { repeating = true })
|
||||||
|
|
||||||
-- === Screenshots ===
|
-- === Screenshots ===
|
||||||
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
|
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
|
||||||
|
|||||||
@@ -11,14 +11,8 @@ end)
|
|||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
input = {
|
input = {
|
||||||
-- empty inherits XKB_DEFAULT_LAYOUT (libxkbcommon), falls back to "us"
|
kb_layout = "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,
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Auto-generated by DMS. Regenerated from DMS settings (dms/layout.conf).
|
|
||||||
|
|
||||||
border_radius=12
|
|
||||||
gappih=5
|
|
||||||
gappiv=5
|
|
||||||
gappoh=5
|
|
||||||
gappov=5
|
|
||||||
borderpx=2
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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+O repeat=false { toggle-overview; }
|
Mod+D 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" "dash" "toggle" "wallpaper";
|
spawn "dms" "ipc" "call" "dankdash" "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 {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DMS_RUN_GREETER "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
gestures {
|
||||||
|
hot-corners {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
background-color "#000000"
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
input {
|
|
||||||
mouse {
|
|
||||||
accel-speed 0.0
|
|
||||||
}
|
|
||||||
touchpad {
|
|
||||||
tap
|
|
||||||
accel-speed 0.0
|
|
||||||
natural-scroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,10 +20,3 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,31 @@ input {
|
|||||||
numlock
|
numlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next sections include libinput settings.
|
||||||
|
// Omitting settings disables them, or leaves them at their default values.
|
||||||
|
// All commented-out settings here are examples, not defaults.
|
||||||
|
touchpad {
|
||||||
|
// off
|
||||||
|
tap
|
||||||
|
// dwt
|
||||||
|
// dwtp
|
||||||
|
// drag false
|
||||||
|
// drag-lock
|
||||||
|
natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "two-finger"
|
||||||
|
// disabled-on-external-mouse
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse {
|
||||||
|
// off
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "no-scroll"
|
||||||
|
}
|
||||||
|
|
||||||
trackpoint {
|
trackpoint {
|
||||||
// off
|
// off
|
||||||
// natural-scroll
|
// natural-scroll
|
||||||
@@ -52,7 +77,6 @@ input {
|
|||||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||||
// focus-follows-mouse max-scroll-amount="0%"
|
// focus-follows-mouse max-scroll-amount="0%"
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can configure outputs by their name, which you can find
|
// You can configure outputs by their name, which you can find
|
||||||
// by running `niri msg outputs` while inside a niri instance.
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
// The built-in laptop monitor is usually called "eDP-1".
|
// The built-in laptop monitor is usually called "eDP-1".
|
||||||
@@ -241,10 +265,9 @@ recent-windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Include dms files
|
// Include dms files
|
||||||
include optional=true "dms/colors.kdl"
|
include "dms/colors.kdl"
|
||||||
include optional=true "dms/layout.kdl"
|
include "dms/layout.kdl"
|
||||||
include optional=true "dms/alttab.kdl"
|
include "dms/alttab.kdl"
|
||||||
include optional=true "dms/binds.kdl"
|
include "dms/binds.kdl"
|
||||||
include optional=true "dms/outputs.kdl"
|
include "dms/outputs.kdl"
|
||||||
include optional=true "dms/cursor.kdl"
|
include "dms/cursor.kdl"
|
||||||
include optional=true "dms/input.kdl"
|
|
||||||
|
|||||||
@@ -138,9 +138,11 @@ func readExistingHyprlandConfig(configDir string) (data string, sourcePath strin
|
|||||||
return "", "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanupStrayHyprlandConfFile moves stray ~/.config/hypr/hyprland.conf and
|
// CleanupStrayHyprlandConfFile moves a stray ~/.config/hypr/hyprland.conf
|
||||||
// top-level ~/.config/hypr/dms/*.conf files into .dms-backups/<timestamp>/ only
|
// into .dms-backups/<timestamp>/ only when hyprland.lua also exists, which
|
||||||
// when hyprland.lua also exists as the live config.
|
// proves Lua is the live config and the .conf is an autogen Hyprland 0.55
|
||||||
|
// 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
|
||||||
@@ -154,44 +156,19 @@ 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 info, err := os.Lstat(confPath); err == nil && !info.IsDir() {
|
if _, err := os.Stat(confPath); err != nil {
|
||||||
strayPaths = append(strayPaths, confPath)
|
|
||||||
}
|
|
||||||
dmsConfPaths, err := filepath.Glob(filepath.Join(configDir, "dms", "*.conf"))
|
|
||||||
if err == nil {
|
|
||||||
for _, p := range dmsConfPaths {
|
|
||||||
if info, err := os.Lstat(p); err == nil && !info.IsDir() {
|
|
||||||
strayPaths = append(strayPaths, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(strayPaths) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := time.Now().Format("2006-01-02_15-04-05")
|
ts := time.Now().Format("2006-01-02_15-04-05")
|
||||||
moved := 0
|
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, "hyprland.conf")
|
||||||
for _, src := range strayPaths {
|
if err := moveHyprlandConfigFile(confPath, dst); err != nil {
|
||||||
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 {
|
if logFn != nil {
|
||||||
logFn("Moved stray Hyprland conf file to %s", dst)
|
logFn("Could not move stray hyprland.conf: %v", err)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if moved > 0 && logFn != nil {
|
if logFn != nil {
|
||||||
logFn("Moved %d stray Hyprland conf file(s) out of the active Lua config tree", moved)
|
logFn("Moved stray hyprland.conf to %s", dst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -17,5 +17,5 @@ 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-input.kdl
|
//go:embed embedded/niri-greeter.kdl
|
||||||
var NiriInputConfig string
|
var NiriGreeterConfig string
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ type WindowManager int
|
|||||||
const (
|
const (
|
||||||
WindowManagerHyprland WindowManager = iota
|
WindowManagerHyprland WindowManager = iota
|
||||||
WindowManagerNiri
|
WindowManagerNiri
|
||||||
WindowManagerMango
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Terminal int
|
type Terminal int
|
||||||
|
|||||||
@@ -133,11 +133,6 @@ 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()
|
||||||
@@ -157,7 +152,6 @@ 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))
|
||||||
@@ -166,14 +160,7 @@ func writeUserMimeapps(update func(*MimeAssociations)) error {
|
|||||||
}
|
}
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
v := entries[k]
|
fmt.Fprintf(w, "%s=%s\n", k, 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)
|
||||||
}
|
}
|
||||||
@@ -190,10 +177,6 @@ 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,37 +112,12 @@ 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"))
|
||||||
}
|
}
|
||||||
@@ -152,13 +127,7 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
installed := a.packageInstalled("greetd-dms-greeter-git") || a.packageInstalled("greetd-dms-greeter-bin")
|
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||||
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 {
|
||||||
@@ -203,11 +172,6 @@ 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))
|
||||||
}
|
}
|
||||||
@@ -217,7 +181,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": a.getDMSGreeterMapping(variants["dms-greeter"]),
|
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
||||||
"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},
|
||||||
@@ -225,8 +189,6 @@ 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 {
|
||||||
@@ -237,9 +199,6 @@ 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
|
||||||
@@ -263,13 +222,6 @@ 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}
|
||||||
@@ -281,27 +233,6 @@ 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}
|
||||||
@@ -793,7 +724,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[dep] = true
|
seen[dep] = true
|
||||||
if isSonameProvides(dep) || a.isInSystemRepo(dep) {
|
if a.isInSystemRepo(dep) {
|
||||||
systemPkgs = append(systemPkgs, dep)
|
systemPkgs = append(systemPkgs, dep)
|
||||||
} else {
|
} else {
|
||||||
aurPkgs = append(aurPkgs, dep)
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
|||||||
@@ -107,14 +107,6 @@ 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")
|
||||||
|
|
||||||
@@ -345,36 +337,6 @@ 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,8 +71,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -137,8 +135,6 @@ 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 {
|
||||||
@@ -161,60 +157,7 @@ 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"}
|
||||||
}
|
}
|
||||||
// Debian 13 ships stable quickshell in trixie-backports only.
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
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 {
|
||||||
@@ -319,10 +262,6 @@ 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{
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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,11 +77,7 @@ 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())
|
||||||
wmDep := f.detectWindowManager(wm)
|
dependencies = append(dependencies, 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())
|
||||||
@@ -97,15 +93,8 @@ 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
|
||||||
}
|
}
|
||||||
@@ -140,8 +129,6 @@ 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 {
|
||||||
@@ -152,10 +139,6 @@ 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
|
||||||
@@ -176,7 +159,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: "lionheartp/Hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
@@ -314,22 +297,6 @@ 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{
|
||||||
@@ -456,30 +423,6 @@ 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,14 +106,8 @@ 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
|
||||||
}
|
}
|
||||||
@@ -172,7 +166,6 @@ 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 {
|
||||||
@@ -183,10 +176,6 @@ 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
|
||||||
@@ -208,10 +197,6 @@ 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,7 +17,6 @@ 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
|
||||||
@@ -30,7 +29,6 @@ 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
|
||||||
@@ -44,7 +42,6 @@ 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 --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
|
CommandInfo: "git clone 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", "--recurse-submodules",
|
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||||
"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,8 +553,6 @@ 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
|
||||||
@@ -593,8 +591,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -613,19 +609,10 @@ 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", "hyprland", "hyprland-git":
|
case "niri", "niri-git":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -91,8 +91,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -131,8 +129,6 @@ 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,8 +80,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -126,8 +124,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,541 +0,0 @@
|
|||||||
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,15 +1,9 @@
|
|||||||
package errdefs
|
package errdefs
|
||||||
|
|
||||||
import (
|
type ErrorType int
|
||||||
dankerrdefs "github.com/AvengeMedia/dankgo/errdefs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrorType = dankerrdefs.ErrorType
|
|
||||||
|
|
||||||
type CustomError = dankerrdefs.CustomError
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ErrTypeNotLinux ErrorType = dankerrdefs.AppErrorBase + iota
|
ErrTypeNotLinux ErrorType = iota
|
||||||
ErrTypeInvalidArchitecture
|
ErrTypeInvalidArchitecture
|
||||||
ErrTypeUnsupportedDistribution
|
ErrTypeUnsupportedDistribution
|
||||||
ErrTypeUnsupportedVersion
|
ErrTypeUnsupportedVersion
|
||||||
@@ -28,8 +22,20 @@ 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 dankerrdefs.NewCustomError(errType, message)
|
return &CustomError{
|
||||||
|
Type: errType,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -41,10 +47,6 @@ 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/dankgo/dbusutil"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
"github.com/AvengeMedia/dankgo/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/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.WithMatchMember("LocationUpdated"),
|
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 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"),
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user