mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 15:29:43 -04:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db4de55338 | ||
|
|
37ecbbbbde | ||
|
|
d6a6d2a438 | ||
|
|
bf1c6eec74 | ||
|
|
0ddae80584 | ||
|
|
5c96c03bfa | ||
|
|
dfe36e47d8 | ||
|
|
63e1b75e57 | ||
|
|
29efdd8598 | ||
|
|
34d03cf11b | ||
|
|
c339389d44 | ||
|
|
af5f6eb656 | ||
|
|
a6d28e2553 | ||
|
|
6213267908 | ||
|
|
d084114149 | ||
|
|
f6d99eca0d | ||
|
|
722eb3289e | ||
|
|
b7f2bdcb2d | ||
|
|
11c20db6e6 | ||
|
|
8a4e3f8bb1 | ||
|
|
bc8fe97c13 | ||
|
|
47262155aa |
@@ -1,104 +0,0 @@
|
|||||||
# Agent Skills
|
|
||||||
|
|
||||||
This directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.
|
|
||||||
|
|
||||||
Each skill is a directory with a `SKILL.md` entrypoint, optional reference docs, scripts, and templates. Agents load skills progressively: metadata at startup, full instructions on activation, and supporting files on demand.
|
|
||||||
|
|
||||||
## Available Skills
|
|
||||||
|
|
||||||
| Skill | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for DankMaterialShell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
The `.agents/skills/` directory at the project root is the standard location defined by the agentskills.io spec. Many agents discover skills from this path automatically. Some agents use their own directory conventions and need a symlink or copy.
|
|
||||||
|
|
||||||
### Claude Code
|
|
||||||
|
|
||||||
Claude Code discovers skills from `.claude/skills/` (project-level) or `~/.claude/skills/` (personal). To make skills from `.agents/skills/` available, symlink them into the Claude Code skills directory:
|
|
||||||
|
|
||||||
**Project-level** (this repo only):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .claude/skills
|
|
||||||
ln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Personal** (all your projects):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ln -s /path/to/DankMaterialShell/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
After linking, the skill appears in Claude Code's `/` menu as `/dms-plugin-dev`, and Claude loads it automatically when you ask about DMS plugin development.
|
|
||||||
|
|
||||||
See the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.
|
|
||||||
|
|
||||||
### Cursor
|
|
||||||
|
|
||||||
Cursor discovers skills from `.cursor/skills/` in the project root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .cursor/skills
|
|
||||||
ln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Cursor skills docs](https://cursor.com/docs/context/skills) for details.
|
|
||||||
|
|
||||||
### VS Code (Copilot)
|
|
||||||
|
|
||||||
VS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .github/skills
|
|
||||||
ln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
See [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.
|
|
||||||
|
|
||||||
### Gemini CLI
|
|
||||||
|
|
||||||
Gemini CLI discovers skills from `.gemini/skills/` in the project root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .gemini/skills
|
|
||||||
ln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.
|
|
||||||
|
|
||||||
### OpenAI Codex
|
|
||||||
|
|
||||||
Codex discovers skills from `.codex/skills/` in the project root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .codex/skills
|
|
||||||
ln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Codex skills docs](https://developers.openai.com/codex/skills/) for details.
|
|
||||||
|
|
||||||
### Other Agents
|
|
||||||
|
|
||||||
The Agent Skills standard is supported by 30+ tools including Goose, Roo Code, JetBrains Junie, Amp, OpenCode, OpenHands, Kiro, and more. Most discover skills from a dot-directory at the project root (e.g., `.goose/skills/`, `.roo/skills/`). Some read `.agents/skills/` directly.
|
|
||||||
|
|
||||||
Check the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.
|
|
||||||
|
|
||||||
The general pattern is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .<agent>/skills
|
|
||||||
ln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding New Skills
|
|
||||||
|
|
||||||
To add a new skill to this directory:
|
|
||||||
|
|
||||||
1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)
|
|
||||||
2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions
|
|
||||||
3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories
|
|
||||||
4. Keep `SKILL.md` under 500 lines - move detailed content to reference files
|
|
||||||
|
|
||||||
See the [Agent Skills specification](https://agentskills.io/specification) for the full format.
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
---
|
|
||||||
name: dms-plugin-dev
|
|
||||||
description: >
|
|
||||||
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
|
|
||||||
Quickshell. Supports four plugin types: widget (bar + Control Center), daemon (background
|
|
||||||
service), launcher (search + actions), and desktop (draggable desktop widgets). Covers
|
|
||||||
manifest creation, QML component development, settings UI, data persistence, theme
|
|
||||||
integration, PopoutService usage, and external command execution. Use when the user wants
|
|
||||||
to create, modify, or debug a DMS plugin, or asks about the DMS plugin API.
|
|
||||||
compatibility: Designed for Claude Code (or similar products)
|
|
||||||
metadata:
|
|
||||||
author: DankMaterialShell
|
|
||||||
version: "1.0"
|
|
||||||
domain: qml-desktop-development
|
|
||||||
framework: DankMaterialShell
|
|
||||||
languages: qml, javascript
|
|
||||||
allowed-tools: Bash Read Write Edit
|
|
||||||
---
|
|
||||||
|
|
||||||
# DankMaterialShell Plugin Development
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
DMS plugins extend the desktop shell with custom widgets, background services, launcher
|
|
||||||
integrations, and desktop widgets. Plugins are QML components discovered from
|
|
||||||
`~/.config/DankMaterialShell/plugins/`.
|
|
||||||
|
|
||||||
**Minimum plugin structure:**
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.config/DankMaterialShell/plugins/YourPlugin/
|
|
||||||
plugin.json # Required: manifest with metadata
|
|
||||||
YourComponent.qml # Required: main QML component
|
|
||||||
YourSettings.qml # Optional: settings UI
|
|
||||||
*.js # Optional: JavaScript utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
|
|
||||||
|
|
||||||
**Four plugin types:**
|
|
||||||
|
|
||||||
| Type | Purpose | Base Component | Bar pills | CC integration |
|
|
||||||
|------------|--------------------------------|----------------------------|-----------|----------------|
|
|
||||||
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
|
|
||||||
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
|
|
||||||
| `launcher` | Searchable items in launcher | `Item` | No | No |
|
|
||||||
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
|
|
||||||
|
|
||||||
## Step 1: Determine Plugin Type
|
|
||||||
|
|
||||||
Choose the type based on what the plugin does:
|
|
||||||
|
|
||||||
- **Shows in the bar?** - Use `widget`. Displays a pill in DankBar, optionally opens a popout,
|
|
||||||
optionally integrates with Control Center.
|
|
||||||
- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper
|
|
||||||
changes, notifications, battery level, etc.).
|
|
||||||
- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher
|
|
||||||
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
|
|
||||||
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
|
|
||||||
desktop layer.
|
|
||||||
|
|
||||||
## Step 2: Create the Manifest
|
|
||||||
|
|
||||||
Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.
|
|
||||||
|
|
||||||
**Minimal manifest:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "yourPlugin",
|
|
||||||
"name": "Your Plugin Name",
|
|
||||||
"description": "Brief description of what your plugin does",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"type": "widget",
|
|
||||||
"capabilities": ["your-capability"],
|
|
||||||
"component": "./YourWidget.qml"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**With settings and permissions:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "yourPlugin",
|
|
||||||
"name": "Your Plugin Name",
|
|
||||||
"description": "Brief description",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"type": "widget",
|
|
||||||
"capabilities": ["your-capability"],
|
|
||||||
"component": "./YourWidget.qml",
|
|
||||||
"icon": "extension",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"requires_dms": ">=0.1.0",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key rules:**
|
|
||||||
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
|
|
||||||
- `version` must be semver (e.g., `1.0.0`)
|
|
||||||
- `component` must start with `./` and end with `.qml`
|
|
||||||
- `type: "launcher"` requires a `trigger` field
|
|
||||||
- `settings_write` permission is **required** if the plugin has a settings component
|
|
||||||
|
|
||||||
## Step 3: Create the Main Component
|
|
||||||
|
|
||||||
### Widget
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: label.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: parent.widgetThickness
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Hello"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: parent.widgetThickness
|
|
||||||
height: label.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Hi"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
rotation: 90
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.
|
|
||||||
|
|
||||||
### Launcher
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string trigger: "#"
|
|
||||||
|
|
||||||
signal itemsChanged()
|
|
||||||
|
|
||||||
function getItems(query) {
|
|
||||||
const items = [
|
|
||||||
{ name: "Item One", icon: "material:star", comment: "Description",
|
|
||||||
action: "toast:Hello!", categories: ["MyPlugin"] }
|
|
||||||
]
|
|
||||||
if (!query) return items
|
|
||||||
const q = query.toLowerCase()
|
|
||||||
return items.filter(i => i.name.toLowerCase().includes(q))
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeItem(item) {
|
|
||||||
const [type, ...rest] = item.action.split(":")
|
|
||||||
const data = rest.join(":")
|
|
||||||
if (type === "toast") ToastService?.showInfo(data)
|
|
||||||
else if (type === "copy") Quickshell.execDetached(["dms", "cl", "copy", data])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.
|
|
||||||
|
|
||||||
### Desktop
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string pluginId: ""
|
|
||||||
property bool editMode: false
|
|
||||||
property real widgetWidth: 200
|
|
||||||
property real widgetHeight: 200
|
|
||||||
property real minWidth: 150
|
|
||||||
property real minHeight: 150
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
opacity: 0.85
|
|
||||||
border.color: root.editMode ? Theme.primary : "transparent"
|
|
||||||
border.width: root.editMode ? 2 : 0
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Desktop Widget"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.
|
|
||||||
|
|
||||||
### Daemon
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onSomeSignal() {
|
|
||||||
console.log("Event received")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
|
|
||||||
|
|
||||||
## Step 4: Add Settings (Optional)
|
|
||||||
|
|
||||||
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "yourPlugin"
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "apiKey"
|
|
||||||
label: "API Key"
|
|
||||||
description: "Your API key"
|
|
||||||
placeholder: "sk-..."
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "enabled"
|
|
||||||
label: "Enable Feature"
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectionSetting {
|
|
||||||
settingKey: "interval"
|
|
||||||
label: "Refresh Interval"
|
|
||||||
options: [
|
|
||||||
{ label: "1 min", value: "60" },
|
|
||||||
{ label: "5 min", value: "300" }
|
|
||||||
]
|
|
||||||
defaultValue: "300"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.
|
|
||||||
|
|
||||||
See [settings-components-reference.md](references/settings-components-reference.md) for full property lists.
|
|
||||||
|
|
||||||
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
|
|
||||||
|
|
||||||
## Step 5: Use Data Persistence
|
|
||||||
|
|
||||||
Three tiers of persistence:
|
|
||||||
|
|
||||||
| API | Persisted | Use case |
|
|
||||||
|-----|-----------|----------|
|
|
||||||
| `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 |
|
|
||||||
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
|
|
||||||
|
|
||||||
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
|
|
||||||
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
|
|
||||||
- Global vars sync across all instances (multi-monitor, multiple bar sections)
|
|
||||||
|
|
||||||
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
|
|
||||||
|
|
||||||
## Step 6: Theme Integration
|
|
||||||
|
|
||||||
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
|
|
||||||
|
|
||||||
**Essential properties:**
|
|
||||||
- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`
|
|
||||||
- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)
|
|
||||||
- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`
|
|
||||||
- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`
|
|
||||||
- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)
|
|
||||||
|
|
||||||
**Common widgets from `qs.Widgets`:** `StyledText`, `StyledRect`, `DankIcon`, `DankButton`, `DankToggle`, `DankTextField`, `DankSlider`, `DankGridView`, `CachingImage`.
|
|
||||||
|
|
||||||
See [theme-reference.md](references/theme-reference.md) for the complete property list.
|
|
||||||
|
|
||||||
## Step 7: Add Popout Content (Widgets Only)
|
|
||||||
|
|
||||||
Add a popout that opens when the bar pill is clicked:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
popoutWidth: 400
|
|
||||||
popoutHeight: 300
|
|
||||||
|
|
||||||
popoutContent: Component {
|
|
||||||
PopoutComponent {
|
|
||||||
headerText: "My Plugin"
|
|
||||||
detailsText: "Optional subtitle"
|
|
||||||
showCloseButton: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Content here"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
horizontalBarPill: Component { /* ... */ }
|
|
||||||
verticalBarPill: Component { /* ... */ }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**PopoutComponent properties:** `headerText`, `detailsText`, `showCloseButton`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).
|
|
||||||
|
|
||||||
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
|
||||||
|
|
||||||
## Step 8: Control Center Integration (Widgets Only)
|
|
||||||
|
|
||||||
Add your widget to the Control Center grid:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
ccWidgetIcon: "toggle_on"
|
|
||||||
ccWidgetPrimaryText: "My Feature"
|
|
||||||
ccWidgetSecondaryText: isActive ? "On" : "Off"
|
|
||||||
ccWidgetIsActive: isActive
|
|
||||||
|
|
||||||
onCcWidgetToggled: {
|
|
||||||
isActive = !isActive
|
|
||||||
pluginService?.savePluginData(pluginId, "active", isActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: expandable detail panel (for CompoundPill)
|
|
||||||
ccDetailContent: Component {
|
|
||||||
Rectangle {
|
|
||||||
implicitHeight: 200
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
|
|
||||||
|
|
||||||
## Step 9: External Commands and Clipboard
|
|
||||||
|
|
||||||
**Run commands and capture output:**
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Proc.runCommand(
|
|
||||||
"myPlugin.fetch",
|
|
||||||
["curl", "-s", "https://api.example.com/data"],
|
|
||||||
(stdout, exitCode) => {
|
|
||||||
if (exitCode === 0) processData(stdout)
|
|
||||||
},
|
|
||||||
500 // debounce ms
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fire-and-forget (clipboard, notifications):**
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
|
||||||
```
|
|
||||||
|
|
||||||
**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.
|
|
||||||
|
|
||||||
**Shell commands with pipes:** `["sh", "-c", "ps aux | grep foo"]`
|
|
||||||
|
|
||||||
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
|
||||||
|
|
||||||
## Step 10: Validate and Test
|
|
||||||
|
|
||||||
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`
|
|
||||||
3. Open Settings > Plugins > Scan for Plugins
|
|
||||||
4. Enable your plugin and add it to the DankBar layout
|
|
||||||
|
|
||||||
**Common issues:**
|
|
||||||
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
|
|
||||||
- Widget not showing: ensure it's enabled AND added to a DankBar section
|
|
||||||
- Settings error: verify `settings_write` permission is declared
|
|
||||||
- Data not persisting: check pluginService injection and permissions
|
|
||||||
|
|
||||||
## Common Mistakes
|
|
||||||
|
|
||||||
1. **Missing `settings_write` permission** - Settings UI shows error without it
|
|
||||||
2. **Missing `property var popoutService: null`** - Must declare for injection to work
|
|
||||||
3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge
|
|
||||||
4. **Hardcoded colors** - Use `Theme.*` properties, not hex values
|
|
||||||
5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached(["dms", "cl", "copy", text])`
|
|
||||||
6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`
|
|
||||||
7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)
|
|
||||||
8. **Forgetting `categories` in launcher items** - Items won't display without it
|
|
||||||
9. **Not handling null pluginService** - Always use optional chaining or null checks
|
|
||||||
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
|
|
||||||
|
|
||||||
## Quick Reference: Imports
|
|
||||||
|
|
||||||
**Widget / Daemon:**
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
```
|
|
||||||
|
|
||||||
**Launcher:**
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Services
|
|
||||||
```
|
|
||||||
|
|
||||||
**Desktop:**
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
```
|
|
||||||
|
|
||||||
**For clipboard/exec:** `import Quickshell`
|
|
||||||
**For processes:** `import Quickshell.Io`
|
|
||||||
**For networking:** `import Quickshell.Networking`
|
|
||||||
**For toast notifications:** access `ToastService` from `qs.Services`
|
|
||||||
|
|
||||||
## Quick Reference: File Naming
|
|
||||||
|
|
||||||
- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)
|
|
||||||
- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)
|
|
||||||
- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)
|
|
||||||
- **Component paths in manifest:** relative with `./` prefix (e.g., `"./MyWidget.qml"`)
|
|
||||||
- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)
|
|
||||||
|
|
||||||
## Reference Files
|
|
||||||
|
|
||||||
Load these on demand for detailed API documentation:
|
|
||||||
|
|
||||||
- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema
|
|
||||||
- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar pills, popouts, click actions, CC integration
|
|
||||||
- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view
|
|
||||||
- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence
|
|
||||||
- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution
|
|
||||||
- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists
|
|
||||||
- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns
|
|
||||||
- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables
|
|
||||||
- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals
|
|
||||||
- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"$id": "https://danklinux.com/schemas/plugin.json",
|
|
||||||
"title": "DankMaterialShell Plugin Manifest",
|
|
||||||
"description": "Schema for DankMaterialShell plugin.json manifest files",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"version",
|
|
||||||
"author",
|
|
||||||
"type",
|
|
||||||
"capabilities",
|
|
||||||
"component"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Unique plugin identifier (camelCase, no spaces)",
|
|
||||||
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Human-readable plugin name",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Short description of plugin functionality",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Semantic version string (e.g., '1.0.0')",
|
|
||||||
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Plugin creator name or email",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Plugin type",
|
|
||||||
"enum": ["widget", "daemon", "launcher", "desktop"]
|
|
||||||
},
|
|
||||||
"capabilities": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of plugin capabilities",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"minItems": 1
|
|
||||||
},
|
|
||||||
"component": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Relative path to main QML component file",
|
|
||||||
"pattern": "^\\./.*\\.qml$"
|
|
||||||
},
|
|
||||||
"trigger": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Trigger string for launcher activation (required for launcher type)"
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Material Design icon name"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path to settings component QML file",
|
|
||||||
"pattern": "^\\./.*\\.qml$"
|
|
||||||
},
|
|
||||||
"requires_dms": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
|
||||||
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
|
||||||
},
|
|
||||||
"requires": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of required system tools/dependencies",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Required capabilities",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"settings_read",
|
|
||||||
"settings_write",
|
|
||||||
"process",
|
|
||||||
"network"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"if": {
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"const": "launcher"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"then": {
|
|
||||||
"required": ["trigger"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"additionalProperties": true
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
// TODO: Read configuration from settings
|
|
||||||
property string configValue: pluginData?.configValue || ""
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId !== pluginId) return
|
|
||||||
configValue = pluginService.loadPluginData(pluginId, "configValue", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Connect to the service events you need
|
|
||||||
// Connections {
|
|
||||||
// target: SessionData
|
|
||||||
// function onWallpaperPathChanged() {
|
|
||||||
// console.log("[MyDaemon] Wallpaper changed:", SessionData.wallpaperPath)
|
|
||||||
// handleEvent(SessionData.wallpaperPath)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
function handleEvent(data) {
|
|
||||||
Proc.runCommand(
|
|
||||||
"myDaemon.handle",
|
|
||||||
["echo", "Event received:", data],
|
|
||||||
(stdout, exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
console.log("[MyDaemon] Output:", stdout)
|
|
||||||
} else {
|
|
||||||
console.error("[MyDaemon] Failed:", exitCode)
|
|
||||||
ToastService?.showInfo("Daemon action failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
console.log("[MyDaemon] Started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myDaemon"
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "configValue"
|
|
||||||
label: "Configuration"
|
|
||||||
description: "Value used by the daemon"
|
|
||||||
placeholder: "Enter value..."
|
|
||||||
defaultValue: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "myDaemon",
|
|
||||||
"name": "My Daemon",
|
|
||||||
"description": "A background service that reacts to events",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"type": "daemon",
|
|
||||||
"capabilities": ["background-service"],
|
|
||||||
"component": "./Daemon.qml",
|
|
||||||
"icon": "settings",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write", "process"]
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myDesktopWidget"
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "opacity"
|
|
||||||
label: "Opacity"
|
|
||||||
description: "Widget background opacity"
|
|
||||||
defaultValue: 85
|
|
||||||
minimum: 10
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string pluginId: ""
|
|
||||||
property bool editMode: false
|
|
||||||
property real widgetWidth: 200
|
|
||||||
property real widgetHeight: 200
|
|
||||||
property real minWidth: 150
|
|
||||||
property real minHeight: 150
|
|
||||||
|
|
||||||
// TODO: Load settings reactively
|
|
||||||
property real bgOpacity: {
|
|
||||||
if (!pluginService) return 0.85
|
|
||||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
|
||||||
return val / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId !== pluginId) return
|
|
||||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
|
||||||
bgOpacity = val / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
opacity: root.bgOpacity
|
|
||||||
border.color: root.editMode ? Theme.primary : "transparent"
|
|
||||||
border.width: root.editMode ? 2 : 0
|
|
||||||
|
|
||||||
// TODO: Add your widget content here
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Desktop Widget"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "myDesktopWidget",
|
|
||||||
"name": "My Desktop Widget",
|
|
||||||
"description": "A custom desktop widget",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"type": "desktop",
|
|
||||||
"capabilities": ["desktop-widget"],
|
|
||||||
"component": "./Widget.qml",
|
|
||||||
"icon": "widgets",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string trigger: "#"
|
|
||||||
|
|
||||||
signal itemsChanged()
|
|
||||||
|
|
||||||
// TODO: Define your items
|
|
||||||
property var allItems: [
|
|
||||||
{
|
|
||||||
name: "Example Item",
|
|
||||||
icon: "material:star",
|
|
||||||
comment: "An example launcher item",
|
|
||||||
action: "toast:Hello from my launcher!",
|
|
||||||
categories: ["MyLauncher"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
function getItems(query) {
|
|
||||||
if (!query || query.length === 0) return allItems
|
|
||||||
|
|
||||||
var q = query.toLowerCase()
|
|
||||||
return allItems.filter(function(item) {
|
|
||||||
return item.name.toLowerCase().includes(q) ||
|
|
||||||
item.comment.toLowerCase().includes(q)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeItem(item) {
|
|
||||||
var actionParts = item.action.split(":")
|
|
||||||
var actionType = actionParts[0]
|
|
||||||
var actionData = actionParts.slice(1).join(":")
|
|
||||||
|
|
||||||
switch (actionType) {
|
|
||||||
case "toast":
|
|
||||||
if (typeof ToastService !== "undefined")
|
|
||||||
ToastService.showInfo(actionData)
|
|
||||||
break
|
|
||||||
case "copy":
|
|
||||||
Quickshell.execDetached(["dms", "cl", "copy", actionData])
|
|
||||||
if (typeof ToastService !== "undefined")
|
|
||||||
ToastService.showInfo("Copied to clipboard")
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.warn("Unknown action type:", actionType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (pluginService) {
|
|
||||||
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myLauncher"
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "trigger"
|
|
||||||
label: "Trigger"
|
|
||||||
description: "Type this prefix in the launcher to activate the plugin"
|
|
||||||
placeholder: "#"
|
|
||||||
defaultValue: "#"
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "noTrigger"
|
|
||||||
label: "Always Visible"
|
|
||||||
description: "Show items alongside regular apps without needing a trigger"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "myLauncher",
|
|
||||||
"name": "My Launcher",
|
|
||||||
"description": "Custom launcher plugin with searchable items",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"type": "launcher",
|
|
||||||
"capabilities": ["launcher"],
|
|
||||||
"component": "./Launcher.qml",
|
|
||||||
"trigger": "#",
|
|
||||||
"icon": "search",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myWidget"
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "text"
|
|
||||||
label: "Display Text"
|
|
||||||
description: "Text shown in the bar widget"
|
|
||||||
placeholder: "Hello"
|
|
||||||
defaultValue: "Hello"
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "showIcon"
|
|
||||||
label: "Show Icon"
|
|
||||||
description: "Display an icon next to the text"
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
// TODO: Read settings reactively
|
|
||||||
property string displayText: pluginData?.text || "Hello"
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId !== pluginId) return
|
|
||||||
displayText = pluginService.loadPluginData(pluginId, "text", "Hello")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: label.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: parent.widgetThickness
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.displayText
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: parent.widgetThickness
|
|
||||||
height: label.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.displayText
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
rotation: 90
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Uncomment and customize popout content
|
|
||||||
// popoutWidth: 350
|
|
||||||
// popoutHeight: 300
|
|
||||||
// popoutContent: Component {
|
|
||||||
// PopoutComponent {
|
|
||||||
// headerText: "My Widget"
|
|
||||||
// showCloseButton: true
|
|
||||||
//
|
|
||||||
// Column {
|
|
||||||
// width: parent.width
|
|
||||||
// spacing: Theme.spacingM
|
|
||||||
//
|
|
||||||
// StyledText {
|
|
||||||
// text: "Popout content here"
|
|
||||||
// color: Theme.surfaceText
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "myWidget",
|
|
||||||
"name": "My Widget",
|
|
||||||
"description": "A custom bar widget",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Your Name",
|
|
||||||
"type": "widget",
|
|
||||||
"capabilities": ["dankbar-widget"],
|
|
||||||
"component": "./Widget.qml",
|
|
||||||
"icon": "extension",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
# Advanced Patterns
|
|
||||||
|
|
||||||
Patterns observed in production DMS plugins that go beyond the basics.
|
|
||||||
|
|
||||||
## Plugin Variants
|
|
||||||
|
|
||||||
Create multiple widget instances from a single plugin definition. Each variant has its own configuration.
|
|
||||||
|
|
||||||
### Manifest
|
|
||||||
|
|
||||||
No special manifest changes needed - the variant system is built into PluginComponent.
|
|
||||||
|
|
||||||
### Widget with Variant Support
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
property string variantId: ""
|
|
||||||
property var variantData: ({})
|
|
||||||
|
|
||||||
property string displayText: variantData?.text || "Default"
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: label.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: parent.widgetThickness
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.displayText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Widget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)
|
|
||||||
|
|
||||||
### Settings with Variant Management
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "exampleVariants"
|
|
||||||
|
|
||||||
// Variant creation UI
|
|
||||||
DankButton {
|
|
||||||
text: "Add New Instance"
|
|
||||||
onClicked: {
|
|
||||||
var id = "variant_" + Date.now()
|
|
||||||
root.createVariant(id, { name: "New Instance", text: "Hello" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-variant configuration
|
|
||||||
Repeater {
|
|
||||||
model: root.variants
|
|
||||||
delegate: Column {
|
|
||||||
StringSetting {
|
|
||||||
settingKey: modelData.id + "_text"
|
|
||||||
label: modelData.name || modelData.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## JavaScript Utility Files
|
|
||||||
|
|
||||||
For complex logic, split into `.js` files:
|
|
||||||
|
|
||||||
### utils.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
.pragma library
|
|
||||||
|
|
||||||
function formatDuration(ms) {
|
|
||||||
if (ms < 60000) return "just now"
|
|
||||||
if (ms < 3600000) return Math.floor(ms / 60000) + "m ago"
|
|
||||||
return Math.floor(ms / 3600000) + "h ago"
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseResponse(json) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(json)
|
|
||||||
} catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using in QML
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import "utils.js" as Utils
|
|
||||||
|
|
||||||
Item {
|
|
||||||
StyledText {
|
|
||||||
text: Utils.formatDuration(Date.now() - timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.
|
|
||||||
|
|
||||||
## qmldir for Singleton Services
|
|
||||||
|
|
||||||
For plugins with internal singleton services:
|
|
||||||
|
|
||||||
### qmldir
|
|
||||||
|
|
||||||
```
|
|
||||||
singleton MyService 1.0 MyService.qml
|
|
||||||
```
|
|
||||||
|
|
||||||
### MyService.qml
|
|
||||||
|
|
||||||
```qml
|
|
||||||
pragma Singleton
|
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
property var cache: ({})
|
|
||||||
|
|
||||||
function getData(key) {
|
|
||||||
return cache[key] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function setData(key, value) {
|
|
||||||
cache[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the singleton
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import "." as Local
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Component.onCompleted: {
|
|
||||||
Local.MyService.setData("key", "value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Inline Component Declarations
|
|
||||||
|
|
||||||
Reusable sub-components defined inline:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Item {
|
|
||||||
component StatusBadge: Rectangle {
|
|
||||||
property string label: ""
|
|
||||||
property color badgeColor: Theme.primary
|
|
||||||
|
|
||||||
width: badgeText.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
color: badgeColor
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: badgeText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: label
|
|
||||||
color: Theme.onPrimary
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
StatusBadge { label: "Running"; badgeColor: Theme.success }
|
|
||||||
StatusBadge { label: "Stopped"; badgeColor: Theme.error }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multi-Provider Adapter Pattern
|
|
||||||
|
|
||||||
For plugins supporting multiple backends (AI providers, API services):
|
|
||||||
|
|
||||||
### apiAdapters.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
.pragma library
|
|
||||||
|
|
||||||
function createAdapter(provider) {
|
|
||||||
switch (provider) {
|
|
||||||
case "openai": return {
|
|
||||||
url: "https://api.openai.com/v1/chat/completions",
|
|
||||||
headers: (key) => ({ "Authorization": "Bearer " + key }),
|
|
||||||
formatRequest: (messages) => JSON.stringify({ model: "gpt-4", messages: messages }),
|
|
||||||
parseResponse: (text) => JSON.parse(text).choices[0].message.content
|
|
||||||
}
|
|
||||||
case "anthropic": return {
|
|
||||||
url: "https://api.anthropic.com/v1/messages",
|
|
||||||
headers: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" }),
|
|
||||||
formatRequest: (messages) => JSON.stringify({ model: "claude-sonnet-4-20250514", messages: messages }),
|
|
||||||
parseResponse: (text) => JSON.parse(text).content[0].text
|
|
||||||
}
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## IPC Integration
|
|
||||||
|
|
||||||
For plugins that respond to keyboard shortcuts or external commands:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
Connections {
|
|
||||||
target: DMSIpc
|
|
||||||
function onCommandReceived(command, args) {
|
|
||||||
if (command === "myPlugin.toggle") {
|
|
||||||
doToggle()
|
|
||||||
} else if (command === "myPlugin.next") {
|
|
||||||
goNext()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
External trigger: `dms ipc call myPlugin.toggle`
|
|
||||||
|
|
||||||
## Networking with Quickshell.Networking
|
|
||||||
|
|
||||||
For API calls using the built-in networking module:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import Quickshell.Networking
|
|
||||||
|
|
||||||
Item {
|
|
||||||
NetworkRequest {
|
|
||||||
id: request
|
|
||||||
url: "https://api.example.com/data"
|
|
||||||
method: "GET"
|
|
||||||
|
|
||||||
onResponseReceived: (response) => {
|
|
||||||
const data = JSON.parse(response.body)
|
|
||||||
processData(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
onErrorOccurred: (error) => {
|
|
||||||
console.error("Network error:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchData() {
|
|
||||||
request.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Toast Notifications
|
|
||||||
|
|
||||||
Show user feedback:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
// Info toast
|
|
||||||
ToastService?.showInfo("Operation completed")
|
|
||||||
|
|
||||||
// With title
|
|
||||||
ToastService?.showInfo("Plugin Name", "Data refreshed successfully")
|
|
||||||
```
|
|
||||||
|
|
||||||
Always use optional chaining since ToastService may not be available in all contexts.
|
|
||||||
|
|
||||||
## Clipboard Operations
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
function copyToClipboard(text) {
|
|
||||||
Quickshell.execDetached(["dms", "cl", "copy", text])
|
|
||||||
ToastService?.showInfo("Copied to clipboard")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Do NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.
|
|
||||||
|
|
||||||
## Multi-File Plugin Architecture
|
|
||||||
|
|
||||||
Large plugins can be split across multiple files:
|
|
||||||
|
|
||||||
```
|
|
||||||
MyPlugin/
|
|
||||||
plugin.json
|
|
||||||
Main.qml # Main widget component
|
|
||||||
Settings.qml # Settings UI
|
|
||||||
DetailView.qml # Popout detail view
|
|
||||||
utils.js # Utility functions
|
|
||||||
apiAdapter.js # API adapter layer
|
|
||||||
qmldir # Optional: singleton registrations
|
|
||||||
```
|
|
||||||
|
|
||||||
Import sibling files:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// In Main.qml
|
|
||||||
import "." as Local
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Loader {
|
|
||||||
source: "DetailView.qml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Tips
|
|
||||||
|
|
||||||
1. Use `Proc.runCommand` with appropriate debounce for external commands
|
|
||||||
2. Pre-cache images and thumbnails for image-heavy plugins
|
|
||||||
3. Limit concurrent network requests
|
|
||||||
4. Use `Timer` with reasonable intervals (don't poll faster than needed)
|
|
||||||
5. Lazy-load heavy content (use `Loader` for complex popout content)
|
|
||||||
6. Avoid blocking the UI thread with synchronous operations
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
# Daemon Plugin Guide
|
|
||||||
|
|
||||||
Daemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.
|
|
||||||
|
|
||||||
## Base Component
|
|
||||||
|
|
||||||
Daemons use `PluginComponent` with no bar pills:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
// Event-driven logic goes here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## When to Use Daemons
|
|
||||||
|
|
||||||
- Monitor system events (wallpaper changes, battery level, notifications)
|
|
||||||
- Run periodic background tasks (polling APIs, checking system state)
|
|
||||||
- Execute scripts in response to events
|
|
||||||
- Control shell UI via PopoutService based on conditions
|
|
||||||
|
|
||||||
## Event-Driven Pattern
|
|
||||||
|
|
||||||
Use `Connections` to react to service signals:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onWallpaperPathChanged() {
|
|
||||||
console.log("Wallpaper changed to:", SessionData.wallpaperPath)
|
|
||||||
runScript(SessionData.wallpaperPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: BatteryService
|
|
||||||
function onPercentageChanged() {
|
|
||||||
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
|
|
||||||
popoutService?.openBattery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Services
|
|
||||||
|
|
||||||
Common services daemons can connect to:
|
|
||||||
|
|
||||||
| Service | Signals/Properties | Description |
|
|
||||||
|---------|-------------------|-------------|
|
|
||||||
| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |
|
|
||||||
| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |
|
|
||||||
| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |
|
|
||||||
| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |
|
|
||||||
|
|
||||||
Import services from `qs.Services`.
|
|
||||||
|
|
||||||
## Process Execution
|
|
||||||
|
|
||||||
### Simple command with Proc
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
function runScript(arg) {
|
|
||||||
Proc.runCommand(
|
|
||||||
"myDaemon.script",
|
|
||||||
["bash", "-c", "echo 'Processing: " + arg + "'"],
|
|
||||||
(stdout, exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
console.log("Script output:", stdout)
|
|
||||||
} else {
|
|
||||||
ToastService?.showInfo("Script failed: exit " + exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Long-running process with Process component
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
property string scriptPath: ""
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: proc
|
|
||||||
command: ["bash", scriptPath]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onTextReceived: (text) => {
|
|
||||||
console.log("stdout:", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onTextReceived: (text) => {
|
|
||||||
console.error("stderr:", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
ToastService?.showInfo("Process failed: exit " + exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startProcess() {
|
|
||||||
if (scriptPath && !proc.running) {
|
|
||||||
proc.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Timer-Based Polling
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
Timer {
|
|
||||||
interval: 60000 // every minute
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: checkStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkStatus() {
|
|
||||||
Proc.runCommand(
|
|
||||||
"myDaemon.check",
|
|
||||||
["sh", "-c", "systemctl is-active myservice"],
|
|
||||||
(stdout, exitCode) => {
|
|
||||||
const active = stdout.trim() === "active"
|
|
||||||
PluginService.setGlobalVar("myDaemon", "serviceActive", active)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Persistence
|
|
||||||
|
|
||||||
Daemons access PluginService directly (it's injected via PluginComponent):
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
property string configuredScript: pluginData?.scriptPath || ""
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId === pluginId) {
|
|
||||||
configuredScript = pluginService.loadPluginData(pluginId, "scriptPath", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PopoutService Usage
|
|
||||||
|
|
||||||
Daemons can control shell UI via the injected popoutService:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
function showAlert() {
|
|
||||||
popoutService?.openNotificationCenter()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSettings() {
|
|
||||||
popoutService?.openSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [popout-service-reference.md](popout-service-reference.md) for the full API.
|
|
||||||
|
|
||||||
## Complete Example
|
|
||||||
|
|
||||||
Based on the WallpaperWatcherDaemon:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
property string scriptPath: pluginData?.scriptPath || ""
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId === pluginId) {
|
|
||||||
scriptPath = pluginService.loadPluginData(pluginId, "scriptPath", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onWallpaperPathChanged() {
|
|
||||||
if (scriptPath) {
|
|
||||||
runWallpaperScript(SessionData.wallpaperPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runWallpaperScript(wallpaperPath) {
|
|
||||||
console.log("[WallpaperWatcher] Running script:", scriptPath, wallpaperPath)
|
|
||||||
|
|
||||||
Proc.runCommand(
|
|
||||||
"wallpaperWatcher.run",
|
|
||||||
["bash", scriptPath, wallpaperPath],
|
|
||||||
(stdout, exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
console.log("[WallpaperWatcher] Script output:", stdout)
|
|
||||||
} else {
|
|
||||||
console.error("[WallpaperWatcher] Script failed:", exitCode)
|
|
||||||
ToastService?.showInfo("Wallpaper script failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
console.log("[WallpaperWatcher] Daemon started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manifest Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "wallpaperWatcher",
|
|
||||||
"name": "Wallpaper Watcher",
|
|
||||||
"description": "Runs a script when the wallpaper changes",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Developer",
|
|
||||||
"type": "daemon",
|
|
||||||
"capabilities": ["wallpaper-automation"],
|
|
||||||
"component": "./WallpaperWatcher.qml",
|
|
||||||
"icon": "wallpaper",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write", "process"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
# Data Persistence Guide
|
|
||||||
|
|
||||||
DMS plugins have three tiers of data persistence, each suited for different use cases.
|
|
||||||
|
|
||||||
## Tier 1: Plugin Data (Settings)
|
|
||||||
|
|
||||||
Persisted to `settings.json`. Use for user preferences and configuration.
|
|
||||||
|
|
||||||
### Saving
|
|
||||||
|
|
||||||
```qml
|
|
||||||
pluginService.savePluginData(pluginId, "key", value)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading
|
|
||||||
|
|
||||||
```qml
|
|
||||||
var value = pluginService.loadPluginData(pluginId, "key", defaultValue)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reactive Access via pluginData
|
|
||||||
|
|
||||||
`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
property string displayText: pluginData?.text || "Default"
|
|
||||||
property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reacting to Settings Changes
|
|
||||||
|
|
||||||
When settings are changed (e.g., from the settings UI), react with `Connections`:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId !== pluginId) return
|
|
||||||
displayText = pluginService.loadPluginData(pluginId, "text", "Default")
|
|
||||||
showIcon = pluginService.loadPluginData(pluginId, "showIcon", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tier 2: Plugin State
|
|
||||||
|
|
||||||
Persisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).
|
|
||||||
|
|
||||||
### Saving
|
|
||||||
|
|
||||||
```qml
|
|
||||||
pluginService.savePluginState(pluginId, "key", value)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading
|
|
||||||
|
|
||||||
```qml
|
|
||||||
var state = pluginService.loadPluginState(pluginId, "key", defaultValue)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Additional Methods
|
|
||||||
|
|
||||||
```qml
|
|
||||||
pluginService.clearPluginState(pluginId)
|
|
||||||
pluginService.removePluginStateKey(pluginId, "key")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example: Persistent History
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Item {
|
|
||||||
property var history: []
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
history = pluginService?.loadPluginState(pluginId, "history", []) || []
|
|
||||||
}
|
|
||||||
|
|
||||||
function addToHistory(entry) {
|
|
||||||
history.unshift({
|
|
||||||
text: entry,
|
|
||||||
timestamp: Date.now()
|
|
||||||
})
|
|
||||||
if (history.length > 100) history = history.slice(0, 100)
|
|
||||||
pluginService?.savePluginState(pluginId, "history", history)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearHistory() {
|
|
||||||
history = []
|
|
||||||
pluginService?.removePluginStateKey(pluginId, "history")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tier 3: Global Variables (Runtime Only)
|
|
||||||
|
|
||||||
NOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).
|
|
||||||
|
|
||||||
### Using PluginGlobalVar Component
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
PluginGlobalVar {
|
|
||||||
id: globalCounter
|
|
||||||
varName: "counter"
|
|
||||||
defaultValue: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
// ...
|
|
||||||
StyledText {
|
|
||||||
text: "Count: " + globalCounter.value
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
onClicked: globalCounter.set(globalCounter.value + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**PluginGlobalVar properties:**
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `varName` | string | Required: name of the global variable |
|
|
||||||
| `defaultValue` | any | Optional: default if not set |
|
|
||||||
| `value` | any | Readonly: current value |
|
|
||||||
|
|
||||||
**Methods:**
|
|
||||||
- `set(newValue)` - update the value (triggers reactivity across all instances)
|
|
||||||
|
|
||||||
### Using PluginService API Directly
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
property int counter: PluginService.getGlobalVar("myPlugin", "counter", 0)
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: PluginService
|
|
||||||
function onGlobalVarChanged(pluginId, varName) {
|
|
||||||
if (pluginId === "myPlugin" && varName === "counter") {
|
|
||||||
counter = PluginService.getGlobalVar("myPlugin", "counter", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function increment() {
|
|
||||||
var current = PluginService.getGlobalVar("myPlugin", "counter", 0)
|
|
||||||
PluginService.setGlobalVar("myPlugin", "counter", current + 1)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Decision Matrix
|
|
||||||
|
|
||||||
| Need | API | Persisted | Scope |
|
|
||||||
|------|-----|-----------|-------|
|
|
||||||
| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |
|
|
||||||
| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
1. **pluginData is reactive** - bindings update automatically when data changes
|
|
||||||
2. **Global vars are NOT persistent** - they reset when the shell restarts
|
|
||||||
3. **State vs Data** - data is for user-facing settings, state is for internal runtime data
|
|
||||||
4. **Null safety** - always check `pluginService` is not null before calling methods
|
|
||||||
5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin
|
|
||||||
6. **Performance** - global vars are efficient for frequent updates; settings writes are batched
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# Desktop Plugin Guide
|
|
||||||
|
|
||||||
Desktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.
|
|
||||||
|
|
||||||
## Base Component
|
|
||||||
|
|
||||||
Desktop widgets use a plain `Item` with injected properties:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string pluginId: ""
|
|
||||||
property bool editMode: false
|
|
||||||
property real widgetWidth: 200
|
|
||||||
property real widgetHeight: 200
|
|
||||||
property real minWidth: 150
|
|
||||||
property real minHeight: 150
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
opacity: 0.85
|
|
||||||
|
|
||||||
// Your content here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Injected Properties
|
|
||||||
|
|
||||||
These are set automatically by the DesktopPluginWrapper:
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `pluginService` | object | PluginService reference for data persistence |
|
|
||||||
| `pluginId` | string | Plugin's unique identifier |
|
|
||||||
| `editMode` | bool | `true` when user is dragging/resizing |
|
|
||||||
| `widgetWidth` | real | Current widget container width |
|
|
||||||
| `widgetHeight` | real | Current widget container height |
|
|
||||||
|
|
||||||
## Optional Properties
|
|
||||||
|
|
||||||
Define these on your root item to customize behavior:
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|----------|------|---------|-------------|
|
|
||||||
| `minWidth` | real | 100 | Minimum allowed width during resize |
|
|
||||||
| `minHeight` | real | 100 | Minimum allowed height during resize |
|
|
||||||
|
|
||||||
## Position and Size Persistence
|
|
||||||
|
|
||||||
Position (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.
|
|
||||||
|
|
||||||
## Edit Mode
|
|
||||||
|
|
||||||
When `editMode` is true, the user is repositioning or resizing. Use this to:
|
|
||||||
- Show visual indicators (borders, handles)
|
|
||||||
- Disable interactive elements to prevent accidental actions
|
|
||||||
- Display additional controls
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
border.color: root.editMode ? Theme.primary : "transparent"
|
|
||||||
border.width: root.editMode ? 2 : 0
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: !root.editMode
|
|
||||||
onClicked: doSomething()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Loading and Saving Data
|
|
||||||
|
|
||||||
Use the injected `pluginService` for data persistence:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
property string displayMode: {
|
|
||||||
if (!pluginService) return "default"
|
|
||||||
return pluginService.loadPluginData(pluginId, "displayMode", "default")
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId !== pluginId) return
|
|
||||||
root.displayMode = pluginService.loadPluginData(pluginId, "displayMode", "default")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveMode(mode) {
|
|
||||||
pluginService?.savePluginData(pluginId, "displayMode", mode)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Settings Component
|
|
||||||
|
|
||||||
Desktop plugin settings use the same `PluginSettings` wrapper as other types:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myDesktopWidget"
|
|
||||||
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "opacity"
|
|
||||||
label: "Opacity"
|
|
||||||
description: "Widget background opacity"
|
|
||||||
defaultValue: 85
|
|
||||||
minimum: 10
|
|
||||||
maximum: 100
|
|
||||||
unit: "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectionSetting {
|
|
||||||
settingKey: "style"
|
|
||||||
label: "Display Style"
|
|
||||||
options: [
|
|
||||||
{ label: "Compact", value: "compact" },
|
|
||||||
{ label: "Expanded", value: "expanded" }
|
|
||||||
]
|
|
||||||
defaultValue: "compact"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Interaction
|
|
||||||
|
|
||||||
Desktop widgets support:
|
|
||||||
1. **Drag** - click and drag anywhere (in edit mode)
|
|
||||||
2. **Resize** - drag bottom-right corner handle (in edit mode)
|
|
||||||
3. **Edit mode toggle** - via the desktop edit button
|
|
||||||
|
|
||||||
## Complete Example
|
|
||||||
|
|
||||||
Based on the ExampleDesktopClock pattern:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string pluginId: ""
|
|
||||||
property bool editMode: false
|
|
||||||
property real widgetWidth: 250
|
|
||||||
property real widgetHeight: 250
|
|
||||||
property real minWidth: 150
|
|
||||||
property real minHeight: 150
|
|
||||||
|
|
||||||
property string clockStyle: {
|
|
||||||
if (!pluginService) return "digital"
|
|
||||||
return pluginService.loadPluginData(pluginId, "clockStyle", "digital")
|
|
||||||
}
|
|
||||||
|
|
||||||
property real bgOpacity: {
|
|
||||||
if (!pluginService) return 0.85
|
|
||||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
|
||||||
return val / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId !== pluginId) return
|
|
||||||
clockStyle = pluginService.loadPluginData(pluginId, "clockStyle", "digital")
|
|
||||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
|
||||||
bgOpacity = val / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
opacity: root.bgOpacity
|
|
||||||
border.color: root.editMode ? Theme.primary : "transparent"
|
|
||||||
border.width: root.editMode ? 2 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: Qt.formatTime(new Date(), "hh:mm:ss")
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: root.widgetWidth * 0.15
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: Qt.formatDate(new Date(), "ddd, MMM d")
|
|
||||||
color: Theme.onSurfaceVariant
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 1000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: root.widgetWidth = root.widgetWidth // force update
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manifest Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "myDesktopClock",
|
|
||||||
"name": "Desktop Clock",
|
|
||||||
"description": "Analog and digital clock for the desktop",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Developer",
|
|
||||||
"type": "desktop",
|
|
||||||
"capabilities": ["desktop-widget"],
|
|
||||||
"component": "./ClockWidget.qml",
|
|
||||||
"icon": "schedule",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
# Launcher Plugin Guide
|
|
||||||
|
|
||||||
Launcher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.
|
|
||||||
|
|
||||||
## Base Component
|
|
||||||
|
|
||||||
Launchers use a plain `Item` (not PluginComponent):
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string trigger: "#"
|
|
||||||
|
|
||||||
signal itemsChanged()
|
|
||||||
|
|
||||||
function getItems(query) {
|
|
||||||
// Return array of items
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeItem(item) {
|
|
||||||
// Handle item selection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Required Interface
|
|
||||||
|
|
||||||
| Member | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| `pluginService` | property | Injected PluginService reference (declare as `null`) |
|
|
||||||
| `trigger` | property | Trigger string for activation |
|
|
||||||
| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |
|
|
||||||
| `getItems(query)` | function | Return array of items matching query |
|
|
||||||
| `executeItem(item)` | function | Handle item selection |
|
|
||||||
|
|
||||||
## Item Structure
|
|
||||||
|
|
||||||
Each item returned by `getItems()`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
name: "Item Display Name", // Required: shown in launcher
|
|
||||||
icon: "material:star", // Optional: icon specification
|
|
||||||
comment: "Description text", // Required: subtitle text
|
|
||||||
action: "type:data", // Required: action identifier
|
|
||||||
categories: ["MyPlugin"], // Required: array with plugin category
|
|
||||||
imageUrl: "https://..." // Optional: image for tile view
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Icon Types
|
|
||||||
|
|
||||||
### 1. Material Design Icons
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{ icon: "material:lightbulb" }
|
|
||||||
{ icon: "material:terminal" }
|
|
||||||
{ icon: "material:translate" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Uses the Material Symbols Rounded font.
|
|
||||||
|
|
||||||
### 2. Unicode / Emoji Icons
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{ icon: "unicode:smile_face" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Rendered at 70-80% of icon size with theming.
|
|
||||||
|
|
||||||
### 3. Desktop Theme Icons
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{ icon: "firefox" }
|
|
||||||
{ icon: "folder" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Uses the user's installed icon theme.
|
|
||||||
|
|
||||||
### 4. No Icon
|
|
||||||
|
|
||||||
Omit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.
|
|
||||||
|
|
||||||
## Trigger System
|
|
||||||
|
|
||||||
**Custom trigger** (items only appear when trigger is typed):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "trigger": "#" }
|
|
||||||
```
|
|
||||||
|
|
||||||
- Type `#` alone: shows all plugin items
|
|
||||||
- Type `# query`: filters plugin items by query
|
|
||||||
- The query string (without trigger) is passed to `getItems(query)`
|
|
||||||
|
|
||||||
**No trigger** (items always visible alongside regular apps):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "trigger": "" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Save empty trigger at runtime:
|
|
||||||
```qml
|
|
||||||
Component.onCompleted: {
|
|
||||||
trigger = pluginService?.loadPluginData(pluginId, "trigger", "#") ?? "#"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Action Execution
|
|
||||||
|
|
||||||
Parse action strings in `executeItem()`:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
function executeItem(item) {
|
|
||||||
const actionParts = item.action.split(":")
|
|
||||||
const actionType = actionParts[0]
|
|
||||||
const actionData = actionParts.slice(1).join(":")
|
|
||||||
|
|
||||||
switch (actionType) {
|
|
||||||
case "toast":
|
|
||||||
ToastService?.showInfo(actionData)
|
|
||||||
break
|
|
||||||
case "copy":
|
|
||||||
Quickshell.execDetached(["dms", "cl", "copy", actionData])
|
|
||||||
ToastService?.showInfo("Copied to clipboard")
|
|
||||||
break
|
|
||||||
case "exec":
|
|
||||||
Quickshell.execDetached(actionData.split(" "))
|
|
||||||
break
|
|
||||||
case "url":
|
|
||||||
Quickshell.execDetached(["xdg-open", actionData])
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.warn("Unknown action type:", actionType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Search / Filtering
|
|
||||||
|
|
||||||
The `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).
|
|
||||||
|
|
||||||
```qml
|
|
||||||
function getItems(query) {
|
|
||||||
const allItems = [
|
|
||||||
{ name: "Calculator", icon: "material:calculate",
|
|
||||||
comment: "Open calculator", action: "exec:gnome-calculator",
|
|
||||||
categories: ["Tools"] },
|
|
||||||
{ name: "Terminal", icon: "material:terminal",
|
|
||||||
comment: "Open terminal", action: "exec:alacritty",
|
|
||||||
categories: ["Tools"] }
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!query || query.length === 0) return allItems
|
|
||||||
|
|
||||||
const q = query.toLowerCase()
|
|
||||||
return allItems.filter(item =>
|
|
||||||
item.name.toLowerCase().includes(q) ||
|
|
||||||
item.comment.toLowerCase().includes(q)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Context Menu Actions
|
|
||||||
|
|
||||||
Add right-click actions to launcher items:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
function getContextMenuActions(item) {
|
|
||||||
return [
|
|
||||||
{ name: "Copy", icon: "material:content_copy",
|
|
||||||
action: "copy:" + item.name },
|
|
||||||
{ name: "Open in Browser", icon: "material:open_in_new",
|
|
||||||
action: "url:" + item.url }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Context menu actions use the same `executeItem()` handler.
|
|
||||||
|
|
||||||
## Image Tile View
|
|
||||||
|
|
||||||
For image-heavy launchers (GIF search, sticker pickers), use tile view:
|
|
||||||
|
|
||||||
In `plugin.json`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"viewMode": "tile",
|
|
||||||
"viewModeEnforced": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In items:
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
name: "Image Title",
|
|
||||||
imageUrl: "https://example.com/image.png",
|
|
||||||
comment: "Description",
|
|
||||||
action: "copy:https://example.com/image.png",
|
|
||||||
categories: ["MyPlugin"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## State Persistence
|
|
||||||
|
|
||||||
For plugins with persistent state (notes, history, favorites):
|
|
||||||
|
|
||||||
```qml
|
|
||||||
property var notes: []
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
const saved = pluginService?.loadPluginState(pluginId, "notes", [])
|
|
||||||
if (saved) notes = saved
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNote(text) {
|
|
||||||
notes.push({ text: text, timestamp: Date.now() })
|
|
||||||
pluginService?.savePluginState(pluginId, "notes", notes)
|
|
||||||
itemsChanged()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.
|
|
||||||
|
|
||||||
## Settings for Trigger Configuration
|
|
||||||
|
|
||||||
Provide a PluginSettings component for trigger customization:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myLauncher"
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "trigger"
|
|
||||||
label: "Trigger"
|
|
||||||
description: "Type this prefix to activate the launcher plugin"
|
|
||||||
placeholder: "#"
|
|
||||||
defaultValue: "#"
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "noTrigger"
|
|
||||||
label: "Always Visible"
|
|
||||||
description: "Show items alongside regular apps (no trigger needed)"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Example
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var pluginService: null
|
|
||||||
property string trigger: "!"
|
|
||||||
|
|
||||||
signal itemsChanged()
|
|
||||||
|
|
||||||
property var commands: [
|
|
||||||
{ name: "Lock Screen", icon: "material:lock",
|
|
||||||
comment: "Lock the session", action: "exec:loginctl lock-session" },
|
|
||||||
{ name: "Screenshot", icon: "material:screenshot_monitor",
|
|
||||||
comment: "Take a screenshot", action: "exec:grim" },
|
|
||||||
{ name: "File Manager", icon: "material:folder",
|
|
||||||
comment: "Open file manager", action: "exec:nautilus" }
|
|
||||||
]
|
|
||||||
|
|
||||||
function getItems(query) {
|
|
||||||
if (!query) return commands
|
|
||||||
const q = query.toLowerCase()
|
|
||||||
return commands.filter(c =>
|
|
||||||
c.name.toLowerCase().includes(q) ||
|
|
||||||
c.comment.toLowerCase().includes(q)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeItem(item) {
|
|
||||||
const [type, ...rest] = item.action.split(":")
|
|
||||||
const data = rest.join(":")
|
|
||||||
if (type === "exec") {
|
|
||||||
Quickshell.execDetached(data.split(" "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (pluginService) {
|
|
||||||
trigger = pluginService.loadPluginData("quickCommands", "trigger", "!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
# Plugin Manifest Reference (plugin.json)
|
|
||||||
|
|
||||||
## Required Fields
|
|
||||||
|
|
||||||
| Field | Type | Description | Validation |
|
|
||||||
|-------|------|-------------|------------|
|
|
||||||
| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |
|
|
||||||
| `name` | string | Human-readable name | 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.-]+)?$` |
|
|
||||||
| `author` | string | Creator name or email | Non-empty |
|
|
||||||
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop` |
|
|
||||||
| `capabilities` | array | Plugin capabilities | At least 1 string item |
|
|
||||||
| `component` | string | Path to main QML file | Must start with `./`, end with `.qml` |
|
|
||||||
|
|
||||||
## Conditional Requirements
|
|
||||||
|
|
||||||
| Condition | Required Field | Description |
|
|
||||||
|-----------|---------------|-------------|
|
|
||||||
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
|
|
||||||
|
|
||||||
## Optional Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
|
|
||||||
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
|
|
||||||
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
|
|
||||||
| `requires` | array | System tool dependencies (e.g., `["curl", "jq"]`) |
|
|
||||||
| `permissions` | array | Required permissions |
|
|
||||||
| `trigger` | string | Launcher trigger string (required for launcher type) |
|
|
||||||
|
|
||||||
## Permissions
|
|
||||||
|
|
||||||
| Permission | Description | Enforced |
|
|
||||||
|------------|-------------|----------|
|
|
||||||
| `settings_read` | Read plugin configuration | No (not currently enforced) |
|
|
||||||
| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |
|
|
||||||
| `process` | Execute system commands | No (not currently enforced) |
|
|
||||||
| `network` | Network access | No (not currently enforced) |
|
|
||||||
|
|
||||||
If your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
|
|
||||||
Capabilities are free-form strings that describe what the plugin does. Common values:
|
|
||||||
|
|
||||||
- `dankbar-widget` - general bar widget
|
|
||||||
- `control-center` - integrates with Control Center
|
|
||||||
- `monitoring` - system/service monitoring
|
|
||||||
- `launcher` - launcher search provider
|
|
||||||
- `desktop-widget` - desktop background widget
|
|
||||||
- `ai` - AI/LLM integration
|
|
||||||
- `slideout` - uses slideout panel
|
|
||||||
|
|
||||||
## Complete Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "myPlugin",
|
|
||||||
"name": "My Plugin",
|
|
||||||
"description": "A sample plugin demonstrating all fields",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Developer Name",
|
|
||||||
"type": "widget",
|
|
||||||
"capabilities": ["dankbar-widget", "control-center"],
|
|
||||||
"component": "./MyWidget.qml",
|
|
||||||
"icon": "extension",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"requires_dms": ">=0.1.18",
|
|
||||||
"requires": ["curl", "jq"],
|
|
||||||
"permissions": ["settings_read", "settings_write", "process", "network"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Launcher Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "myLauncher",
|
|
||||||
"name": "My Launcher",
|
|
||||||
"description": "Search and execute custom actions",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Developer Name",
|
|
||||||
"type": "launcher",
|
|
||||||
"capabilities": ["launcher"],
|
|
||||||
"component": "./MyLauncher.qml",
|
|
||||||
"trigger": "#",
|
|
||||||
"icon": "search",
|
|
||||||
"settings": "./Settings.qml",
|
|
||||||
"requires_dms": ">=0.1.18",
|
|
||||||
"permissions": ["settings_read", "settings_write"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON Schema
|
|
||||||
|
|
||||||
The complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Using python
|
|
||||||
python3 -c "
|
|
||||||
import json, jsonschema
|
|
||||||
schema = json.load(open('path/to/plugin-schema.json'))
|
|
||||||
manifest = json.load(open('plugin.json'))
|
|
||||||
jsonschema.validate(manifest, schema)
|
|
||||||
print('Valid!')
|
|
||||||
"
|
|
||||||
|
|
||||||
# Using jq (syntax check only)
|
|
||||||
jq . plugin.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional Properties
|
|
||||||
|
|
||||||
The schema allows additional properties (`"additionalProperties": true`), so plugins can include custom fields. Common custom fields seen in production plugins:
|
|
||||||
|
|
||||||
- `viewMode` - launcher display mode (`"tile"` for image grids)
|
|
||||||
- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# PopoutService Reference
|
|
||||||
|
|
||||||
The `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Declare the property in your component for injection to work:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
property var popoutService: null
|
|
||||||
```
|
|
||||||
|
|
||||||
Without this declaration, injection fails with: `Cannot assign to non-existent property "popoutService"`
|
|
||||||
|
|
||||||
## Popouts (DankPopout-based)
|
|
||||||
|
|
||||||
| Component | Open | Close | Toggle |
|
|
||||||
|-----------|------|-------|--------|
|
|
||||||
| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |
|
|
||||||
| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |
|
|
||||||
| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |
|
|
||||||
| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |
|
|
||||||
| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |
|
|
||||||
| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |
|
|
||||||
| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |
|
|
||||||
| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |
|
|
||||||
|
|
||||||
## Modals (DankModal-based)
|
|
||||||
|
|
||||||
| Modal | Show | Hide | Notes |
|
|
||||||
|-------|------|------|-------|
|
|
||||||
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
|
|
||||||
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |
|
|
||||||
| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |
|
|
||||||
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
|
|
||||||
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |
|
|
||||||
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |
|
|
||||||
| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |
|
|
||||||
| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |
|
|
||||||
| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |
|
|
||||||
|
|
||||||
## Slideouts
|
|
||||||
|
|
||||||
| Component | Open | Close | Toggle |
|
|
||||||
|-----------|------|-------|--------|
|
|
||||||
| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Simple toggle
|
|
||||||
|
|
||||||
```qml
|
|
||||||
MouseArea {
|
|
||||||
onClicked: popoutService?.toggleControlCenter()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conditional popout
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Connections {
|
|
||||||
target: BatteryService
|
|
||||||
function onPercentageChanged() {
|
|
||||||
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
|
|
||||||
popoutService?.openBattery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Context menu with multiple actions
|
|
||||||
|
|
||||||
```qml
|
|
||||||
MouseArea {
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (mouse.button === Qt.RightButton) contextMenu.popup()
|
|
||||||
else popoutService?.toggleControlCenter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: contextMenu
|
|
||||||
MenuItem { text: "Settings"; onClicked: popoutService?.openSettings() }
|
|
||||||
MenuItem { text: "Notifications"; onClicked: popoutService?.toggleNotificationCenter() }
|
|
||||||
MenuItem { text: "Power"; onClicked: popoutService?.openPowerMenu() }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Position-aware toggle (from bar pill)
|
|
||||||
|
|
||||||
Some toggle functions accept position parameters for proper popout placement:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
pillClickAction: (x, y, width, section, screen) => {
|
|
||||||
popoutService?.toggleControlCenter(x, y, width, section, screen)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Always use optional chaining** (`?.`) - the service may not be injected yet
|
|
||||||
2. **Check feature availability** before opening feature-specific popouts:
|
|
||||||
```qml
|
|
||||||
if (BatteryService.batteryAvailable) {
|
|
||||||
popoutService?.openBattery()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. **Lazy loading** - first access may activate lazy loaders; this is normal
|
|
||||||
4. **Popouts are shared** - avoid opening conflicting popouts simultaneously
|
|
||||||
5. **User intent** - only trigger popouts from user actions or critical system events
|
|
||||||
6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters
|
|
||||||
|
|
||||||
## Injection Locations
|
|
||||||
|
|
||||||
The service is injected at these points:
|
|
||||||
- `DMSShell.qml` - daemon plugins
|
|
||||||
- `WidgetHost.qml` - widget plugins in left/right bar sections
|
|
||||||
- `CenterSection.qml` - center bar widgets
|
|
||||||
- `PluginsTab.qml` - settings components
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
# Settings Components Reference
|
|
||||||
|
|
||||||
All plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.
|
|
||||||
|
|
||||||
## PluginSettings Wrapper
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "yourPlugin" // Required: must match plugin.json id
|
|
||||||
|
|
||||||
// Setting components go here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** The plugin must declare `"permissions": ["settings_write"]` in plugin.json for the settings UI to render. Without it, users see an error.
|
|
||||||
|
|
||||||
**PluginSettings provides to children:**
|
|
||||||
- `saveValue(key, value)` - save a setting value
|
|
||||||
- `loadValue(key, defaultValue)` - load a setting value
|
|
||||||
- `saveState(key, value)` - save plugin state (separate file)
|
|
||||||
- `loadState(key, defaultValue)` - load plugin state
|
|
||||||
- `clearState()` - clear all plugin state
|
|
||||||
- Variant management functions (for variant plugins)
|
|
||||||
|
|
||||||
## StringSetting
|
|
||||||
|
|
||||||
Text input field.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "apiKey" // Required: storage key
|
|
||||||
label: "API Key" // Required: display label
|
|
||||||
description: "Your API key" // Optional: help text
|
|
||||||
placeholder: "sk-..." // Optional: input placeholder
|
|
||||||
defaultValue: "" // Optional: default (default: "")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Layout:** Vertical stack - label, description, input field.
|
|
||||||
|
|
||||||
## ToggleSetting
|
|
||||||
|
|
||||||
Boolean toggle switch.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "notifications" // Required: storage key
|
|
||||||
label: "Enable Notifications" // Required: display label
|
|
||||||
description: "Show alerts" // Optional: help text
|
|
||||||
defaultValue: true // Optional: default (default: false)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Layout:** Horizontal - label/description on left, toggle on right.
|
|
||||||
|
|
||||||
## SelectionSetting
|
|
||||||
|
|
||||||
Dropdown menu.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
SelectionSetting {
|
|
||||||
settingKey: "theme" // Required: storage key
|
|
||||||
label: "Theme" // Required: display label
|
|
||||||
description: "Color scheme" // Optional: help text
|
|
||||||
options: [ // Required: array of options
|
|
||||||
{ label: "Dark", value: "dark" },
|
|
||||||
{ label: "Light", value: "light" },
|
|
||||||
{ label: "Auto", value: "auto" }
|
|
||||||
]
|
|
||||||
defaultValue: "dark" // Optional: default value
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Options can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.
|
|
||||||
|
|
||||||
**Layout:** Horizontal - label/description on left, dropdown on right.
|
|
||||||
|
|
||||||
**Reacting to changes:**
|
|
||||||
```qml
|
|
||||||
SelectionSetting {
|
|
||||||
settingKey: "updateInterval"
|
|
||||||
label: "Update Interval"
|
|
||||||
options: [
|
|
||||||
{ label: "1 minute", value: "60" },
|
|
||||||
{ label: "5 minutes", value: "300" }
|
|
||||||
]
|
|
||||||
defaultValue: "300"
|
|
||||||
onValueChanged: (newValue) => {
|
|
||||||
console.log("Interval changed to:", newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## SliderSetting
|
|
||||||
|
|
||||||
Numeric slider with min/max.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
SliderSetting {
|
|
||||||
settingKey: "opacity" // Required: storage key
|
|
||||||
label: "Opacity" // Required: display label
|
|
||||||
description: "Background" // Optional: help text
|
|
||||||
defaultValue: 85 // Optional: default value
|
|
||||||
minimum: 0 // Required: min value
|
|
||||||
maximum: 100 // Required: max value
|
|
||||||
unit: "%" // Optional: unit label shown after value
|
|
||||||
leftIcon: "dark_mode" // Optional: Material icon on left
|
|
||||||
rightIcon: "light_mode" // Optional: Material icon on right
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ColorSetting
|
|
||||||
|
|
||||||
Color picker.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
ColorSetting {
|
|
||||||
settingKey: "accentColor" // Required: storage key
|
|
||||||
label: "Accent Color" // Required: display label
|
|
||||||
description: "Custom accent" // Optional: help text
|
|
||||||
defaultValue: "#ff5722" // Optional: default hex color
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Displays a color swatch that opens a color picker dialog.
|
|
||||||
|
|
||||||
## ListSetting
|
|
||||||
|
|
||||||
Manage a list of items with manual add/remove. Use when you need custom UI for adding items.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
ListSetting {
|
|
||||||
id: itemList
|
|
||||||
settingKey: "items" // Required: storage key
|
|
||||||
label: "Saved Items" // Required: display label
|
|
||||||
description: "Your items" // Optional: help text
|
|
||||||
defaultValue: [] // Optional: default array
|
|
||||||
delegate: Component { // Optional: custom item display
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: modelData.name
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 60
|
|
||||||
height: 28
|
|
||||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Remove"
|
|
||||||
color: Theme.errorText
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: removeArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: itemList.removeItem(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Methods:**
|
|
||||||
- `addItem(item)` - add an item to the list
|
|
||||||
- `removeItem(index)` - remove item at index
|
|
||||||
|
|
||||||
## ListSettingWithInput
|
|
||||||
|
|
||||||
Complete list management with built-in form. Best for collecting structured data.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
ListSettingWithInput {
|
|
||||||
settingKey: "locations" // Required: storage key
|
|
||||||
label: "Locations" // Required: display label
|
|
||||||
description: "Track zones" // Optional: help text
|
|
||||||
defaultValue: [] // Optional: default array
|
|
||||||
fields: [ // Required: field definitions
|
|
||||||
{
|
|
||||||
id: "name", // Required: key in saved object
|
|
||||||
label: "Name", // Required: column header
|
|
||||||
placeholder: "Home", // Optional: input placeholder
|
|
||||||
width: 150, // Optional: column width (default: 200)
|
|
||||||
required: true // Optional: must have value to add
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "timezone",
|
|
||||||
label: "Timezone",
|
|
||||||
placeholder: "America/New_York",
|
|
||||||
width: 200,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Automatically generates: column headers, input fields, add button with validation, list display, remove buttons.
|
|
||||||
|
|
||||||
## Mixing Custom UI with Settings
|
|
||||||
|
|
||||||
You can interleave regular QML elements with setting components:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginSettings {
|
|
||||||
pluginId: "myPlugin"
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "General Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
StringSetting {
|
|
||||||
settingKey: "name"
|
|
||||||
label: "Display Name"
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: "Advanced Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
topPadding: Theme.spacingL
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSetting {
|
|
||||||
settingKey: "debug"
|
|
||||||
label: "Debug Mode"
|
|
||||||
defaultValue: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Default Values
|
|
||||||
|
|
||||||
Define sensible defaults in every setting component. The default is used when no saved value exists:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
StringSetting { settingKey: "text"; defaultValue: "Hello" }
|
|
||||||
ToggleSetting { settingKey: "enabled"; defaultValue: true }
|
|
||||||
SelectionSetting { settingKey: "mode"; defaultValue: "auto" }
|
|
||||||
SliderSetting { settingKey: "opacity"; defaultValue: 85 }
|
|
||||||
ColorSetting { settingKey: "color"; defaultValue: "#ff5722" }
|
|
||||||
ListSetting { settingKey: "items"; defaultValue: [] }
|
|
||||||
ListSettingWithInput { settingKey: "data"; defaultValue: [] }
|
|
||||||
```
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
# Theme Property Reference
|
|
||||||
|
|
||||||
All theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.
|
|
||||||
|
|
||||||
## Font Sizes
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.fontSizeSmall // 12px (scaled by SettingsData.fontScale)
|
|
||||||
Theme.fontSizeMedium // 14px (scaled)
|
|
||||||
Theme.fontSizeLarge // 16px (scaled)
|
|
||||||
Theme.fontSizeXLarge // 20px (scaled)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Icon Sizes
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.iconSizeSmall // 16px
|
|
||||||
Theme.iconSize // 24px (default)
|
|
||||||
Theme.iconSizeLarge // 32px
|
|
||||||
```
|
|
||||||
|
|
||||||
## Spacing
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.spacingXS // Extra small
|
|
||||||
Theme.spacingS // Small
|
|
||||||
Theme.spacingM // Medium
|
|
||||||
Theme.spacingL // Large
|
|
||||||
Theme.spacingXL // Extra large
|
|
||||||
```
|
|
||||||
|
|
||||||
## Border Radius
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.cornerRadius // Standard
|
|
||||||
Theme.cornerRadiusSmall // Smaller
|
|
||||||
Theme.cornerRadiusLarge // Larger
|
|
||||||
```
|
|
||||||
|
|
||||||
## Surface Colors
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.surface
|
|
||||||
Theme.surfaceContainerLow
|
|
||||||
Theme.surfaceContainer
|
|
||||||
Theme.surfaceContainerHigh
|
|
||||||
Theme.surfaceContainerHighest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Text Colors
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.onSurface // Primary text on surface
|
|
||||||
Theme.onSurfaceVariant // Secondary text on surface
|
|
||||||
Theme.surfaceText // Alias for primary surface text
|
|
||||||
Theme.surfaceVariantText // Alias for secondary surface text
|
|
||||||
Theme.outline // Border/divider color
|
|
||||||
```
|
|
||||||
|
|
||||||
## Semantic Colors
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.primary
|
|
||||||
Theme.onPrimary
|
|
||||||
Theme.secondary
|
|
||||||
Theme.onSecondary
|
|
||||||
Theme.error
|
|
||||||
Theme.errorHover
|
|
||||||
Theme.errorText
|
|
||||||
Theme.warning
|
|
||||||
Theme.success
|
|
||||||
```
|
|
||||||
|
|
||||||
## Special Functions
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Theme.popupBackground() // Popup background with proper opacity
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Widget Patterns
|
|
||||||
|
|
||||||
### Icon with Text
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "icon_name"
|
|
||||||
color: Theme.onSurface
|
|
||||||
font.pixelSize: Theme.iconSize
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Label"
|
|
||||||
color: Theme.onSurface
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container with Border
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Rectangle {
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hover Effect
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Rectangle {
|
|
||||||
id: container
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)
|
|
||||||
onExited: container.color = Theme.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clickable Pill
|
|
||||||
|
|
||||||
```qml
|
|
||||||
StyledRect {
|
|
||||||
width: content.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: parent.widgetThickness
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: mouseArea.containsMouse
|
|
||||||
? Qt.lighter(Theme.surfaceContainerHigh, 1.1)
|
|
||||||
: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: content
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "star"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Label"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Mistakes
|
|
||||||
|
|
||||||
**Wrong property names** (these do NOT exist):
|
|
||||||
```qml
|
|
||||||
Theme.fontSizeS // Use Theme.fontSizeSmall
|
|
||||||
Theme.iconSizeS // Use Theme.iconSizeSmall
|
|
||||||
Theme.spacingSmall // Use Theme.spacingS
|
|
||||||
Theme.borderRadius // Use Theme.cornerRadius
|
|
||||||
```
|
|
||||||
|
|
||||||
**Hardcoded values** (do NOT do this):
|
|
||||||
```qml
|
|
||||||
color: "#1e1e1e" // Use Theme.surfaceContainerHigh
|
|
||||||
color: "white" // Use Theme.surfaceText
|
|
||||||
font.pixelSize: 14 // Use Theme.fontSizeMedium
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Widgets from qs.Widgets
|
|
||||||
|
|
||||||
| Widget | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `StyledText` | Themed text with proper color defaults |
|
|
||||||
| `StyledRect` | Themed rectangle |
|
|
||||||
| `DankIcon` | Material Symbols icon renderer |
|
|
||||||
| `DankNFIcon` | Nerd Font icon renderer |
|
|
||||||
| `DankButton` | Themed button |
|
|
||||||
| `DankToggle` | Toggle switch |
|
|
||||||
| `DankTextField` | Text input field |
|
|
||||||
| `DankSlider` | Slider control |
|
|
||||||
| `DankDropdown` | Dropdown menu |
|
|
||||||
| `DankGridView` | Grid layout view |
|
|
||||||
| `DankListView` | List layout view |
|
|
||||||
| `DankFlickable` | Scrollable container |
|
|
||||||
| `DankTabBar` | Tab bar navigation |
|
|
||||||
| `DankCollapsibleSection` | Collapsible content section |
|
|
||||||
| `DankTooltip` | Hover tooltip |
|
|
||||||
| `DankNumberStepper` | Number +/- control |
|
|
||||||
| `DankFilterChips` | Filter chip row |
|
|
||||||
| `CachingImage` | Image with disk cache |
|
|
||||||
| `NumericText` | Fixed-width numeric display |
|
|
||||||
|
|
||||||
## Checking All Properties
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep "property" Common/Theme.qml
|
|
||||||
```
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
# Widget Plugin Guide
|
|
||||||
|
|
||||||
Widgets are bar plugins that display pills in DankBar, optionally open popouts, and can integrate with the Control Center.
|
|
||||||
|
|
||||||
## Base Component
|
|
||||||
|
|
||||||
Widgets use `PluginComponent` from `qs.Modules.Plugins`.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
horizontalBarPill: Component { /* ... */ }
|
|
||||||
verticalBarPill: Component { /* ... */ }
|
|
||||||
popoutContent: Component { /* ... */ }
|
|
||||||
popoutWidth: 400
|
|
||||||
popoutHeight: 300
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Injected Properties
|
|
||||||
|
|
||||||
These are automatically set by the plugin host:
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `axis` | object | Bar axis info (horizontal/vertical) |
|
|
||||||
| `section` | string | Bar section: `"left"`, `"center"`, or `"right"` |
|
|
||||||
| `parentScreen` | object | Screen reference for multi-monitor |
|
|
||||||
| `widgetThickness` | real | Widget size perpendicular to bar edge |
|
|
||||||
| `barThickness` | real | Bar thickness parallel to edge |
|
|
||||||
| `pluginId` | string | This plugin's ID |
|
|
||||||
| `pluginService` | object | PluginService reference |
|
|
||||||
| `pluginData` | object | Reactive plugin settings data |
|
|
||||||
|
|
||||||
## Bar Pills
|
|
||||||
|
|
||||||
Define `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).
|
|
||||||
|
|
||||||
### Horizontal Bar Pill
|
|
||||||
|
|
||||||
```qml
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: content.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: parent.widgetThickness
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: content
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "star"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Label"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Vertical Bar Pill
|
|
||||||
|
|
||||||
```qml
|
|
||||||
verticalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: parent.widgetThickness
|
|
||||||
height: content.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: content
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "star"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.iconSizeSmall
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.
|
|
||||||
|
|
||||||
## Popout Content
|
|
||||||
|
|
||||||
Open a popout window when the bar pill is clicked:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
popoutWidth: 400
|
|
||||||
popoutHeight: 300
|
|
||||||
|
|
||||||
popoutContent: Component {
|
|
||||||
PopoutComponent {
|
|
||||||
headerText: "My Plugin"
|
|
||||||
detailsText: "Optional subtitle"
|
|
||||||
showCloseButton: true
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Content here"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**PopoutComponent properties:**
|
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
|
||||||
|----------|------|---------|-------------|
|
|
||||||
| `headerText` | string | `""` | Main header (bold, large). Hidden if empty. |
|
|
||||||
| `detailsText` | string | `""` | Subtitle below header. Hidden if empty. |
|
|
||||||
| `showCloseButton` | bool | `false` | Show X button in top-right corner. |
|
|
||||||
| `closePopout` | function | (injected) | Call to close the popout programmatically. |
|
|
||||||
| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |
|
|
||||||
| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |
|
|
||||||
|
|
||||||
**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
|
||||||
|
|
||||||
## Custom Click Actions
|
|
||||||
|
|
||||||
Override the default popout behavior:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
// Simple no-args handler
|
|
||||||
pillClickAction: () => {
|
|
||||||
popoutService?.toggleControlCenter()
|
|
||||||
}
|
|
||||||
|
|
||||||
// With position params (x, y, width, section, screen)
|
|
||||||
pillClickAction: (x, y, width, section, screen) => {
|
|
||||||
popoutService?.toggleControlCenter(x, y, width, section, screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
pillRightClickAction: () => {
|
|
||||||
popoutService?.openSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Control Center Integration
|
|
||||||
|
|
||||||
Add CC properties to show your widget in the Control Center grid:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
ccWidgetIcon: "toggle_on"
|
|
||||||
ccWidgetPrimaryText: "Feature Name"
|
|
||||||
ccWidgetSecondaryText: isActive ? "Active" : "Off"
|
|
||||||
ccWidgetIsActive: isActive
|
|
||||||
|
|
||||||
onCcWidgetToggled: {
|
|
||||||
isActive = !isActive
|
|
||||||
pluginService?.savePluginData(pluginId, "active", isActive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**CC properties:**
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `ccWidgetIcon` | string | Material icon name |
|
|
||||||
| `ccWidgetPrimaryText` | string | Main label |
|
|
||||||
| `ccWidgetSecondaryText` | string | Subtitle / status text |
|
|
||||||
| `ccWidgetIsActive` | bool | Active state (changes styling) |
|
|
||||||
|
|
||||||
**CC signals:**
|
|
||||||
|
|
||||||
| Signal | When fired |
|
|
||||||
|--------|-----------|
|
|
||||||
| `ccWidgetToggled()` | Icon area clicked |
|
|
||||||
| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |
|
|
||||||
|
|
||||||
**CC sizing rules:**
|
|
||||||
- 25% width - SmallToggleButton (icon only)
|
|
||||||
- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)
|
|
||||||
- Users can resize in CC edit mode
|
|
||||||
|
|
||||||
### Detail Content (CompoundPill)
|
|
||||||
|
|
||||||
Add an expandable panel below the CC widget:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
ccDetailContent: Component {
|
|
||||||
Rectangle {
|
|
||||||
implicitHeight: 200
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
// Detail UI here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Visibility Control
|
|
||||||
|
|
||||||
Conditionally show/hide the bar pill:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
visibilityCommand: "pgrep -x myapp"
|
|
||||||
visibilityInterval: 5000 // check every 5 seconds
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Popout Namespace
|
|
||||||
|
|
||||||
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
layerNamespacePlugin: true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reading Plugin Data
|
|
||||||
|
|
||||||
Access saved settings reactively via the injected `pluginData`:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PluginComponent {
|
|
||||||
property string displayText: pluginData?.text || "Default"
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: pluginService
|
|
||||||
function onPluginDataChanged(changedId) {
|
|
||||||
if (changedId === pluginId)
|
|
||||||
displayText = pluginService.loadPluginData(pluginId, "text", "Default")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Example
|
|
||||||
|
|
||||||
Based on the ExampleEmojiPlugin pattern:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Services
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
|
|
||||||
PluginComponent {
|
|
||||||
id: root
|
|
||||||
property var popoutService: null
|
|
||||||
|
|
||||||
property var emojis: ["star", "heart", "smile"]
|
|
||||||
property int currentIndex: 0
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 2000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: currentIndex = (currentIndex + 1) % emojis.length
|
|
||||||
}
|
|
||||||
|
|
||||||
popoutWidth: 350
|
|
||||||
popoutHeight: 400
|
|
||||||
|
|
||||||
horizontalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: label.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: parent.widgetThickness
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.emojis[root.currentIndex]
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalBarPill: Component {
|
|
||||||
StyledRect {
|
|
||||||
width: parent.widgetThickness
|
|
||||||
height: label.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainerHigh
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: label
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.emojis[root.currentIndex]
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popoutContent: Component {
|
|
||||||
PopoutComponent {
|
|
||||||
headerText: "Emoji Picker"
|
|
||||||
showCloseButton: true
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
width: parent.width
|
|
||||||
height: 300
|
|
||||||
cellWidth: 50
|
|
||||||
cellHeight: 50
|
|
||||||
model: root.emojis
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: 48
|
|
||||||
height: 48
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: 24
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: {
|
|
||||||
Quickshell.execDetached(["dms", "cl", "copy", modelData])
|
|
||||||
ToastService?.showInfo("Copied " + modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,32 +7,32 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## DankMaterialShell Bug Report
|
## DankMaterialShell Bug Report
|
||||||
Limit your report to one issue per submission unless similarly related
|
Limit your report to one issue per submission unless closely related
|
||||||
- type: dropdown
|
- type: checkboxes
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor
|
label: Compositor
|
||||||
options:
|
options:
|
||||||
- Niri
|
- label: Niri
|
||||||
- Hyprland
|
- label: Hyprland
|
||||||
- MangoWC (dwl)
|
- label: MangoWC (dwl)
|
||||||
- Sway
|
- label: Sway
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: checkboxes
|
||||||
id: distribution
|
id: distribution
|
||||||
attributes:
|
attributes:
|
||||||
label: Distribution
|
label: Distribution
|
||||||
options:
|
options:
|
||||||
- Arch Linux
|
- label: Arch Linux
|
||||||
- CachyOS
|
- label: CachyOS
|
||||||
- Fedora
|
- label: Fedora
|
||||||
- NixOS
|
- label: NixOS
|
||||||
- Debian
|
- label: Debian
|
||||||
- Ubuntu
|
- label: Ubuntu
|
||||||
- Gentoo
|
- label: Gentoo
|
||||||
- OpenSUSE
|
- label: OpenSUSE
|
||||||
- Other (specify below)
|
- label: Other (specify below)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@@ -42,45 +42,12 @@ body:
|
|||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: dropdown
|
|
||||||
id: installation_method
|
|
||||||
attributes:
|
|
||||||
label: Select your Installation Method
|
|
||||||
options:
|
|
||||||
- DankInstaller
|
|
||||||
- Distro Packaging
|
|
||||||
- Source
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: original_installation_method
|
|
||||||
attributes:
|
|
||||||
label: Was this your original Installation method?
|
|
||||||
options:
|
|
||||||
- "Yes"
|
|
||||||
- No (specify below)
|
|
||||||
default: 0
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: original_installation_method_specify
|
|
||||||
attributes:
|
|
||||||
label: If no, specify
|
|
||||||
placeholder: e.g., Distro Packaging, then Source
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -vC
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
|
description: Output of `dms doctor -vC` command
|
||||||
placeholder: Paste the output of `dms doctor -vC` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
value: |
|
|
||||||
<details>
|
|
||||||
<summary>Click to expand</summary>
|
|
||||||
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -102,7 +69,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps_to_reproduce
|
id: steps_to_reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce
|
label: Steps to Reproduce & Installation Method
|
||||||
description: Please provide detailed steps to reproduce the issue
|
description: Please provide detailed steps to reproduce the issue
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. ...
|
1. ...
|
||||||
|
|||||||
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -23,25 +23,18 @@ body:
|
|||||||
placeholder: Why is this feature important?
|
placeholder: Why is this feature important?
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: dropdown
|
- type: checkboxes
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor(s)
|
label: Compositor(s)
|
||||||
description: Is this feature specific to one or more compositors?
|
description: Is this feature specific to one or more compositors?
|
||||||
options:
|
options:
|
||||||
- All compositors
|
- label: All compositors
|
||||||
- Niri
|
- label: Niri
|
||||||
- Hyprland
|
- label: Hyprland
|
||||||
- MangoWC (dwl)
|
- label: MangoWC (dwl)
|
||||||
- Sway
|
- label: Sway
|
||||||
- Other (specify below)
|
- label: Other (specify below)
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: compositor_other
|
|
||||||
attributes:
|
|
||||||
label: If Other, please specify
|
|
||||||
placeholder: e.g., Wayfire, Mutter, etc.
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
75
.github/ISSUE_TEMPLATE/support_request.yml
vendored
75
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -7,87 +7,32 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## DankMaterialShell Support Request
|
## DankMaterialShell Support Request
|
||||||
- type: dropdown
|
- type: checkboxes
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor
|
label: Compositor
|
||||||
options:
|
options:
|
||||||
- Niri
|
- label: Niri
|
||||||
- Hyprland
|
- label: Hyprland
|
||||||
- MangoWC (dwl)
|
- label: MangoWC (dwl)
|
||||||
- Sway
|
- label: Sway
|
||||||
- Other (specify below)
|
- label: Other (specify below)
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: compositor_other
|
|
||||||
attributes:
|
|
||||||
label: If Other, please specify
|
|
||||||
placeholder: e.g., Wayfire, Mutter, etc.
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: dropdown
|
- type: input
|
||||||
id: distribution
|
id: distribution
|
||||||
attributes:
|
attributes:
|
||||||
label: Distribution
|
label: Distribution
|
||||||
options:
|
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
||||||
- Arch Linux
|
placeholder: Your Linux distribution
|
||||||
- CachyOS
|
|
||||||
- Fedora
|
|
||||||
- NixOS
|
|
||||||
- Debian
|
|
||||||
- Ubuntu
|
|
||||||
- Gentoo
|
|
||||||
- OpenSUSE
|
|
||||||
- Other (specify below)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: distribution_other
|
|
||||||
attributes:
|
|
||||||
label: If Other, please specify
|
|
||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: dropdown
|
|
||||||
id: installation_method
|
|
||||||
attributes:
|
|
||||||
label: Select your Installation Method
|
|
||||||
options:
|
|
||||||
- DankInstaller
|
|
||||||
- Distro Packaging
|
|
||||||
- Source
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: original_installation_method_different
|
|
||||||
attributes:
|
|
||||||
label: Was your original Installation method different?
|
|
||||||
options:
|
|
||||||
- "Yes"
|
|
||||||
- No (specify below)
|
|
||||||
default: 0
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: original_installation_method_specify
|
|
||||||
attributes:
|
|
||||||
label: If no, specify
|
|
||||||
placeholder: e.g., Distro Packaging, then Source
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -vC
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
description: Output of `dms doctor -vC` command
|
||||||
placeholder: Paste the output of `dms doctor -vC` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
value: |
|
|
||||||
<details>
|
|
||||||
<summary>Click to expand</summary>
|
|
||||||
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
383
.github/workflows/backup/run-obs.yml.bak
vendored
Normal file
383
.github/workflows/backup/run-obs.yml.bak
vendored
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
name: Update OBS Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: "Package to update (dms, dms-git, or all)"
|
||||||
|
required: false
|
||||||
|
default: "all"
|
||||||
|
force_upload:
|
||||||
|
description: "Force upload without version check"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "false"
|
||||||
|
- "true"
|
||||||
|
rebuild_release:
|
||||||
|
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION (always update)"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match spec format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Check OBS for last uploaded commit
|
||||||
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
|
mkdir -p "$OBS_BASE"
|
||||||
|
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||||
|
|
||||||
|
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||||
|
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||||
|
osc up -q 2>/dev/null || true
|
||||||
|
|
||||||
|
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||||
|
if [[ -f "dms-git.spec" ]]; then
|
||||||
|
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$OBS_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No spec file in OBS, proceeding with update"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 First upload to OBS, update needed"
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
||||||
|
PKG="${{ github.event.inputs.package }}"
|
||||||
|
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
||||||
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🚀 Force upload: all packages"
|
||||||
|
else
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🚀 Force upload: $PKG"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
update-obs:
|
||||||
|
name: Upload to OBS
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
if: |
|
||||||
|
github.event.inputs.force_upload == 'true' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Generate GitHub App Token
|
||||||
|
id: generate_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Check if last commit was automated
|
||||||
|
id: check-loop
|
||||||
|
run: |
|
||||||
|
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
||||||
|
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
||||||
|
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "✅ Last commit was not automated, proceeding"
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Determine packages to update
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: updating git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update dms-git spec version
|
||||||
|
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
||||||
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Update version in spec
|
||||||
|
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
# Add changelog entry
|
||||||
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
|
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
|
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
- name: Update Debian dms-git changelog version
|
||||||
|
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
||||||
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
# Debian version format: 0.6.2+git2256.9162e314
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
|
||||||
|
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
# Get current version from changelog
|
||||||
|
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||||
|
|
||||||
|
echo "Current Debian version: $CURRENT_VERSION"
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Only update if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||||
|
# Create new changelog entry at top
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
|
||||||
|
cat > "$TEMP_CHANGELOG" << EOF
|
||||||
|
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||||
|
|
||||||
|
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Prepend to existing changelog
|
||||||
|
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||||
|
else
|
||||||
|
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update dms stable version
|
||||||
|
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
echo "Updating packaging to version $VERSION_NO_V"
|
||||||
|
|
||||||
|
# Update openSUSE dms spec (stable only)
|
||||||
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
|
||||||
|
# Update openSUSE spec changelog
|
||||||
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
|
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
|
||||||
|
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
|
||||||
|
|
||||||
|
# Update Debian _service files (both tar_scm and download_url formats)
|
||||||
|
for service in distro/debian/*/_service; do
|
||||||
|
if [[ -f "$service" ]]; then
|
||||||
|
# Update tar_scm revision parameter (for dms-git)
|
||||||
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
|
||||||
|
# Update download_url paths (for dms stable)
|
||||||
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update Debian changelog for dms stable
|
||||||
|
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
|
||||||
|
cat > "$TEMP_CHANGELOG" << EOF
|
||||||
|
dms ($VERSION_NO_V) stable; urgency=medium
|
||||||
|
|
||||||
|
* Update to $VERSION stable release
|
||||||
|
* Bug fixes and improvements
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
|
||||||
|
|
||||||
|
echo "✓ Updated Debian changelog to $VERSION_NO_V"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.24"
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Upload to OBS
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
||||||
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||||
|
else
|
||||||
|
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get changed packages
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
id: changed-packages
|
||||||
|
run: |
|
||||||
|
# Check if there are any changes to commit
|
||||||
|
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
|
||||||
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog or spec changes to commit"
|
||||||
|
else
|
||||||
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
# Get list of changed packages for commit message
|
||||||
|
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
|
||||||
|
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
|
||||||
|
|
||||||
|
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||||
|
echo "packages=$PKGS" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Changed packages: $PKGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit packaging changes
|
||||||
|
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
|
||||||
|
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
||||||
|
git pull --rebase origin master
|
||||||
|
git push
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||||
298
.github/workflows/backup/run-ppa.yml.bak
vendored
Normal file
298
.github/workflows/backup/run-ppa.yml.bak
vendored
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
name: Update PPA Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||||
|
required: false
|
||||||
|
default: "dms-git"
|
||||||
|
force_upload:
|
||||||
|
description: "Force upload without version check"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "false"
|
||||||
|
- "true"
|
||||||
|
rebuild_release:
|
||||||
|
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
schedule:
|
||||||
|
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match changelog format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Extract commit hash from changelog
|
||||||
|
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||||
|
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||||
|
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog file found, proceeding with upload"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
upload-ppa:
|
||||||
|
name: Upload to PPA
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
if: |
|
||||||
|
github.event.inputs.force_upload == 'true' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Generate GitHub App Token
|
||||||
|
id: generate_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Check if last commit was automated
|
||||||
|
id: check-loop
|
||||||
|
run: |
|
||||||
|
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
||||||
|
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
||||||
|
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "✅ Last commit was not automated, proceeding"
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.24"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
debhelper \
|
||||||
|
devscripts \
|
||||||
|
dput \
|
||||||
|
lftp \
|
||||||
|
build-essential \
|
||||||
|
fakeroot \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
- name: Configure GPG
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "$GPG_KEY" | gpg --import
|
||||||
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Determine packages to upload
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
||||||
|
PKG="${{ github.event.inputs.package }}"
|
||||||
|
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
||||||
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
|
echo "🚀 Force upload: all packages"
|
||||||
|
else
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "🚀 Force upload: $PKG"
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: uploading git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
# Manual package selection should respect change detection
|
||||||
|
SELECTED_PKG="${{ github.event.inputs.package }}"
|
||||||
|
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
|
||||||
|
|
||||||
|
# Check if manually selected package is in the updated list
|
||||||
|
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
|
||||||
|
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "📦 Manual selection (has updates): $SELECTED_PKG"
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload to PPA
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
|
echo "No packages selected for upload. Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build command arguments
|
||||||
|
BUILD_ARGS=()
|
||||||
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
|
BUILD_ARGS+=("$REBUILD_RELEASE")
|
||||||
|
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms to PPA..."
|
||||||
|
if [ -n "$REBUILD_RELEASE" ]; then
|
||||||
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-git to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-greeter to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
|
||||||
|
else
|
||||||
|
# Map package to PPA name
|
||||||
|
case "$PACKAGES" in
|
||||||
|
dms)
|
||||||
|
PPA_NAME="dms"
|
||||||
|
;;
|
||||||
|
dms-git)
|
||||||
|
PPA_NAME="dms-git"
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
PPA_NAME="danklinux"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
PPA_NAME="$PACKAGES"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PACKAGES to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get changed packages
|
||||||
|
if: steps.check-loop.outputs.skip != 'true'
|
||||||
|
id: changed-packages
|
||||||
|
run: |
|
||||||
|
# Check if there are any changelog changes to commit
|
||||||
|
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
|
||||||
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog changes to commit"
|
||||||
|
else
|
||||||
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
# Get list of changed packages for commit message (deduplicate)
|
||||||
|
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||||
|
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Changed packages: $CHANGED"
|
||||||
|
echo "📋 Debug - Changed files:"
|
||||||
|
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit changelog changes
|
||||||
|
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
git add distro/ubuntu/*/debian/changelog
|
||||||
|
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
||||||
|
git pull --rebase origin master
|
||||||
|
git push
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
2
.github/workflows/dms-stable.yml
vendored
2
.github/workflows/dms-stable.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|||||||
4
.github/workflows/go-ci.yml
vendored
4
.github/workflows/go-ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/nix-pr-check.yml
vendored
26
.github/workflows/nix-pr-check.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Nix flake and NixOS tests
|
name: Check nix flake
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -9,35 +9,15 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
check-flake:
|
check-flake:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 120
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Nix
|
- name: Install Nix
|
||||||
uses: cachix/install-nix-action@v31
|
uses: cachix/install-nix-action@v31
|
||||||
with:
|
|
||||||
enable_kvm: true
|
|
||||||
extra_nix_config: |
|
|
||||||
system-features = nixos-test benchmark big-parallel kvm
|
|
||||||
|
|
||||||
- name: Check the flake
|
- name: Check the flake
|
||||||
run: nix flake check -L
|
run: nix flake check
|
||||||
|
|
||||||
- name: Run NixOS module test
|
|
||||||
run: nix build .#nixosTests.x86_64-linux.nixos-module -L
|
|
||||||
|
|
||||||
- name: Run NixOS service start test
|
|
||||||
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
|
|
||||||
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
|
||||||
|
|
||||||
- name: Run niri home-manager module test
|
|
||||||
run: nix build .#nixosTests.x86_64-linux.niri-home-module -L
|
|
||||||
|
|||||||
4
.github/workflows/prek.yml
vendored
4
.github/workflows/prek.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v6
|
# uses: actions/checkout@v4
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
# fetch-depth: 0
|
# fetch-depth: 0
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -192,12 +192,12 @@ jobs:
|
|||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
4
.github/workflows/run-copr.yml
vendored
4
.github/workflows/run-copr.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
|||||||
275
.github/workflows/run-obs.yml
vendored
275
.github/workflows/run-obs.yml
vendored
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -32,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -73,27 +72,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to check dms-greeter stable tag
|
|
||||||
check_dms_greeter_stable() {
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
|
||||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
|
||||||
|
|
||||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
|
||||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
# Run from tag with no package specified - update both stable packages
|
# Tag selected or pushed - always update stable package
|
||||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
@@ -119,18 +103,15 @@ jobs:
|
|||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
# Check each stable package and build list of those needing updates
|
# Check each package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
|
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
PACKAGES_TO_UPDATE+=("dms")
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
|
||||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -139,7 +120,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "✓ Both packages up to date"
|
echo "✓ All packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -163,18 +144,6 @@ jobs:
|
|||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
else
|
||||||
# Unknown package - proceed anyway
|
# Unknown package - proceed anyway
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
@@ -195,18 +164,22 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Wait before OBS upload
|
|
||||||
run: sleep 3
|
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
# Use check-updates outputs when available
|
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
||||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
# Tag selected or pushed - use the tag from GITHUB_REF
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
|
# Check if check-updates already determined a version (from auto-detection)
|
||||||
|
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
# Use version from check-updates job
|
# Use version from check-updates job
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
@@ -218,16 +191,40 @@ jobs:
|
|||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
# Manual workflow dispatch
|
||||||
|
|
||||||
# Determine version for dms stable and dms-greeter using the API
|
# Determine version for dms stable
|
||||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Using latest release from API: $LATEST_TAG"
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
else
|
else
|
||||||
echo "ERROR: Could not fetch latest release from API"
|
# Auto-detect latest release for dms
|
||||||
exit 1
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Auto-detected latest release: $LATEST_TAG"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not auto-detect latest release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
|
# Use github.ref if tag selected, otherwise auto-detect latest
|
||||||
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using tag from GITHUB_REF: $VERSION"
|
||||||
|
else
|
||||||
|
# Auto-detect latest release for "all"
|
||||||
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Auto-detected latest release: $LATEST_TAG"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not auto-detect latest release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -247,7 +244,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
- name: Update dms-git spec version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
run: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
@@ -268,7 +265,7 @@ jobs:
|
|||||||
} > distro/opensuse/dms-git.spec
|
} > distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
- name: Update Debian dms-git changelog version
|
- name: Update Debian dms-git changelog version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
run: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
@@ -286,68 +283,57 @@ jobs:
|
|||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
- name: Update stable version (dms + dms-greeter)
|
- name: Update dms stable version
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
VERSION_NO_V="${VERSION#v}"
|
VERSION_NO_V="${VERSION#v}"
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||||
|
|
||||||
# Update dms spec and changelog when dms is in the upload list
|
# Update spec file
|
||||||
if [[ "$PACKAGES" == *"dms"* ]]; then
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
|
||||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
|
||||||
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
|
|
||||||
|
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
# Verify the update
|
||||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||||
{
|
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
||||||
echo "$LOCAL_SPEC_HEAD"
|
|
||||||
echo "%changelog"
|
|
||||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
|
||||||
echo "- Update to stable $VERSION release"
|
|
||||||
} > distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
# Single changelog entry (full history on OBS website)
|
||||||
CHANGELOG_DATE=$(date -R)
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
{
|
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
{
|
||||||
echo ""
|
echo "$LOCAL_SPEC_HEAD"
|
||||||
echo " * Update to $VERSION stable release"
|
echo "%changelog"
|
||||||
echo ""
|
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo "- Update to stable $VERSION release"
|
||||||
} > "distro/debian/dms/debian/changelog"
|
} > distro/opensuse/dms.spec
|
||||||
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
# Update Debian _service files (both tar_scm and download_url formats)
|
||||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
{
|
|
||||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
|
||||||
echo ""
|
|
||||||
echo " * Update to $VERSION stable release"
|
|
||||||
echo ""
|
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
|
||||||
} > "distro/debian/dms-greeter/debian/changelog"
|
|
||||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update Debian _service files for packages in upload list (download_url paths)
|
|
||||||
for service in distro/debian/*/_service; do
|
for service in distro/debian/*/_service; do
|
||||||
if [[ -f "$service" ]]; then
|
if [[ -f "$service" ]]; then
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
# Update tar_scm revision parameter (for dms-git)
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
# Update download_url paths (for dms, dms-greeter stable)
|
|
||||||
|
# Update download_url paths (for dms stable)
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Update Debian changelog for dms stable (single entry, history on OBS website)
|
||||||
|
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
{
|
||||||
|
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||||
|
echo ""
|
||||||
|
echo " * Update to $VERSION stable release"
|
||||||
|
echo ""
|
||||||
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
|
} > "distro/debian/dms/debian/changelog"
|
||||||
|
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -367,18 +353,7 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
chmod 600 ~/.config/osc/oscrc
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
# Cache OBS bundled Go toolchains
|
|
||||||
- name: Cache OBS bundled Go toolchains (dms-git)
|
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: /home/runner/.cache/dms-obs-go-toolchain
|
|
||||||
key: dms-obs-go-toolchain-${{ runner.os }}-${{ hashFiles('core/go.mod') }}
|
|
||||||
restore-keys: |
|
|
||||||
dms-obs-go-toolchain-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Upload to OBS
|
- name: Upload to OBS
|
||||||
id: upload
|
|
||||||
env:
|
env:
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||||
@@ -387,8 +362,6 @@ jobs:
|
|||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
echo "✓ No packages need uploading. All up to date!"
|
echo "✓ No packages need uploading. All up to date!"
|
||||||
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -398,10 +371,7 @@ jobs:
|
|||||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
UPLOADED_PACKAGES=()
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||||
SKIPPED_PACKAGES=()
|
|
||||||
|
|
||||||
# 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 ""
|
||||||
@@ -412,37 +382,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
LOG_FILE=$(mktemp)
|
|
||||||
set +e
|
|
||||||
if [[ "$PKG" == "dms-git" ]]; then
|
if [[ "$PKG" == "dms-git" ]]; then
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||||
else
|
else
|
||||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
||||||
fi
|
fi
|
||||||
STATUS=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cat "$LOG_FILE"
|
|
||||||
|
|
||||||
if [[ $STATUS -ne 0 ]]; then
|
|
||||||
rm -f "$LOG_FILE"
|
|
||||||
echo "❌ Upload failed for $PKG"
|
|
||||||
exit $STATUS
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
|
|
||||||
echo "ℹ️ $PKG is already up to date. Skipped."
|
|
||||||
SKIPPED_PACKAGES+=("$PKG")
|
|
||||||
else
|
|
||||||
UPLOADED_PACKAGES+=("$PKG")
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$LOG_FILE"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
|
||||||
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@@ -456,59 +402,20 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
|
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||||
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
|
|
||||||
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
|
|
||||||
UPLOADED_COUNT=0
|
|
||||||
SKIPPED_COUNT=0
|
|
||||||
if [[ -n "$UPLOADED_PACKAGES" ]]; then
|
|
||||||
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
|
|
||||||
fi
|
|
||||||
if [[ -n "$SKIPPED_PACKAGES" ]]; then
|
|
||||||
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
|
|
||||||
fi
|
|
||||||
in_list() {
|
|
||||||
local item="$1"
|
|
||||||
local list="$2"
|
|
||||||
[[ " $list " == *" $item "* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${{ job.status }}" == "success" ]]; then
|
|
||||||
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
STATUS_ICON="✅"
|
|
||||||
STATUS_TEXT="uploaded"
|
|
||||||
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
|
|
||||||
STATUS_ICON="ℹ️"
|
|
||||||
STATUS_TEXT="up to date (skipped)"
|
|
||||||
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
|
|
||||||
STATUS_ICON="❌"
|
|
||||||
STATUS_TEXT="failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$PKG" in
|
case "$PKG" in
|
||||||
dms)
|
dms)
|
||||||
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
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 "- ✅ **dms-git** → [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
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
|
|||||||
284
.github/workflows/run-ppa.yml
vendored
284
.github/workflows/run-ppa.yml
vendored
@@ -4,15 +4,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to upload"
|
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||||
required: true
|
required: false
|
||||||
type: choice
|
default: "dms-git"
|
||||||
options:
|
|
||||||
- dms
|
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
|
||||||
- all
|
|
||||||
default: "dms"
|
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||||
required: false
|
required: false
|
||||||
@@ -22,80 +16,147 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
name: Check package/series updates
|
name: Check for updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
targets: ${{ steps.check.outputs.targets }}
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
targets_json: ${{ steps.check.outputs.targets_json }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y jq curl git
|
|
||||||
|
|
||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
run: |
|
run: |
|
||||||
chmod +x distro/scripts/ppa-sync-plan.sh
|
# Helper function to check dms-git commit
|
||||||
|
check_dms_git() {
|
||||||
|
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
|
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
|
||||||
|
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to check stable package tag
|
||||||
|
check_stable_package() {
|
||||||
|
local PKG="$1"
|
||||||
|
local PPA_NAME="$2"
|
||||||
|
# Use git ls-remote to find the latest tag, sorted by version (descending)
|
||||||
|
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
|
||||||
|
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
|
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
||||||
|
|
||||||
|
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
|
||||||
|
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main logic
|
||||||
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
PACKAGE="dms-git"
|
# Scheduled run - check dms-git only
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
if check_dms_git; then
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
# Manual workflow trigger
|
||||||
|
PKG="${{ github.event.inputs.package }}"
|
||||||
|
|
||||||
|
if [[ -n "$REBUILD" ]]; then
|
||||||
|
# Rebuild requested - always proceed
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
|
# Check each package and build list of those needing updates
|
||||||
|
PACKAGES_TO_UPDATE=()
|
||||||
|
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||||
|
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
|
||||||
|
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||||
|
|
||||||
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ All packages up to date"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
|
if check_dms_git; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms" ]]; then
|
||||||
|
if check_stable_package "dms" "dms"; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||||
|
if check_stable_package "dms-greeter" "danklinux"; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# Unknown package - proceed anyway
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: $PKG"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
PACKAGE="${{ github.event.inputs.package }}"
|
# Fallback
|
||||||
fi
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
ARGS=(--package "$PACKAGE" --json)
|
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
|
||||||
ARGS+=(--rebuild "$REBUILD_RELEASE")
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGETS_JSON=$(distro/scripts/ppa-sync-plan.sh "${ARGS[@]}" 2> ppa-audit.log)
|
|
||||||
cat ppa-audit.log
|
|
||||||
|
|
||||||
TARGETS=$(echo "$TARGETS_JSON" | jq -r 'join(" ")')
|
|
||||||
if [[ "$TARGETS_JSON" != "[]" ]]; then
|
|
||||||
echo "has_updates=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "targets_json=$TARGETS_JSON" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Package/series targets: $TARGETS"
|
|
||||||
else
|
|
||||||
echo "has_updates=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "targets=" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "targets_json=[]" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "No package/series uploads needed"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
upload-ppa:
|
upload-ppa:
|
||||||
name: Upload ${{ matrix.target }}
|
name: Upload to PPA
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: needs.check-updates.outputs.has_updates == 'true'
|
if: needs.check-updates.outputs.has_updates == 'true'
|
||||||
timeout-minutes: 120
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
target: ${{ fromJson(needs.check-updates.outputs.targets_json) }}
|
|
||||||
concurrency:
|
|
||||||
group: ppa-dms-${{ matrix.target }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
@@ -110,8 +171,7 @@ jobs:
|
|||||||
lftp \
|
lftp \
|
||||||
build-essential \
|
build-essential \
|
||||||
fakeroot \
|
fakeroot \
|
||||||
dpkg-dev \
|
dpkg-dev
|
||||||
openssh-client
|
|
||||||
|
|
||||||
- name: Configure GPG
|
- name: Configure GPG
|
||||||
env:
|
env:
|
||||||
@@ -119,32 +179,102 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "$GPG_KEY" | gpg --import
|
echo "$GPG_KEY" | gpg --import
|
||||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> "$GITHUB_ENV"
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload target
|
- name: Determine packages to upload
|
||||||
env:
|
id: packages
|
||||||
TARGET: ${{ matrix.target }}
|
|
||||||
LAUNCHPAD_SSH_PRIVATE_KEY: ${{ secrets.LAUNCHPAD_SSH_PRIVATE_KEY }}
|
|
||||||
LAUNCHPAD_SSH_LOGIN: ${{ secrets.LAUNCHPAD_SSH_LOGIN }}
|
|
||||||
run: |
|
run: |
|
||||||
IFS=':' read -r PACKAGE UBUNTU_SERIES PPA_NUM <<< "$TARGET"
|
# Use packages determined by check-updates job
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
case "$PACKAGE" in
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
dms) PPA_NAME="dms" ;;
|
echo "Triggered by schedule: uploading git package"
|
||||||
dms-git) PPA_NAME="dms-git" ;;
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
dms-greeter) PPA_NAME="danklinux" ;;
|
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
|
||||||
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
fi
|
||||||
esac
|
|
||||||
|
|
||||||
echo "Uploading $PACKAGE to $PPA_NAME/$UBUNTU_SERIES with ppa$PPA_NUM"
|
- name: Upload to PPA
|
||||||
bash distro/scripts/ppa-upload.sh "$PACKAGE" "$PPA_NAME" "$UBUNTU_SERIES" "$PPA_NUM"
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
|
echo "✓ No packages need uploading. All up to date!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export REBUILD_RELEASE so ppa-build.sh can use it
|
||||||
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
|
export REBUILD_RELEASE
|
||||||
|
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||||
|
# Loop through each package and upload
|
||||||
|
for PKG in $PACKAGES; do
|
||||||
|
# Map package to PPA name
|
||||||
|
case "$PKG" in
|
||||||
|
dms)
|
||||||
|
PPA_NAME="dms"
|
||||||
|
;;
|
||||||
|
dms-git)
|
||||||
|
PPA_NAME="dms-git"
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
PPA_NAME="danklinux"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "⚠️ Unknown package: $PKG, skipping"
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PKG to PPA $PPA_NAME..."
|
||||||
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||||
|
done
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "### PPA Package Upload" >> "$GITHUB_STEP_SUMMARY"
|
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
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"
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
|
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
for PKG in $PACKAGES; do
|
||||||
|
case "$PKG" in
|
||||||
|
dms)
|
||||||
|
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
dms-git)
|
||||||
|
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
|
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|||||||
8
.github/workflows/update-vendor-hash.yml
vendored
8
.github/workflows/update-vendor-hash.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
git add flake.nix
|
git add flake.nix
|
||||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
git pull --rebase origin ${{ github.ref_name }}
|
git pull --rebase origin master
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
else
|
else
|
||||||
echo "No changes to flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- repo: local
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
|
rev: v0.10.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
name: shellcheck
|
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
||||||
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
|
|
||||||
language: system
|
|
||||||
types: [shell]
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-mod-tidy
|
- id: go-mod-tidy
|
||||||
@@ -20,11 +18,3 @@ 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: no-console-in-qml
|
|
||||||
name: no console.* in QML (use Log service)
|
|
||||||
entry: bash -c 'if grep -nE "console\.(log|error|info|warn|debug)" "$@"; then echo "Use the Log service (log.info/warn/error/debug/fatal) instead of console.*" >&2; exit 1; fi' --
|
|
||||||
language: system
|
|
||||||
files: ^quickshell/.*\.qml$
|
|
||||||
exclude: ^quickshell/(Services/Log\.qml$|dms-plugins/|PLUGINS/)
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
# 1.5.0
|
|
||||||
- Overhauled shadows
|
|
||||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
|
||||||
- Greeter stuff
|
|
||||||
- Terminal mux
|
|
||||||
- Locale overrides
|
|
||||||
- new neovim theming
|
|
||||||
|
|
||||||
# 1.4.0
|
# 1.4.0
|
||||||
|
|
||||||
- Overhauled system monitor, graphs, styling
|
- Overhauled system monitor, graphs, styling
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ nix develop
|
|||||||
|
|
||||||
This will provide:
|
This will provide:
|
||||||
|
|
||||||
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make
|
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
- Quickshell and required QML packages
|
- Quickshell and required QML packages
|
||||||
- Properly configured QML2_IMPORT_PATH
|
- Properly configured QML2_IMPORT_PATH
|
||||||
|
|
||||||
@@ -86,9 +86,7 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
6. Make your changes, test, and open a pull request.
|
|
||||||
|
|
||||||
### I18n/Localization
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
.PHONY: all build clean 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
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ clean:
|
|||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
lint-qml:
|
|
||||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -79,7 +76,7 @@ install-desktop:
|
|||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entry installed"
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -133,7 +130,6 @@ help:
|
|||||||
@echo " all (default) - Build the DMS binary"
|
@echo " all (default) - Build the DMS binary"
|
||||||
@echo " build - Same as 'all'"
|
@echo " build - Same as 'all'"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
|||||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://archlinux.org/packages/extra/x86_64/dms-shell/)
|
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||||
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/danklinux)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,6 @@ packages:
|
|||||||
outpkg: mocks_brightness
|
outpkg: mocks_brightness
|
||||||
interfaces:
|
interfaces:
|
||||||
DBusConn:
|
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:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/network"
|
dir: "internal/mocks/network"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.10.1
|
rev: v2.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
@@ -63,19 +63,19 @@ endif
|
|||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
install:
|
install: build
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-all:
|
install-all: build-all
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-dankinstall:
|
install-dankinstall: dankinstall
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
|
|||||||
Command-line interface and daemon for shell management and system control.
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
**dankinstall**
|
**dankinstall**
|
||||||
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
|
||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires Go 1.25+
|
Requires Go 1.24+
|
||||||
|
|
||||||
**Development build:**
|
**Development build:**
|
||||||
|
|
||||||
@@ -147,50 +147,10 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
|||||||
|
|
||||||
## Installation via dankinstall
|
## Installation via dankinstall
|
||||||
|
|
||||||
**Interactive (TUI):**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://install.danklinux.com | sh
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Headless (unattended):**
|
|
||||||
|
|
||||||
Headless mode requires cached sudo credentials. Run `sudo -v` first:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
|
|
||||||
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y
|
|
||||||
```
|
|
||||||
|
|
||||||
| Flag | Short | Description |
|
|
||||||
|------|-------|-------------|
|
|
||||||
| `--compositor <niri|hyprland>` | `-c` | Compositor/WM to install (required for headless) |
|
|
||||||
| `--term <ghostty|kitty|alacritty>` | `-t` | Terminal emulator (required for headless) |
|
|
||||||
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
|
|
||||||
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
|
||||||
| `--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`) |
|
|
||||||
| `--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.
|
|
||||||
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
|
||||||
`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it.
|
|
||||||
|
|
||||||
When no flags are provided, `dankinstall` launches the interactive TUI.
|
|
||||||
|
|
||||||
### Headless mode validation rules
|
|
||||||
|
|
||||||
Headless mode activates when `--compositor` or `--term` is provided.
|
|
||||||
|
|
||||||
- Both `--compositor` and `--term` are required; providing only one results in an error.
|
|
||||||
- Headless-only flags (`--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`) are rejected in TUI mode.
|
|
||||||
- Positional arguments are not accepted.
|
|
||||||
|
|
||||||
### Log file location
|
|
||||||
|
|
||||||
`dankinstall` writes logs to `/tmp` by default.
|
|
||||||
Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory.
|
|
||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
||||||
|
|||||||
@@ -3,152 +3,20 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/headless"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
// Flag variables bound via pflag
|
|
||||||
var (
|
|
||||||
compositor string
|
|
||||||
term string
|
|
||||||
includeDeps []string
|
|
||||||
excludeDeps []string
|
|
||||||
replaceConfigs []string
|
|
||||||
replaceConfigsAll bool
|
|
||||||
yes bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "dankinstall",
|
|
||||||
Short: "Install DankMaterialShell and its dependencies",
|
|
||||||
Long: `dankinstall sets up DankMaterialShell with your chosen compositor and terminal.
|
|
||||||
|
|
||||||
Without flags, it launches an interactive TUI. Providing either --compositor
|
|
||||||
or --term activates headless (unattended) mode, which requires both flags.
|
|
||||||
|
|
||||||
Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
|
|
||||||
configure passwordless sudo for your user.`,
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
RunE: runDankinstall,
|
|
||||||
SilenceErrors: true,
|
|
||||||
SilenceUsage: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
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().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(&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().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runDankinstall(cmd *cobra.Command, args []string) error {
|
|
||||||
headlessMode := compositor != "" || term != ""
|
|
||||||
|
|
||||||
if !headlessMode {
|
|
||||||
// Reject headless-only flags when running in TUI mode.
|
|
||||||
headlessOnly := []string{
|
|
||||||
"include-deps",
|
|
||||||
"exclude-deps",
|
|
||||||
"replace-configs",
|
|
||||||
"replace-configs-all",
|
|
||||||
"yes",
|
|
||||||
}
|
|
||||||
var set []string
|
|
||||||
for _, name := range headlessOnly {
|
|
||||||
if cmd.Flags().Changed(name) {
|
|
||||||
set = append(set, "--"+name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(set) > 0 {
|
|
||||||
return fmt.Errorf("flags %s are only valid in headless mode (requires both --compositor and --term)", strings.Join(set, ", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if headlessMode {
|
|
||||||
return runHeadless()
|
|
||||||
}
|
|
||||||
return runTUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runHeadless() error {
|
|
||||||
// Validate required flags
|
|
||||||
if compositor == "" {
|
|
||||||
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
|
|
||||||
}
|
|
||||||
if term == "" {
|
|
||||||
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := headless.Config{
|
|
||||||
Compositor: compositor,
|
|
||||||
Terminal: term,
|
|
||||||
IncludeDeps: includeDeps,
|
|
||||||
ExcludeDeps: excludeDeps,
|
|
||||||
ReplaceConfigs: replaceConfigs,
|
|
||||||
ReplaceConfigsAll: replaceConfigsAll,
|
|
||||||
Yes: yes,
|
|
||||||
}
|
|
||||||
|
|
||||||
runner := headless.NewRunner(cfg)
|
|
||||||
|
|
||||||
// Set up file logging
|
|
||||||
fileLogger, err := log.NewFileLogger()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileLogger != nil {
|
|
||||||
fmt.Printf("Logging to: %s\n", fileLogger.GetLogPath())
|
|
||||||
fileLogger.StartListening(runner.GetLogChan())
|
|
||||||
defer func() {
|
|
||||||
if err := fileLogger.Close(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: Failed to close log file: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
// Drain the log channel to prevent blocking sends from deadlocking
|
|
||||||
// downstream components (distros, config deployer) that write to it.
|
|
||||||
// Use an explicit stop signal because this code does not own the
|
|
||||||
// runner log channel and cannot assume it will be closed.
|
|
||||||
defer drainLogChan(runner.GetLogChan())()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runner.Run(); err != nil {
|
|
||||||
if fileLogger != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", fileLogger.GetLogPath())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileLogger != nil {
|
|
||||||
fmt.Printf("\nFull logs are available at: %s\n", fileLogger.GetLogPath())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTUI() error {
|
|
||||||
fileLogger, err := log.NewFileLogger()
|
fileLogger, err := log.NewFileLogger()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
||||||
@@ -170,50 +38,18 @@ func runTUI() error {
|
|||||||
|
|
||||||
if fileLogger != nil {
|
if fileLogger != nil {
|
||||||
fileLogger.StartListening(model.GetLogChan())
|
fileLogger.StartListening(model.GetLogChan())
|
||||||
} else {
|
|
||||||
// Drain the log channel to prevent blocking sends from deadlocking
|
|
||||||
// downstream components (distros, config deployer) that write to it.
|
|
||||||
// Use an explicit stop signal because this code does not own the
|
|
||||||
// model log channel and cannot assume it will be closed.
|
|
||||||
defer drainLogChan(model.GetLogChan())()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Printf("Error running program: %v\n", err)
|
||||||
if logFilePath != "" {
|
if logFilePath != "" {
|
||||||
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", logFilePath)
|
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("error running program: %w", err)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if logFilePath != "" {
|
if logFilePath != "" {
|
||||||
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// drainLogChan starts a goroutine that discards all messages from logCh,
|
|
||||||
// preventing blocking sends from deadlocking downstream components. It returns
|
|
||||||
// a cleanup function that signals the goroutine to stop and waits for it to
|
|
||||||
// exit. Callers should defer the returned function.
|
|
||||||
func drainLogChan(logCh <-chan string) func() {
|
|
||||||
drainStop := make(chan struct{})
|
|
||||||
drainDone := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(drainDone)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-drainStop:
|
|
||||||
return
|
|
||||||
case _, ok := <-logCh:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return func() {
|
|
||||||
close(drainStop)
|
|
||||||
<-drainDone
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"policy_version": 1,
|
|
||||||
"blocked_commands": [
|
|
||||||
"greeter install",
|
|
||||||
"greeter enable",
|
|
||||||
"greeter uninstall",
|
|
||||||
"setup"
|
|
||||||
],
|
|
||||||
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var authCmd = &cobra.Command{
|
|
||||||
Use: "auth",
|
|
||||||
Short: "Manage DMS authentication sync",
|
|
||||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
|
||||||
}
|
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
|
||||||
Use: "sync",
|
|
||||||
Short: "Sync DMS authentication configuration",
|
|
||||||
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
|
||||||
PreRunE: preRunPrivileged,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
|
||||||
if term {
|
|
||||||
if err := syncAuthInTerminal(yes); err != nil {
|
|
||||||
log.Fatalf("Error launching auth sync in terminal: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := syncAuth(yes); err != nil {
|
|
||||||
log.Fatalf("Error syncing authentication: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
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)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAuth(nonInteractive bool) error {
|
|
||||||
if !nonInteractive {
|
|
||||||
fmt.Println("=== DMS Authentication Sync ===")
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc := func(msg string) {
|
|
||||||
fmt.Println(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !nonInteractive {
|
|
||||||
fmt.Println("\n=== Authentication Sync Complete ===")
|
|
||||||
fmt.Println("\nAuthentication changes have been applied.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAuthInTerminal(nonInteractive bool) error {
|
|
||||||
syncFlags := make([]string, 0, 1)
|
|
||||||
if nonInteractive {
|
|
||||||
syncFlags = append(syncFlags, "--yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
shellSyncCmd := "dms auth sync"
|
|
||||||
if len(syncFlags) > 0 {
|
|
||||||
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
|
||||||
}
|
|
||||||
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
|
||||||
return runCommandInTerminal(shellCmd)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var blurCmd = &cobra.Command{
|
|
||||||
Use: "blur",
|
|
||||||
Short: "Background blur utilities",
|
|
||||||
}
|
|
||||||
|
|
||||||
var blurCheckCmd = &cobra.Command{
|
|
||||||
Use: "check",
|
|
||||||
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: runBlurCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
blurCmd.AddCommand(blurCheckCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runBlurCheck(cmd *cobra.Command, args []string) {
|
|
||||||
supported, err := blur.ProbeSupport()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch supported {
|
|
||||||
case true:
|
|
||||||
fmt.Println("supported")
|
|
||||||
default:
|
|
||||||
fmt.Println("unsupported")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -236,7 +236,6 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
defer ddc.Close()
|
defer ddc.Close()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||||
ddc.WaitPending()
|
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,19 +222,16 @@ func init() {
|
|||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
copyFromStdin := false
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) > 0:
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
case clipCopyDownload || clipCopyType == "__multi__":
|
default:
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("read stdin: %v", err)
|
log.Fatalf("read stdin: %v", err)
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
copyFromStdin = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipCopyDownload {
|
if clipCopyDownload {
|
||||||
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if copyFromStdin {
|
|
||||||
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("copy: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ Output format flags (mutually exclusive, default: --hex):
|
|||||||
--cmyk - CMYK values (C% M% Y% K%)
|
--cmyk - CMYK values (C% M% Y% K%)
|
||||||
--json - JSON with all formats
|
--json - JSON with all formats
|
||||||
|
|
||||||
Optional:
|
|
||||||
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
dms color pick # Pick color, output as hex
|
dms color pick # Pick color, output as hex
|
||||||
dms color pick --rgb # Output as RGB
|
dms color pick --rgb # Output as RGB
|
||||||
@@ -56,7 +53,6 @@ func init() {
|
|||||||
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||||
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||||
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
|
|
||||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
@@ -117,15 +113,7 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
fmt.Println(output)
|
fmt.Println(output)
|
||||||
return
|
} else if color.IsDark() {
|
||||||
}
|
|
||||||
|
|
||||||
if raw, _ := cmd.Flags().GetBool("raw"); raw {
|
|
||||||
fmt.Printf("%s\n", output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if color.IsDark() {
|
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
|
|||||||
@@ -26,17 +26,6 @@ var runCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
daemon, _ := cmd.Flags().GetBool("daemon")
|
daemon, _ := cmd.Flags().GetBool("daemon")
|
||||||
session, _ := cmd.Flags().GetBool("session")
|
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()
|
|
||||||
if daemon {
|
if daemon {
|
||||||
runShellDaemon(session)
|
runShellDaemon(session)
|
||||||
} else {
|
} else {
|
||||||
@@ -75,8 +64,9 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
|
PreRunE: findConfig,
|
||||||
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)
|
_ = findConfig(cmd, args)
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -535,9 +525,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
doctorCmd,
|
doctorCmd,
|
||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
|
||||||
blurCmd,
|
|
||||||
trashCmd,
|
|
||||||
systemCmd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
@@ -83,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
@@ -91,7 +90,6 @@ var (
|
|||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -470,7 +468,6 @@ func checkWindowManagers() []checkResult {
|
|||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -503,7 +500,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -512,24 +509,9 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, checkCompositorBlurSupport())
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCompositorBlurSupport() checkResult {
|
|
||||||
supported, err := blur.ProbeSupport()
|
|
||||||
if err != nil {
|
|
||||||
return checkResult{catCompositor, "Background Blur", statusInfo, "Unable to verify", err.Error(), doctorDocsURL + "#compositor-checks"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if supported {
|
|
||||||
return checkResult{catCompositor, "Background Blur", statusOK, "Supported", "Compositor supports ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil && len(output) == 0 {
|
if err != nil && len(output) == 0 {
|
||||||
@@ -553,8 +535,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("MIRACLESOCK") != "":
|
|
||||||
return "Miracle WM"
|
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||||
}
|
}
|
||||||
@@ -573,7 +553,6 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
|||||||
qmlContent := `
|
qmlContent := `
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -582,7 +561,6 @@ ShellRoot {
|
|||||||
property bool idleMonitorAvailable: false
|
property bool idleMonitorAvailable: false
|
||||||
property bool idleInhibitorAvailable: false
|
property bool idleInhibitorAvailable: false
|
||||||
property bool shortcutInhibitorAvailable: false
|
property bool shortcutInhibitorAvailable: false
|
||||||
property bool backgroundBlurAvailable: false
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 50
|
interval: 50
|
||||||
@@ -600,18 +578,16 @@ ShellRoot {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var testItem = Qt.createQmlObject(
|
var testItem = Qt.createQmlObject(
|
||||||
'import Quickshell; import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||||
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined"; ' +
|
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
||||||
'readonly property bool hasBackgroundBlur: typeof BackgroundEffect !== "undefined" ' +
|
|
||||||
'}',
|
'}',
|
||||||
root
|
root
|
||||||
)
|
)
|
||||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||||
root.backgroundBlurAvailable = testItem.hasBackgroundBlur
|
|
||||||
testItem.destroy()
|
testItem.destroy()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
@@ -620,8 +596,6 @@ ShellRoot {
|
|||||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
||||||
|
|
||||||
console.warn(root.backgroundBlurAvailable ? "FEATURE:BackgroundBlur:OK" : "FEATURE:BackgroundBlur:UNAVAILABLE")
|
|
||||||
|
|
||||||
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,7 +616,6 @@ ShellRoot {
|
|||||||
{"IdleMonitor", "Idle detection"},
|
{"IdleMonitor", "Idle detection"},
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -679,14 +652,16 @@ func checkI2CAvailability() checkResult {
|
|||||||
func checkImageFormatPlugins() []checkResult {
|
func checkImageFormatPlugins() []checkResult {
|
||||||
url := doctorDocsURL + "#optional-features"
|
url := doctorDocsURL + "#optional-features"
|
||||||
|
|
||||||
pluginDirs := findQtPluginDirs()
|
pluginDir := findQtPluginDir()
|
||||||
if len(pluginDirs) == 0 {
|
if pluginDir == "" {
|
||||||
return []checkResult{
|
return []checkResult{
|
||||||
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
||||||
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||||
|
|
||||||
type pluginCheck struct {
|
type pluginCheck struct {
|
||||||
name string
|
name string
|
||||||
desc string
|
desc string
|
||||||
@@ -720,18 +695,9 @@ func checkImageFormatPlugins() []checkResult {
|
|||||||
var results []checkResult
|
var results []checkResult
|
||||||
for _, c := range checks {
|
for _, c := range checks {
|
||||||
var found []string
|
var found []string
|
||||||
var foundDirs []string
|
for _, p := range c.plugins {
|
||||||
for _, pluginDir := range pluginDirs {
|
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
found = append(found, p.format)
|
||||||
for _, p := range c.plugins {
|
|
||||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
|
||||||
if !slices.Contains(found, p.format) {
|
|
||||||
found = append(found, p.format)
|
|
||||||
}
|
|
||||||
if !slices.Contains(foundDirs, imageFormatsDir) {
|
|
||||||
foundDirs = append(foundDirs, imageFormatsDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,7 +708,7 @@ func checkImageFormatPlugins() []checkResult {
|
|||||||
default:
|
default:
|
||||||
details := ""
|
details := ""
|
||||||
if doctorVerbose {
|
if doctorVerbose {
|
||||||
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
|
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
|
||||||
}
|
}
|
||||||
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||||
}
|
}
|
||||||
@@ -752,28 +718,22 @@ func checkImageFormatPlugins() []checkResult {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func findQtPluginDirs() []string {
|
func findQtPluginDir() string {
|
||||||
var dirs []string
|
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
|
||||||
|
|
||||||
addDir := func(dir string) {
|
|
||||||
if dir != "" {
|
|
||||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
|
||||||
dirs = append(dirs, dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
|
||||||
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||||
for dir := range strings.SplitSeq(envPath, ":") {
|
for dir := range strings.SplitSeq(envPath, ":") {
|
||||||
addDir(dir)
|
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try qtpaths
|
// Try qtpaths
|
||||||
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||||
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||||
addDir(strings.TrimSpace(string(output)))
|
if dir := strings.TrimSpace(string(output)); dir != "" {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,10 +744,12 @@ func findQtPluginDirs() []string {
|
|||||||
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
||||||
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
||||||
} {
|
} {
|
||||||
addDir(dir)
|
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dirs
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||||
@@ -847,14 +809,10 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
results = append(results, checkImageFormatPlugins()...)
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
return !utils.CommandExists(t)
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||||
})
|
|
||||||
|
|
||||||
if len(terminals) > 0 {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
networkResult, err := network.DetectNetworkStack()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
@@ -1110,14 +1068,14 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
if currentCategory != -1 {
|
if currentCategory != -1 {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
||||||
currentCategory = r.category
|
currentCategory = r.category
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
|
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
|
||||||
|
|
||||||
if doctorVerbose && r.details != "" {
|
if doctorVerbose && r.details != "" {
|
||||||
fmt.Fprintf(&sb, " - %s\n", r.details)
|
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1127,8 +1085,8 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("\n---\n")
|
sb.WriteString("\n---\n")
|
||||||
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
|
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,7 +15,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"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/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -111,37 +109,16 @@ func updateArchLinux() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packageName string
|
var packageName string
|
||||||
var isAUR bool
|
if isArchPackageInstalled("dms-shell-bin") {
|
||||||
if isArchPackageInstalled("dms-shell") {
|
packageName = "dms-shell-bin"
|
||||||
packageName = "dms-shell"
|
|
||||||
} else if isArchPackageInstalled("dms-shell-git") {
|
} else if isArchPackageInstalled("dms-shell-git") {
|
||||||
packageName = "dms-shell-git"
|
packageName = "dms-shell-git"
|
||||||
isAUR = true
|
|
||||||
} else if isArchPackageInstalled("dms-shell-bin") {
|
|
||||||
packageName = "dms-shell-bin"
|
|
||||||
isAUR = true
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Info: No dms-shell package found.")
|
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||||
fmt.Println("Info: Falling back to git-based update method...")
|
fmt.Println("Info: Falling back to git-based update method...")
|
||||||
return updateOtherDistros()
|
return updateOtherDistros()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isAUR {
|
|
||||||
fmt.Printf("This will update %s using pacman.\n", packageName)
|
|
||||||
if !confirmUpdate() {
|
|
||||||
return errdefs.ErrUpdateCancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
|
|
||||||
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
|
||||||
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("dms successfully updated")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
@@ -477,7 +454,11 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
fmt.Printf("Installing to %s...\n", currentPath)
|
fmt.Printf("Installing to %s...\n", currentPath)
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
||||||
|
replaceCmd.Stdin = os.Stdin
|
||||||
|
replaceCmd.Stdout = os.Stdout
|
||||||
|
replaceCmd.Stderr = os.Stderr
|
||||||
|
if err := replaceCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to replace binary: %w", err)
|
return fmt.Errorf("failed to replace binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
var calls []string
|
|
||||||
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
||||||
if dmsPath != "/tmp/dms" {
|
|
||||||
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
|
||||||
}
|
|
||||||
if compositor != "niri" {
|
|
||||||
t.Fatalf("unexpected compositor %q", compositor)
|
|
||||||
}
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
calls = append(calls, "configs")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotOptions sharedpam.SyncAuthOptions
|
|
||||||
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
gotOptions = options
|
|
||||||
calls = append(calls, "auth")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
|
||||||
ForceGreeterAuth: true,
|
|
||||||
}, func() {
|
|
||||||
calls = append(calls, "before-auth")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantCalls := []string{"configs", "before-auth", "auth"}
|
|
||||||
if !reflect.DeepEqual(calls, wantCalls) {
|
|
||||||
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
|
||||||
}
|
|
||||||
if !gotOptions.ForceGreeterAuth {
|
|
||||||
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
|
||||||
return errors.New("config sync failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
authCalled := false
|
|
||||||
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
|
||||||
authCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
|
||||||
if err == nil || err.Error() != "config sync failed" {
|
|
||||||
t.Fatalf("expected config sync error, got %v", err)
|
|
||||||
}
|
|
||||||
if authCalled {
|
|
||||||
t.Fatal("expected auth sync not to run after config sync failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -57,11 +55,10 @@ func init() {
|
|||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||||
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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", 30*time.Second, "Timeout for waiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
@@ -78,7 +75,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -89,7 +85,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
Mode: matugen.ColorMode(mode),
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
Contrast: contrast,
|
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
@@ -100,11 +95,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
|
|
||||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||||
opts := buildMatugenOptions(cmd)
|
opts := buildMatugenOptions(cmd)
|
||||||
err := matugen.Run(opts)
|
if err := matugen.Run(opts); err != nil {
|
||||||
switch {
|
|
||||||
case errors.Is(err, matugen.ErrNoChanges):
|
|
||||||
os.Exit(2)
|
|
||||||
case err != nil:
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +122,6 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
"skipTemplates": opts.SkipTemplates,
|
||||||
"contrast": opts.Contrast,
|
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -139,11 +129,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
if !wait {
|
if !wait {
|
||||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||||
log.Info("Server unavailable, running synchronously")
|
log.Info("Server unavailable, running synchronously")
|
||||||
err := matugen.Run(opts)
|
if err := matugen.Run(opts); err != nil {
|
||||||
switch {
|
|
||||||
case errors.Is(err, matugen.ErrNoChanges):
|
|
||||||
os.Exit(2)
|
|
||||||
case err != nil:
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -160,15 +146,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
resp, ok := tryServerRequest(request)
|
resp, ok := tryServerRequest(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Info("Server unavailable, running synchronously")
|
log.Info("Server unavailable, running synchronously")
|
||||||
err := matugen.Run(opts)
|
if err := matugen.Run(opts); err != nil {
|
||||||
switch {
|
|
||||||
case errors.Is(err, matugen.ErrNoChanges):
|
|
||||||
resultCh <- matugen.ErrNoChanges
|
|
||||||
case err != nil:
|
|
||||||
resultCh <- err
|
resultCh <- err
|
||||||
default:
|
return
|
||||||
resultCh <- nil
|
|
||||||
}
|
}
|
||||||
|
resultCh <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
@@ -180,10 +162,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-resultCh:
|
case err := <-resultCh:
|
||||||
switch {
|
if err != nil {
|
||||||
case errors.Is(err, matugen.ErrNoChanges):
|
|
||||||
os.Exit(2)
|
|
||||||
case err != nil:
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Theme generation completed")
|
fmt.Println("Theme generation completed")
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var randrCmd = &cobra.Command{
|
|
||||||
Use: "randr",
|
|
||||||
Short: "Query output display information",
|
|
||||||
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
|
|
||||||
Run: runRandr,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
randrCmd.Flags().Bool("json", false, "Output in JSON format")
|
|
||||||
}
|
|
||||||
|
|
||||||
type randrJSON struct {
|
|
||||||
Outputs []randrOutput `json:"outputs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func runRandr(cmd *cobra.Command, args []string) {
|
|
||||||
outputs, err := queryRandr()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonFlag, _ := cmd.Flags().GetBool("json")
|
|
||||||
|
|
||||||
if jsonFlag {
|
|
||||||
data, err := json.Marshal(randrJSON{Outputs: outputs})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to marshal JSON: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println(string(data))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, out := range outputs {
|
|
||||||
if i > 0 {
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
status := "enabled"
|
|
||||||
if !out.Enabled {
|
|
||||||
status = "disabled"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s (%s)\n", out.Name, status)
|
|
||||||
fmt.Printf(" Scale: %.4g\n", out.Scale)
|
|
||||||
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
|
|
||||||
if out.Refresh > 0 {
|
|
||||||
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,8 +22,6 @@ var (
|
|||||||
ssNoClipboard bool
|
ssNoClipboard bool
|
||||||
ssNoFile bool
|
ssNoFile bool
|
||||||
ssNoNotify bool
|
ssNoNotify bool
|
||||||
ssNoConfirm bool
|
|
||||||
ssReset bool
|
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,10 +50,8 @@ Examples:
|
|||||||
dms screenshot output -o DP-1 # Specific output
|
dms screenshot output -o DP-1 # Specific output
|
||||||
dms screenshot window # Focused window (Hyprland)
|
dms screenshot window # Focused window (Hyprland)
|
||||||
dms screenshot last # Last region (pre-selected)
|
dms screenshot last # Last region (pre-selected)
|
||||||
dms screenshot --reset # Reset last region pre-selection
|
|
||||||
dms screenshot --no-clipboard # Save file only
|
dms screenshot --no-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
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`,
|
||||||
}
|
}
|
||||||
@@ -123,8 +119,6 @@ func init() {
|
|||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||||
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(&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.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
@@ -148,8 +142,6 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config.Clipboard = !ssNoClipboard
|
config.Clipboard = !ssNoClipboard
|
||||||
config.SaveFile = !ssNoFile
|
config.SaveFile = !ssNoFile
|
||||||
config.Notify = !ssNoNotify
|
config.Notify = !ssNoNotify
|
||||||
config.NoConfirm = ssNoConfirm
|
|
||||||
config.Reset = ssReset
|
|
||||||
config.Stdout = ssStdout
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
if ssOutputDir != "" {
|
if ssOutputDir != "" {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -12,16 +11,14 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/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/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
PersistentPreRunE: preRunPrivileged,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
@@ -269,8 +266,6 @@ func runSetupDmsConfig(name string) error {
|
|||||||
func runSetup() error {
|
func runSetup() error {
|
||||||
fmt.Println("=== DMS Configuration Setup ===")
|
fmt.Println("=== DMS Configuration Setup ===")
|
||||||
|
|
||||||
ensureInputGroup()
|
|
||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := promptSystemd()
|
useSystemd := promptSystemd()
|
||||||
@@ -344,37 +339,6 @@ func runSetup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user to the input group for the evdev manager for inut state tracking.
|
|
||||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
|
||||||
func ensureInputGroup() {
|
|
||||||
if !utils.HasGroup("input") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentUser := os.Getenv("USER")
|
|
||||||
if currentUser == "" {
|
|
||||||
currentUser = os.Getenv("LOGNAME")
|
|
||||||
}
|
|
||||||
if currentUser == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out, err := execGroups(currentUser)
|
|
||||||
if err == nil && strings.Contains(out, "input") {
|
|
||||||
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("Adding user to input group for Caps Lock OSD support...")
|
|
||||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
|
|
||||||
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func execGroups(user string) (string, error) {
|
|
||||||
out, err := exec.Command("groups", user).Output()
|
|
||||||
return string(out), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func promptCompositor() (deps.WindowManager, bool) {
|
func promptCompositor() (deps.WindowManager, bool) {
|
||||||
fmt.Println("Select compositor:")
|
fmt.Println("Select compositor:")
|
||||||
fmt.Println("1) Niri")
|
fmt.Println("1) Niri")
|
||||||
|
|||||||
@@ -1,365 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var systemCmd = &cobra.Command{
|
|
||||||
Use: "system",
|
|
||||||
Short: "System operations",
|
|
||||||
Long: "System-level operations (updates, etc.). Runs against installed package managers directly; does not require the DMS server.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemUpdateCmd = &cobra.Command{
|
|
||||||
Use: "update",
|
|
||||||
Short: "Apply or list system updates",
|
|
||||||
Long: `Apply or list system updates across detected package managers.
|
|
||||||
|
|
||||||
Default behavior is to apply available updates after prompting for confirmation.
|
|
||||||
Use --check to list updates without applying.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms system update --check # list available updates
|
|
||||||
dms system update # apply updates (interactive prompt)
|
|
||||||
dms system update --noconfirm # apply updates without prompting
|
|
||||||
dms system update --dry # simulate without changing anything
|
|
||||||
dms system update --no-flatpak --noconfirm # apply system updates only
|
|
||||||
dms system update --interval 3600 # set the server poll interval to 1h`,
|
|
||||||
Run: runSystemUpdate,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
sysUpdateCheck bool
|
|
||||||
sysUpdateNoConfirm bool
|
|
||||||
sysUpdateDry bool
|
|
||||||
sysUpdateJSON bool
|
|
||||||
sysUpdateNoFlatpak bool
|
|
||||||
sysUpdateNoAUR bool
|
|
||||||
sysUpdateIntervalS int
|
|
||||||
sysUpdateListPmTime = 5 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateCheck, "check", false, "List available updates without applying")
|
|
||||||
systemUpdateCmd.Flags().BoolVarP(&sysUpdateNoConfirm, "noconfirm", "y", false, "Apply updates without prompting")
|
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes")
|
|
||||||
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(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
|
|
||||||
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
|
||||||
|
|
||||||
systemCmd.AddCommand(systemUpdateCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSystemUpdate(cmd *cobra.Command, args []string) {
|
|
||||||
switch {
|
|
||||||
case sysUpdateIntervalS >= 0:
|
|
||||||
runSystemUpdateSetInterval(sysUpdateIntervalS)
|
|
||||||
case sysUpdateCheck:
|
|
||||||
runSystemUpdateCheck()
|
|
||||||
default:
|
|
||||||
runSystemUpdateApply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectBackends(ctx context.Context) []sysupdate.Backend {
|
|
||||||
sel := sysupdate.Select(ctx)
|
|
||||||
backends := sel.All()
|
|
||||||
if !sysUpdateNoFlatpak {
|
|
||||||
return backends
|
|
||||||
}
|
|
||||||
out := backends[:0]
|
|
||||||
for _, b := range backends {
|
|
||||||
if b.Repo() == sysupdate.RepoFlatpak {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, b)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSystemUpdateCheck() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
backends := selectBackends(ctx)
|
|
||||||
if len(backends) == 0 {
|
|
||||||
log.Fatal("No supported package manager found")
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSpin := startSpinner("Checking for updates… ")
|
|
||||||
allPkgs, firstErr := collectUpdates(ctx, backends)
|
|
||||||
stopSpin()
|
|
||||||
allPkgs = filterUpdateTargets(allPkgs)
|
|
||||||
|
|
||||||
if sysUpdateJSON {
|
|
||||||
out, _ := json.MarshalIndent(map[string]any{
|
|
||||||
"backends": backendResults(backends, allPkgs),
|
|
||||||
"packages": allPkgs,
|
|
||||||
"error": errOrEmpty(firstErr),
|
|
||||||
"count": len(allPkgs),
|
|
||||||
}, "", " ")
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
printBackends(backends)
|
|
||||||
fmt.Printf("Updates: %d\n", len(allPkgs))
|
|
||||||
if firstErr != nil {
|
|
||||||
fmt.Printf("Error: %v\n", firstErr)
|
|
||||||
}
|
|
||||||
if len(allPkgs) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
for _, p := range allPkgs {
|
|
||||||
printPackage(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type backendResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Display string `json:"displayName"`
|
|
||||||
Packages []sysupdate.Package `json:"packages"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func backendResults(backends []sysupdate.Backend, pkgs []sysupdate.Package) []backendResult {
|
|
||||||
results := make([]backendResult, 0, len(backends))
|
|
||||||
for _, b := range backends {
|
|
||||||
var backendPkgs []sysupdate.Package
|
|
||||||
for _, p := range pkgs {
|
|
||||||
if sysupdate.BackendHasTargets(b, []sysupdate.Package{p}, true, true) {
|
|
||||||
backendPkgs = append(backendPkgs, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: backendPkgs})
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSystemUpdateApply() {
|
|
||||||
checkCtx, checkCancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
|
|
||||||
defer checkCancel()
|
|
||||||
|
|
||||||
backends := selectBackends(checkCtx)
|
|
||||||
if len(backends) == 0 {
|
|
||||||
log.Fatal("No supported package manager found")
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSpin := startSpinner("Checking for updates…")
|
|
||||||
pkgs, firstErr := collectUpdates(checkCtx, backends)
|
|
||||||
stopSpin()
|
|
||||||
pkgs = filterUpdateTargets(pkgs)
|
|
||||||
if firstErr != nil {
|
|
||||||
fmt.Printf("Warning: %v\n\n", firstErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
printBackends(backends)
|
|
||||||
fmt.Printf("Updates: %d\n", len(pkgs))
|
|
||||||
if len(pkgs) == 0 {
|
|
||||||
fmt.Println("Nothing to upgrade.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
for _, p := range pkgs {
|
|
||||||
printPackage(p)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
if !sysUpdateNoConfirm && !sysUpdateDry {
|
|
||||||
if !promptYesNo("Proceed with upgrade? [Y/n]: ") {
|
|
||||||
fmt.Println("Aborted.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
opts := sysupdate.UpgradeOptions{
|
|
||||||
Targets: pkgs,
|
|
||||||
IncludeFlatpak: !sysUpdateNoFlatpak,
|
|
||||||
IncludeAUR: !sysUpdateNoAUR,
|
|
||||||
DryRun: sysUpdateDry,
|
|
||||||
UseSudo: true,
|
|
||||||
}
|
|
||||||
opts.AttachStdio = sysupdate.UpgradeNeedsPrivilege(backends, pkgs, opts)
|
|
||||||
|
|
||||||
onLine := func(line string) { fmt.Println(line) }
|
|
||||||
ran := false
|
|
||||||
for _, b := range backends {
|
|
||||||
if !sysupdate.BackendHasTargets(b, pkgs, opts.IncludeAUR, opts.IncludeFlatpak) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ran = true
|
|
||||||
fmt.Printf("\n== %s ==\n", b.DisplayName())
|
|
||||||
if err := b.Upgrade(ctx, opts, onLine); err != nil {
|
|
||||||
log.Fatalf("%s upgrade failed: %v", b.ID(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ran {
|
|
||||||
fmt.Println("Nothing to upgrade.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sysUpdateDry {
|
|
||||||
fmt.Println("\nDry run complete (no changes applied).")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("\nUpgrade complete.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupdate.Package, error) {
|
|
||||||
var all []sysupdate.Package
|
|
||||||
var firstErr error
|
|
||||||
for _, b := range backends {
|
|
||||||
pkgs, err := b.CheckUpdates(ctx)
|
|
||||||
if err != nil && firstErr == nil {
|
|
||||||
firstErr = fmt.Errorf("%s: %w", b.ID(), err)
|
|
||||||
}
|
|
||||||
all = append(all, pkgs...)
|
|
||||||
}
|
|
||||||
return all, firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
|
||||||
if !sysUpdateNoAUR {
|
|
||||||
return pkgs
|
|
||||||
}
|
|
||||||
out := pkgs[:0]
|
|
||||||
for _, p := range pkgs {
|
|
||||||
if p.Repo == sysupdate.RepoAUR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, p)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSystemUpdateSetInterval(seconds int) {
|
|
||||||
resp, err := sendServerRequest(models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "sysupdate.setInterval",
|
|
||||||
Params: map[string]any{"seconds": float64(seconds)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed: %v (is dms server running?)", err)
|
|
||||||
}
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
fmt.Printf("Interval set to %d seconds.\n", seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func promptYesNo(prompt string) bool {
|
|
||||||
if !stdinIsTTY() {
|
|
||||||
log.Fatal("Refusing to apply updates non-interactively. Re-run with --noconfirm or --check.")
|
|
||||||
}
|
|
||||||
fmt.Print(prompt)
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch strings.ToLower(strings.TrimSpace(line)) {
|
|
||||||
case "n", "no":
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printBackends(backends []sysupdate.Backend) {
|
|
||||||
if len(backends) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
names := make([]string, 0, len(backends))
|
|
||||||
for _, b := range backends {
|
|
||||||
names = append(names, b.DisplayName())
|
|
||||||
}
|
|
||||||
fmt.Printf("Backends: %s\n", strings.Join(names, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func stdinIsTTY() bool {
|
|
||||||
fi, err := os.Stdin.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func stdoutIsTTY() bool {
|
|
||||||
fi, err := os.Stdout.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// startSpinner prints an animated spinner to stdout for progress indication
|
|
||||||
func startSpinner(msg string) func() {
|
|
||||||
if !stdoutIsTTY() {
|
|
||||||
return func() {}
|
|
||||||
}
|
|
||||||
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
fmt.Print("\r\033[K")
|
|
||||||
return
|
|
||||||
case <-time.After(80 * time.Millisecond):
|
|
||||||
fmt.Printf("\r%s %s", frames[i%len(frames)], msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return func() { close(done) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
styleRepo = lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Bold(false)
|
|
||||||
styleName = lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
|
|
||||||
styleFrom = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
|
||||||
styleArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
|
||||||
styleTo = lipgloss.NewStyle().Foreground(lipgloss.Color("76")).Bold(true)
|
|
||||||
)
|
|
||||||
|
|
||||||
func printPackage(p sysupdate.Package) {
|
|
||||||
if !stdoutIsTTY() {
|
|
||||||
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf(" %s %s %s %s %s\n",
|
|
||||||
styleRepo.Render("["+string(p.Repo)+"]"),
|
|
||||||
styleName.Render(p.Name),
|
|
||||||
styleFrom.Render(defaultIfEmpty(p.FromVersion, "?")),
|
|
||||||
styleArrow.Render("->"),
|
|
||||||
styleTo.Render(defaultIfEmpty(p.ToVersion, "?")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func errOrEmpty(err error) string {
|
|
||||||
if err == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultIfEmpty(s, def string) string {
|
|
||||||
if s == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/trash"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var trashCmd = &cobra.Command{
|
|
||||||
Use: "trash",
|
|
||||||
Short: "Manage the user's trash (XDG Trash spec 1.0)",
|
|
||||||
}
|
|
||||||
|
|
||||||
var trashPutCmd = &cobra.Command{
|
|
||||||
Use: "put <path...>",
|
|
||||||
Short: "Move files or directories into the trash",
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
Run: runTrashPut,
|
|
||||||
}
|
|
||||||
|
|
||||||
var trashListCmd = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List trashed items across all known trash directories",
|
|
||||||
Run: runTrashList,
|
|
||||||
}
|
|
||||||
|
|
||||||
var trashCountCmd = &cobra.Command{
|
|
||||||
Use: "count",
|
|
||||||
Short: "Print the total number of trashed items",
|
|
||||||
Run: runTrashCount,
|
|
||||||
}
|
|
||||||
|
|
||||||
var trashEmptyCmd = &cobra.Command{
|
|
||||||
Use: "empty",
|
|
||||||
Short: "Permanently delete every trashed item",
|
|
||||||
Run: runTrashEmpty,
|
|
||||||
}
|
|
||||||
|
|
||||||
var trashRestoreCmd = &cobra.Command{
|
|
||||||
Use: "restore <name>",
|
|
||||||
Short: "Restore a trashed item to its original location",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runTrashRestore,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
trashJSONOutput bool
|
|
||||||
trashRestoreDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
trashListCmd.Flags().BoolVar(&trashJSONOutput, "json", false, "Output as JSON")
|
|
||||||
trashRestoreCmd.Flags().StringVar(&trashRestoreDir, "trash-dir", "", "Trash directory containing the item (default: home trash)")
|
|
||||||
trashCmd.AddCommand(trashPutCmd, trashListCmd, trashCountCmd, trashEmptyCmd, trashRestoreCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTrashPut(cmd *cobra.Command, args []string) {
|
|
||||||
var failed int
|
|
||||||
for _, p := range args {
|
|
||||||
if _, err := trash.Put(p); err != nil {
|
|
||||||
log.Errorf("trash %s: %v", p, err)
|
|
||||||
failed++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Println(p)
|
|
||||||
}
|
|
||||||
if failed > 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTrashList(cmd *cobra.Command, args []string) {
|
|
||||||
entries, err := trash.List()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("list trash: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if trashJSONOutput {
|
|
||||||
if entries == nil {
|
|
||||||
entries = []trash.Entry{}
|
|
||||||
}
|
|
||||||
out, _ := json.MarshalIndent(entries, "", " ")
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entries) == 0 {
|
|
||||||
fmt.Println("Trash is empty")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, e := range entries {
|
|
||||||
marker := "F"
|
|
||||||
if e.IsDir {
|
|
||||||
marker = "D"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s %s %s %s\n", marker, e.DeletionDate, e.Name, e.OriginalPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTrashCount(cmd *cobra.Command, args []string) {
|
|
||||||
n, err := trash.Count()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("count trash: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTrashEmpty(cmd *cobra.Command, args []string) {
|
|
||||||
if err := trash.Empty(); err != nil {
|
|
||||||
log.Fatalf("empty trash: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTrashRestore(cmd *cobra.Command, args []string) {
|
|
||||||
if err := trash.Restore(args[0], trashRestoreDir); err != nil {
|
|
||||||
log.Fatalf("restore: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
|
||||||
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
immutablePolicyOnce sync.Once
|
|
||||||
immutablePolicy immutableCommandPolicy
|
|
||||||
immutablePolicyErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/cli-policy.default.json
|
|
||||||
var defaultCLIPolicyJSON []byte
|
|
||||||
|
|
||||||
type immutableCommandPolicy struct {
|
|
||||||
ImmutableSystem bool
|
|
||||||
ImmutableReason string
|
|
||||||
BlockedCommands []string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliPolicyFile struct {
|
|
||||||
PolicyVersion int `json:"policy_version"`
|
|
||||||
ImmutableSystem *bool `json:"immutable_system"`
|
|
||||||
BlockedCommands *[]string `json:"blocked_commands"`
|
|
||||||
Message *string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCommandSpec(raw string) string {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
|
||||||
normalized = strings.TrimPrefix(normalized, "dms ")
|
|
||||||
return strings.Join(strings.Fields(normalized), " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeBlockedCommands(raw []string) []string {
|
|
||||||
normalized := make([]string, 0, len(raw))
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, cmd := range raw {
|
|
||||||
spec := normalizeCommandSpec(cmd)
|
|
||||||
if spec == "" || seen[spec] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[spec] = true
|
|
||||||
normalized = append(normalized, spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
|
||||||
normalizedPath := normalizeCommandSpec(commandPath)
|
|
||||||
if normalizedPath == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range blocked {
|
|
||||||
spec := normalizeCommandSpec(entry)
|
|
||||||
if spec == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var policy cliPolicyFile
|
|
||||||
if err := json.Unmarshal(data, &policy); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &policy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
|
||||||
policyFile, err := loadPolicyFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if policyFile == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if policyFile.ImmutableSystem != nil {
|
|
||||||
base.ImmutableSystem = *policyFile.ImmutableSystem
|
|
||||||
}
|
|
||||||
if policyFile.BlockedCommands != nil {
|
|
||||||
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
|
||||||
}
|
|
||||||
if policyFile.Message != nil {
|
|
||||||
msg := strings.TrimSpace(*policyFile.Message)
|
|
||||||
if msg != "" {
|
|
||||||
base.Message = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOSReleaseMap(path string) map[string]string {
|
|
||||||
values := make(map[string]string)
|
|
||||||
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
|
||||||
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
|
||||||
values[key] = strings.ToLower(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasAnyToken(text string, tokens ...string) bool {
|
|
||||||
if text == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.Contains(text, token) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectImmutableSystem() (bool, string) {
|
|
||||||
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
|
||||||
return true, "/run/ostree-booted is present"
|
|
||||||
}
|
|
||||||
|
|
||||||
osRelease := readOSReleaseMap("/etc/os-release")
|
|
||||||
if len(osRelease) == 0 {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
id := osRelease["ID"]
|
|
||||||
idLike := osRelease["ID_LIKE"]
|
|
||||||
variantID := osRelease["VARIANT_ID"]
|
|
||||||
name := osRelease["NAME"]
|
|
||||||
prettyName := osRelease["PRETTY_NAME"]
|
|
||||||
|
|
||||||
immutableIDs := map[string]bool{
|
|
||||||
"bluefin": true,
|
|
||||||
"bazzite": true,
|
|
||||||
"silverblue": true,
|
|
||||||
"kinoite": true,
|
|
||||||
"sericea": true,
|
|
||||||
"onyx": true,
|
|
||||||
"aurora": true,
|
|
||||||
"fedora-iot": true,
|
|
||||||
"fedora-coreos": true,
|
|
||||||
}
|
|
||||||
if immutableIDs[id] {
|
|
||||||
return true, "os-release ID=" + id
|
|
||||||
}
|
|
||||||
|
|
||||||
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
|
||||||
if hasAnyToken(variantID, markers...) {
|
|
||||||
return true, "os-release VARIANT_ID=" + variantID
|
|
||||||
}
|
|
||||||
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
|
||||||
return true, "os-release ID_LIKE=" + idLike
|
|
||||||
}
|
|
||||||
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
|
||||||
return true, "os-release identifies an atomic/ostree variant"
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
|
||||||
immutablePolicyOnce.Do(func() {
|
|
||||||
detectedImmutable, reason := detectImmutableSystem()
|
|
||||||
immutablePolicy = immutableCommandPolicy{
|
|
||||||
ImmutableSystem: detectedImmutable,
|
|
||||||
ImmutableReason: reason,
|
|
||||||
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
|
||||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultPolicy cliPolicyFile
|
|
||||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
|
||||||
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if defaultPolicy.BlockedCommands != nil {
|
|
||||||
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
|
||||||
}
|
|
||||||
if defaultPolicy.Message != nil {
|
|
||||||
msg := strings.TrimSpace(*defaultPolicy.Message)
|
|
||||||
if msg != "" {
|
|
||||||
immutablePolicy.Message = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
|
||||||
immutablePolicyErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
|
||||||
immutablePolicyErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if immutablePolicyErr != nil {
|
|
||||||
return nil, immutablePolicyErr
|
|
||||||
}
|
|
||||||
return &immutablePolicy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
|
||||||
policy, err := getImmutablePolicy()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !policy.ImmutableSystem {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
|
||||||
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reason := ""
|
|
||||||
if policy.ImmutableReason != "" {
|
|
||||||
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// preRunPrivileged combines the immutable-system check with a privesc tool
|
|
||||||
// selection prompt (shown only when multiple tools are available and the
|
|
||||||
// $DMS_PRIVESC env var isn't set).
|
|
||||||
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,27 +14,30 @@ func init() {
|
|||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
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("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
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")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
// Add subcommands to greeter
|
||||||
authCmd.AddCommand(authSyncCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to setup
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
|
// Add subcommands to update
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(authCmd)
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
clipboard.MaybeServeAndExit()
|
if os.Geteuid() == 0 {
|
||||||
|
|
||||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,36 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// Add flags
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
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("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
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")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
// Add subcommands to greeter
|
||||||
authCmd.AddCommand(authSyncCmd)
|
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to setup
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
rootCmd.AddCommand(authCmd)
|
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
clipboard.MaybeServeAndExit()
|
// Block root
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
type randrOutput struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Scale float64 `json:"scale"`
|
|
||||||
Width int32 `json:"width"`
|
|
||||||
Height int32 `json:"height"`
|
|
||||||
Refresh int32 `json:"refresh"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type randrHead struct {
|
|
||||||
name string
|
|
||||||
enabled bool
|
|
||||||
scale float64
|
|
||||||
currentModeID uint32
|
|
||||||
modeIDs []uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type randrMode struct {
|
|
||||||
width int32
|
|
||||||
height int32
|
|
||||||
refresh int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type randrClient struct {
|
|
||||||
display *wlclient.Display
|
|
||||||
ctx *wlclient.Context
|
|
||||||
manager *wlr_output_management.ZwlrOutputManagerV1
|
|
||||||
heads map[uint32]*randrHead
|
|
||||||
modes map[uint32]*randrMode
|
|
||||||
done bool
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryRandr() ([]randrOutput, error) {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &randrClient{
|
|
||||||
display: display,
|
|
||||||
ctx: display.Context(),
|
|
||||||
heads: make(map[uint32]*randrHead),
|
|
||||||
modes: make(map[uint32]*randrMode),
|
|
||||||
}
|
|
||||||
defer c.ctx.Close()
|
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get registry: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
|
||||||
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
|
|
||||||
version := min(e.Version, 4)
|
|
||||||
|
|
||||||
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
|
||||||
c.handleHead(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
|
||||||
c.done = true
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
|
||||||
c.manager = mgr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// First roundtrip: discover globals and bind manager
|
|
||||||
syncCallback, err := display.Sync()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to sync display: %w", err)
|
|
||||||
}
|
|
||||||
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
|
||||||
if c.manager == nil {
|
|
||||||
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
|
|
||||||
c.done = true
|
|
||||||
}
|
|
||||||
// Otherwise wait for manager's DoneHandler
|
|
||||||
})
|
|
||||||
|
|
||||||
for !c.done {
|
|
||||||
if err := c.ctx.Dispatch(); err != nil {
|
|
||||||
return nil, fmt.Errorf("dispatch error: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.err != nil {
|
|
||||||
return nil, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.buildOutputs(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
|
||||||
handle := e.Head
|
|
||||||
headID := handle.ID()
|
|
||||||
|
|
||||||
head := &randrHead{
|
|
||||||
modeIDs: make([]uint32, 0),
|
|
||||||
}
|
|
||||||
c.heads[headID] = head
|
|
||||||
|
|
||||||
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
|
||||||
head.name = e.Name
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
|
|
||||||
head.enabled = e.Enabled != 0
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
|
||||||
head.scale = e.Scale
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
|
|
||||||
head.currentModeID = e.Mode.ID()
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
|
|
||||||
modeHandle := e.Mode
|
|
||||||
modeID := modeHandle.ID()
|
|
||||||
|
|
||||||
head.modeIDs = append(head.modeIDs, modeID)
|
|
||||||
|
|
||||||
mode := &randrMode{}
|
|
||||||
c.modes[modeID] = mode
|
|
||||||
|
|
||||||
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
|
|
||||||
mode.width = e.Width
|
|
||||||
mode.height = e.Height
|
|
||||||
})
|
|
||||||
|
|
||||||
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
|
|
||||||
mode.refresh = e.Refresh
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *randrClient) buildOutputs() []randrOutput {
|
|
||||||
outputs := make([]randrOutput, 0, len(c.heads))
|
|
||||||
|
|
||||||
for _, head := range c.heads {
|
|
||||||
out := randrOutput{
|
|
||||||
Name: head.name,
|
|
||||||
Scale: head.scale,
|
|
||||||
Enabled: head.enabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode, ok := c.modes[head.currentModeID]; ok {
|
|
||||||
out.Width = mode.width
|
|
||||||
out.Height = mode.height
|
|
||||||
out.Refresh = mode.refresh
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs = append(outputs, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputs
|
|
||||||
}
|
|
||||||
@@ -80,16 +80,6 @@ func getRuntimeDir() string {
|
|||||||
return os.TempDir()
|
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 {
|
func hasSystemdRun() bool {
|
||||||
_, err := exec.LookPath("systemd-run")
|
_, err := exec.LookPath("systemd-run")
|
||||||
return err == nil
|
return err == nil
|
||||||
@@ -223,8 +213,6 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = appendLogEnv(cmd.Env)
|
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
@@ -444,9 +432,6 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! 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() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
}
|
}
|
||||||
@@ -468,8 +453,6 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
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)
|
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error opening /dev/null: %v", err)
|
log.Fatalf("Error opening /dev/null: %v", err)
|
||||||
@@ -633,43 +616,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstDMSPID() (int, bool) {
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if proc.Signal(syscall.Signal(0)) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -681,21 +627,10 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
switch pid, ok := getFirstDMSPID(); {
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
case ok:
|
|
||||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
|
||||||
default:
|
|
||||||
if err := findConfig(nil, nil); err != nil {
|
|
||||||
log.Fatalf("Error finding config: %v", err)
|
|
||||||
}
|
|
||||||
// ! TODO - remove check when QS 0.3 is released
|
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
|
||||||
}
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
cmdArgs = append(cmdArgs, args...)
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
@@ -7,20 +7,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
func findCommandPath(cmd string) (string, error) {
|
||||||
// safe to run as root (e.g. shell completion, help).
|
path, err := exec.LookPath(cmd)
|
||||||
func isReadOnlyCommand(args []string) bool {
|
if err != nil {
|
||||||
for _, arg := range args[1:] {
|
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
||||||
if strings.HasPrefix(arg, "-") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch arg {
|
|
||||||
case "completion", "help", "__complete", "system":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return false
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
|
|||||||
78
core/go.mod
78
core/go.mod
@@ -1,100 +1,76 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.26.1
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1
|
github.com/alecthomas/chroma/v2 v2.23.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/charmbracelet/log v0.4.2
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
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-20250804134636-ab1d56a1fe83
|
||||||
github.com/pilebones/go-udev v0.9.1
|
github.com/pilebones/go-udev v0.9.1
|
||||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
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/yuin/goldmark v1.7.16
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
|
||||||
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.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
golang.org/x/image v0.36.0
|
||||||
golang.org/x/image v0.39.0
|
|
||||||
tailscale.com v1.96.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
|
||||||
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.3.0 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
|
github.com/dlclark/regexp2 v1.11.5 // 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/fxamacker/cbor/v2 v2.9.2 // indirect
|
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // 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/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
|
||||||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
|
||||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/mdlayher/netlink v1.11.1 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/mdlayher/socket v0.6.0 // indirect
|
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
|
||||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
|
||||||
golang.org/x/net v0.53.0 // indirect
|
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
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.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
|
||||||
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.3.0
|
||||||
github.com/mattn/go-isatty v0.0.22
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.36.0
|
golang.org/x/text v0.34.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
170
core/go.sum
170
core/go.sum
@@ -1,18 +1,14 @@
|
|||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
|
||||||
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.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||||
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
|
||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
github.com/alecthomas/repr v0.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=
|
||||||
@@ -28,93 +24,68 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f
|
|||||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
|
||||||
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/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
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/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-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 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
|
||||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
|
||||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
|
||||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 h1:wH21vHuv323v9x78JNFNJ6P7HEAsdwr9yq2k9/o4zEE=
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
|
||||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
||||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
|
|
||||||
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=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847 h1:1rQ5UQXFm02DXEtsIpotfA32WJ9KceS6t8w5K8QtFqc=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
|
|
||||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
|
||||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
@@ -126,20 +97,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
|
|
||||||
github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
|
|
||||||
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
|
||||||
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/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=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
@@ -148,13 +113,10 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
|||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -178,62 +140,40 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/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/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
|
|
||||||
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/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=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
|
||||||
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/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
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=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
|
||||||
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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
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/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
|
||||||
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package blur
|
|
||||||
|
|
||||||
import (
|
|
||||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
|
||||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
|
||||||
|
|
||||||
func ProbeSupport() (bool, error) {
|
|
||||||
display, err := client.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer display.Context().Close()
|
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case extBackgroundEffectInterface:
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
@@ -5,196 +5,55 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"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"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
|
||||||
const envMime = "_DMS_CLIPBOARD_MIME"
|
|
||||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
|
||||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
|
||||||
|
|
||||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
|
||||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
|
||||||
func MaybeServeAndExit() {
|
|
||||||
if os.Getenv(envServe) == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeType := os.Getenv(envMime)
|
|
||||||
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
|
||||||
cachePath := os.Getenv(envCacheFile)
|
|
||||||
|
|
||||||
var data []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case cachePath != "":
|
|
||||||
data, err = os.ReadFile(cachePath)
|
|
||||||
os.Remove(cachePath)
|
|
||||||
default:
|
|
||||||
data, err = io.ReadAll(os.Stdin)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return copyForkCached(data, mimeType, false)
|
return CopyOpts(data, mimeType, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if !foreground {
|
||||||
return serveClipboard(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyForkCached(data, mimeType, pasteOnce)
|
return copyServe(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
if foreground {
|
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||||
buf, err := io.ReadAll(data)
|
if pasteOnce {
|
||||||
if err != nil {
|
args = append(args, "--paste-once")
|
||||||
return fmt.Errorf("read source: %w", err)
|
|
||||||
}
|
|
||||||
return serveClipboard(buf, mimeType, pasteOnce)
|
|
||||||
}
|
}
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
args = append(args, "--type", mimeType)
|
||||||
}
|
|
||||||
|
|
||||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
cmd := exec.Command(os.Args[0])
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
envServe+"=1",
|
|
||||||
envMime+"="+mimeType,
|
|
||||||
)
|
|
||||||
if pasteOnce {
|
|
||||||
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
|
||||||
}
|
|
||||||
cmd.Env = append(cmd.Env, extra...)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitReady(cmd *exec.Cmd) error {
|
stdin, err := cmd.StdinPipe()
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
var buf [1]byte
|
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
if _, err := stdin.Write(data); err != nil {
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
}
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
cacheFile, err := createClipboardCacheFile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create cache file: %w", err)
|
|
||||||
}
|
|
||||||
cachePath := cacheFile.Name()
|
|
||||||
|
|
||||||
if _, err := cacheFile.Write(data); err != nil {
|
|
||||||
cacheFile.Close()
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return fmt.Errorf("write cache file: %w", err)
|
|
||||||
}
|
|
||||||
if err := cacheFile.Close(); err != nil {
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return fmt.Errorf("close cache file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
if err := waitReady(cmd); err != nil {
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|
||||||
cmd := newForkCmd(mimeType, pasteOnce)
|
|
||||||
|
|
||||||
switch src := data.(type) {
|
|
||||||
case *os.File:
|
|
||||||
cmd.Stdin = src
|
|
||||||
return waitReady(cmd)
|
|
||||||
|
|
||||||
default:
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(stdin, data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
|
||||||
if err := stdin.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf [1]byte
|
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func signalReady() {
|
|
||||||
if os.Getenv(envServe) == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os.Stdout.Write([]byte{1})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
|
||||||
preferredDirs := []string{}
|
|
||||||
|
|
||||||
if cacheDir, err := os.UserCacheDir(); err == nil {
|
|
||||||
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
|
|
||||||
}
|
|
||||||
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
|
|
||||||
|
|
||||||
for _, dir := range preferredDirs {
|
|
||||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
|
|
||||||
if err == nil {
|
|
||||||
return cachedData, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.CreateTemp("", "dms-clipboard-*")
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -236,10 +95,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -280,10 +141,10 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
_ = syscall.SetNonblock(e.Fd, false)
|
defer syscall.Close(e.Fd)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
_, _ = file.Write(data)
|
file.Write(data)
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -299,7 +160,6 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
signalReady()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -558,10 +418,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -589,12 +451,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
_ = syscall.SetNonblock(e.Fd, false)
|
defer syscall.Close(e.Fd)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
_, _ = file.Write(data)
|
file.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ type LayerSurface struct {
|
|||||||
wlSurface *client.Surface
|
wlSurface *client.Surface
|
||||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
viewport *wp_viewporter.WpViewport
|
viewport *wp_viewporter.WpViewport
|
||||||
wlPools [2]*client.ShmPool
|
wlPool *client.ShmPool
|
||||||
wlBuffers [2]*client.Buffer
|
wlBuffer *client.Buffer
|
||||||
slotBusy [2]bool
|
bufferBusy bool
|
||||||
needsRedraw bool
|
oldPool *client.ShmPool
|
||||||
|
oldBuffer *client.Buffer
|
||||||
scopyBuffer *client.Buffer
|
scopyBuffer *client.Buffer
|
||||||
configured bool
|
configured bool
|
||||||
hidden bool
|
hidden bool
|
||||||
@@ -135,7 +136,6 @@ func (p *Picker) Run() (*Color, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
p.flushRedraws()
|
|
||||||
p.checkDone()
|
p.checkDone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,15 +164,6 @@ func (p *Picker) checkDone() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) flushRedraws() {
|
|
||||||
for _, ls := range p.surfaces {
|
|
||||||
if !ls.needsRedraw {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p.redrawSurface(ls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Picker) connect() error {
|
func (p *Picker) connect() error {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -516,45 +507,47 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
slot := ls.state.FrontIndex()
|
|
||||||
if ls.slotBusy[slot] {
|
|
||||||
ls.needsRedraw = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var renderBuf *ShmBuffer
|
var renderBuf *ShmBuffer
|
||||||
switch {
|
if ls.hidden {
|
||||||
case ls.hidden:
|
|
||||||
renderBuf = ls.state.RedrawScreenOnly()
|
renderBuf = ls.state.RedrawScreenOnly()
|
||||||
default:
|
} else {
|
||||||
renderBuf = ls.state.Redraw()
|
renderBuf = ls.state.Redraw()
|
||||||
}
|
}
|
||||||
if renderBuf == nil {
|
if renderBuf == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ls.needsRedraw = false
|
if ls.oldBuffer != nil {
|
||||||
|
ls.oldBuffer.Destroy()
|
||||||
if ls.wlPools[slot] == nil {
|
ls.oldBuffer = nil
|
||||||
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
}
|
||||||
if err != nil {
|
if ls.oldPool != nil {
|
||||||
return
|
ls.oldPool.Destroy()
|
||||||
}
|
ls.oldPool = nil
|
||||||
ls.wlPools[slot] = pool
|
|
||||||
|
|
||||||
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ls.wlBuffers[slot] = wlBuffer
|
|
||||||
|
|
||||||
s := slot
|
|
||||||
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
|
||||||
ls.slotBusy[s] = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ls.slotBusy[slot] = true
|
ls.oldPool = ls.wlPool
|
||||||
|
ls.oldBuffer = ls.wlBuffer
|
||||||
|
ls.wlPool = nil
|
||||||
|
ls.wlBuffer = nil
|
||||||
|
|
||||||
|
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlPool = pool
|
||||||
|
|
||||||
|
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlBuffer = wlBuffer
|
||||||
|
|
||||||
|
lsRef := ls
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
lsRef.bufferBusy = false
|
||||||
|
})
|
||||||
|
ls.bufferBusy = true
|
||||||
|
|
||||||
logicalW, logicalH := ls.state.LogicalSize()
|
logicalW, logicalH := ls.state.LogicalSize()
|
||||||
if logicalW == 0 || logicalH == 0 {
|
if logicalW == 0 || logicalH == 0 {
|
||||||
@@ -573,7 +566,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
|||||||
}
|
}
|
||||||
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||||
}
|
}
|
||||||
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
|
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||||
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
_ = ls.wlSurface.Commit()
|
_ = ls.wlSurface.Commit()
|
||||||
|
|
||||||
@@ -641,7 +634,7 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
p.activeSurface.needsRedraw = true
|
p.redrawSurface(p.activeSurface)
|
||||||
})
|
})
|
||||||
|
|
||||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||||
@@ -662,7 +655,7 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
p.activeSurface.needsRedraw = true
|
p.redrawSurface(p.activeSurface)
|
||||||
})
|
})
|
||||||
|
|
||||||
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||||
@@ -686,13 +679,17 @@ func (p *Picker) cleanup() {
|
|||||||
if ls.scopyBuffer != nil {
|
if ls.scopyBuffer != nil {
|
||||||
ls.scopyBuffer.Destroy()
|
ls.scopyBuffer.Destroy()
|
||||||
}
|
}
|
||||||
for i := range ls.wlBuffers {
|
if ls.oldBuffer != nil {
|
||||||
if ls.wlBuffers[i] != nil {
|
ls.oldBuffer.Destroy()
|
||||||
ls.wlBuffers[i].Destroy()
|
}
|
||||||
}
|
if ls.oldPool != nil {
|
||||||
if ls.wlPools[i] != nil {
|
ls.oldPool.Destroy()
|
||||||
ls.wlPools[i].Destroy()
|
}
|
||||||
}
|
if ls.wlBuffer != nil {
|
||||||
|
ls.wlBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlPool != nil {
|
||||||
|
ls.wlPool.Destroy()
|
||||||
}
|
}
|
||||||
if ls.viewport != nil {
|
if ls.viewport != nil {
|
||||||
ls.viewport.Destroy()
|
ls.viewport.Destroy()
|
||||||
|
|||||||
@@ -274,12 +274,6 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
|
|||||||
return s.renderBufs[s.front]
|
return s.renderBufs[s.front]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) FrontIndex() int {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.front
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SurfaceState) SwapBuffers() {
|
func (s *SurfaceState) SwapBuffers() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.front ^= 1
|
s.front ^= 1
|
||||||
|
|||||||
@@ -62,31 +62,12 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
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
|
||||||
|
|
||||||
// Primary config file paths used to detect fresh installs.
|
|
||||||
configPrimaryPaths := map[string]string{
|
|
||||||
"Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
|
||||||
"Hyprland": filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
|
||||||
"Ghostty": filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
|
||||||
"Kitty": filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
|
||||||
"Alacritty": filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldReplaceConfig := func(configType string) bool {
|
shouldReplaceConfig := func(configType string) bool {
|
||||||
if replaceConfigs == nil {
|
if replaceConfigs == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
replace, exists := replaceConfigs[configType]
|
replace, exists := replaceConfigs[configType]
|
||||||
if !exists || replace {
|
return !exists || replace
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Config is explicitly set to "don't replace" — but still deploy
|
|
||||||
// if the config file doesn't exist yet (fresh install scenario).
|
|
||||||
if primaryPath, ok := configPrimaryPaths[configType]; ok {
|
|
||||||
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -625,168 +624,3 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
|
|||||||
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
|
|
||||||
allFalse := map[string]bool{
|
|
||||||
"Niri": false,
|
|
||||||
"Hyprland": false,
|
|
||||||
"Ghostty": false,
|
|
||||||
"Kitty": false,
|
|
||||||
"Alacritty": false,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("replaceConfigs nil deploys config", func(t *testing.T) {
|
|
||||||
tempDir, err := os.MkdirTemp("", "dankinstall-replace-nil-test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tempDir)
|
|
||||||
defer os.Setenv("HOME", originalHome)
|
|
||||||
|
|
||||||
logChan := make(chan string, 100)
|
|
||||||
cd := NewConfigDeployer(logChan)
|
|
||||||
|
|
||||||
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
|
||||||
context.Background(),
|
|
||||||
deps.WindowManagerNiri,
|
|
||||||
deps.TerminalGhostty,
|
|
||||||
nil, // installedDeps
|
|
||||||
nil, // replaceConfigs
|
|
||||||
nil, // reinstallItems
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// With replaceConfigs=nil, all configs should be deployed
|
|
||||||
hasDeployed := false
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Deployed {
|
|
||||||
hasDeployed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, hasDeployed, "expected at least one config to be deployed when replaceConfigs is nil")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("replaceConfigs all false and config missing deploys config", func(t *testing.T) {
|
|
||||||
tempDir, err := os.MkdirTemp("", "dankinstall-replace-missing-test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tempDir)
|
|
||||||
defer os.Setenv("HOME", originalHome)
|
|
||||||
|
|
||||||
logChan := make(chan string, 100)
|
|
||||||
cd := NewConfigDeployer(logChan)
|
|
||||||
|
|
||||||
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
|
||||||
context.Background(),
|
|
||||||
deps.WindowManagerNiri,
|
|
||||||
deps.TerminalGhostty,
|
|
||||||
nil, // installedDeps
|
|
||||||
allFalse, // replaceConfigs — all false
|
|
||||||
nil, // reinstallItems
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Config files don't exist on disk, so they should still be deployed
|
|
||||||
hasDeployed := false
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Deployed {
|
|
||||||
hasDeployed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, hasDeployed, "expected configs to be deployed when files are missing, even with replaceConfigs all false")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("replaceConfigs false and config exists skips config", func(t *testing.T) {
|
|
||||||
tempDir, err := os.MkdirTemp("", "dankinstall-replace-exists-test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tempDir)
|
|
||||||
defer os.Setenv("HOME", originalHome)
|
|
||||||
|
|
||||||
// Create the Ghostty primary config file so shouldReplaceConfig returns false
|
|
||||||
ghosttyPath := filepath.Join(tempDir, ".config", "ghostty", "config")
|
|
||||||
err = os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = os.WriteFile(ghosttyPath, []byte("# existing ghostty config\n"), 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Also create the Niri primary config file
|
|
||||||
niriPath := filepath.Join(tempDir, ".config", "niri", "config.kdl")
|
|
||||||
err = os.MkdirAll(filepath.Dir(niriPath), 0o755)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = os.WriteFile(niriPath, []byte("// existing niri config\n"), 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logChan := make(chan string, 100)
|
|
||||||
cd := NewConfigDeployer(logChan)
|
|
||||||
|
|
||||||
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
|
||||||
context.Background(),
|
|
||||||
deps.WindowManagerNiri,
|
|
||||||
deps.TerminalGhostty,
|
|
||||||
nil, // installedDeps
|
|
||||||
allFalse, // replaceConfigs — all false
|
|
||||||
nil, // reinstallItems
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Both Niri and Ghostty config files exist, so with all false they should be skipped
|
|
||||||
for _, r := range results {
|
|
||||||
assert.Fail(t, "expected no configs to be deployed", "got deployed config: %s", r.ConfigType)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("replaceConfigs true and config exists deploys config", func(t *testing.T) {
|
|
||||||
tempDir, err := os.MkdirTemp("", "dankinstall-replace-true-test")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tempDir)
|
|
||||||
defer os.Setenv("HOME", originalHome)
|
|
||||||
|
|
||||||
// Create the Ghostty primary config file
|
|
||||||
ghosttyPath := filepath.Join(tempDir, ".config", "ghostty", "config")
|
|
||||||
err = os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = os.WriteFile(ghosttyPath, []byte("# existing ghostty config\n"), 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
logChan := make(chan string, 100)
|
|
||||||
cd := NewConfigDeployer(logChan)
|
|
||||||
|
|
||||||
replaceConfigs := map[string]bool{
|
|
||||||
"Niri": false,
|
|
||||||
"Hyprland": false,
|
|
||||||
"Ghostty": true, // explicitly true
|
|
||||||
"Kitty": false,
|
|
||||||
"Alacritty": false,
|
|
||||||
}
|
|
||||||
|
|
||||||
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
|
||||||
context.Background(),
|
|
||||||
deps.WindowManagerNiri,
|
|
||||||
deps.TerminalGhostty,
|
|
||||||
nil, // installedDeps
|
|
||||||
replaceConfigs, // Ghostty=true, rest=false
|
|
||||||
nil, // reinstallItems
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Ghostty should be deployed because replaceConfigs["Ghostty"]=true
|
|
||||||
foundGhostty := false
|
|
||||||
for _, r := range results {
|
|
||||||
if r.ConfigType == "Ghostty" && r.Deployed {
|
|
||||||
foundGhostty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
|||||||
|
|
||||||
# === Sizing & Layout ===
|
# === Sizing & Layout ===
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
bind = SUPER, R, layoutmsg, togglesplit
|
||||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
bind = SUPER CTRL, F, resizeactive, exact 100%
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||||
|
|||||||
@@ -94,19 +94,22 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
|||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
windowrule = tile on, match:class ^(pavucontrol)$
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||||
|
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
windowrule = float on, match:class ^(gnome-calculator)$
|
||||||
windowrule = float on, match:class ^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
||||||
|
|
||||||
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
windowrule = noinitialfocus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||||
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||||
|
|
||||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||||
windowrule = float on, match:class ^(zoom)$
|
windowrule = float on, match:class ^(zoom)$
|
||||||
|
|
||||||
|
# DMS windows floating by default
|
||||||
|
# ! Hyprland doesn't size these windows correctly so disabling by default here
|
||||||
|
# windowrule = float on, match:class ^(org.quickshell)$
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
layerrule = no_anim on, match:namespace ^dms:.*
|
layerrule = no_anim on, match:namespace ^dms:.*
|
||||||
|
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"^org\.gnome\.Calculator$"#
|
|
||||||
match app-id=r#"^gnome-calculator$"#
|
match app-id=r#"^gnome-calculator$"#
|
||||||
match app-id=r#"^galculator$"#
|
match app-id=r#"^galculator$"#
|
||||||
match app-id=r#"^blueman-manager$"#
|
match app-id=r#"^blueman-manager$"#
|
||||||
@@ -250,6 +249,11 @@ window-rule {
|
|||||||
match app-id="zoom"
|
match app-id="zoom"
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
// Open dms windows as floating by default
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"org.quickshell$"#
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
debug {
|
debug {
|
||||||
honor-xdg-activation-with-invalid-serial
|
honor-xdg-activation-with-invalid-serial
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -27,9 +26,6 @@ func init() {
|
|||||||
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
|
||||||
return NewArchDistribution(config, logChan)
|
|
||||||
})
|
|
||||||
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
@@ -98,7 +94,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectGit())
|
dependencies = append(dependencies, a.detectGit())
|
||||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, a.detectQuickshell())
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
dependencies = append(dependencies, a.detectDMSGreeter())
|
|
||||||
dependencies = append(dependencies, a.detectXDGPortal())
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
dependencies = append(dependencies, a.detectAccountsService())
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
@@ -126,52 +121,12 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
|
||||||
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", pkg)
|
cmd := exec.Command("pacman", "-Q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
|
||||||
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
|
||||||
data, err := os.ReadFile(srcinfoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
var pkg string
|
|
||||||
var target *[]string
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(line, "makedepends = "):
|
|
||||||
pkg = strings.TrimPrefix(line, "makedepends = ")
|
|
||||||
target = &makedeps
|
|
||||||
case strings.HasPrefix(line, "depends = "):
|
|
||||||
pkg = strings.TrimPrefix(line, "depends = ")
|
|
||||||
target = &deps
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
|
||||||
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
|
||||||
pkg = pkg[:idx]
|
|
||||||
}
|
|
||||||
pkg = strings.TrimSpace(pkg)
|
|
||||||
if pkg != "" {
|
|
||||||
*target = append(*target, pkg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deps, makedeps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
|
||||||
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -181,7 +136,6 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||||
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
|
||||||
"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},
|
||||||
@@ -208,7 +162,8 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
|
|||||||
if forceQuickshellGit || variant == deps.VariantGit {
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
|
// ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
@@ -242,7 +197,11 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
|||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
if a.packageInstalled("dms-shell-bin") {
|
||||||
|
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -292,7 +251,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
|
|||||||
LogOutput: "Installing base-devel development tools",
|
LogOutput: "Installing base-devel development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||||
}
|
}
|
||||||
@@ -324,19 +283,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
|
|
||||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
|
|
||||||
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
|
|
||||||
}
|
|
||||||
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(systemPkgs, "quickshell") && a.packageInstalled("quickshell-git") {
|
|
||||||
if err := a.removeQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove quickshell-git: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: System Packages
|
// Phase 3: System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -454,51 +400,6 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) removeQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.33,
|
|
||||||
Step: "Removing quickshell-git...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell-git",
|
|
||||||
LogOutput: "Removing quickshell-git so stable quickshell can be installed",
|
|
||||||
}
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell-git")
|
|
||||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.33, 0.35)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if a.packageInstalled("quickshell-git") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.packageInstalled("quickshell") {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.15,
|
|
||||||
Step: "Removing stable quickshell...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
|
||||||
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
|
||||||
}
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.18,
|
|
||||||
Step: "Building quickshell-git before system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
|
|
||||||
}
|
|
||||||
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -507,9 +408,6 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||||
if slices.Contains(packages, "dms-shell") {
|
|
||||||
args = append(args, "--assume-installed", "dms-shell-compositor=1")
|
|
||||||
}
|
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -521,7 +419,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,10 +431,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
|
|||||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
hasNiri := false
|
hasNiri := false
|
||||||
|
hasQuickshell := false
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "niri-git" {
|
if pkg == "niri-git" {
|
||||||
hasNiri = true
|
hasNiri = true
|
||||||
}
|
}
|
||||||
|
if pkg == "quickshell" || pkg == "quickshell-git" {
|
||||||
|
hasQuickshell = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If quickshell is in the list, always reinstall google-breakpad first
|
||||||
|
if hasQuickshell {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.63,
|
||||||
|
Step: "Reinstalling google-breakpad for quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
||||||
|
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
@@ -597,7 +514,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
var dmsShell []string
|
var dmsShell []string
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "dms-shell-git" {
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
@@ -617,16 +534,6 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
|
||||||
if visited[pkg] {
|
|
||||||
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
visited[pkg] = true
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -678,7 +585,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pkg == "dms-shell-git" {
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
depsToRemove := []string{
|
depsToRemove := []string{
|
||||||
"depends = quickshell",
|
"depends = quickshell",
|
||||||
@@ -700,66 +607,54 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||||
{
|
// since we manually manage those dependencies
|
||||||
|
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||||
|
// Pre-install dependencies from .SRCINFO
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "Classifying dependencies as system or AUR",
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
// Install dependencies and makedepends explicitly
|
||||||
if err != nil {
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
|
||||||
|
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf(`
|
||||||
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
if [[ "%s" == *"quickshell"* ]]; then
|
||||||
|
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
||||||
|
fi
|
||||||
|
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||||
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||||
|
fi
|
||||||
|
`, srcinfoPath, pkg, sudoPassword))
|
||||||
|
|
||||||
|
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||||
|
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
seen := make(map[string]bool)
|
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
var systemPkgs []string
|
fmt.Sprintf(`
|
||||||
var aurPkgs []string
|
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
||||||
|
if [ ! -z "$makedeps" ]; then
|
||||||
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
||||||
|
fi
|
||||||
|
`, srcinfoPath, sudoPassword))
|
||||||
|
|
||||||
for _, dep := range append(runtimeDeps, makeDeps...) {
|
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
||||||
if seen[dep] || a.packageInstalled(dep) {
|
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[dep] = true
|
|
||||||
if a.isInSystemRepo(dep) {
|
|
||||||
systemPkgs = append(systemPkgs, dep)
|
|
||||||
} else {
|
|
||||||
aurPkgs = append(aurPkgs, dep)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if len(systemPkgs) > 0 {
|
progressChan <- InstallProgressMsg{
|
||||||
progressChan <- InstallProgressMsg{
|
Phase: PhaseAURPackages,
|
||||||
Phase: PhaseAURPackages,
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
Progress: startProgress + 0.32*(endProgress-startProgress),
|
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||||
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
IsComplete: false,
|
||||||
IsComplete: false,
|
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||||
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
|
||||||
}
|
|
||||||
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, aurDep := range aurPkgs {
|
|
||||||
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
|
||||||
}
|
|
||||||
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
|
||||||
startProgress+0.35*(endProgress-startProgress),
|
|
||||||
startProgress+0.39*(endProgress-startProgress),
|
|
||||||
visited,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +668,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
buildCmd.Dir = packageDir
|
buildCmd.Dir = packageDir
|
||||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
||||||
|
|
||||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
@@ -788,9 +683,42 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
CommandInfo: "sudo pacman -U built-package",
|
CommandInfo: "sudo pacman -U built-package",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||||
var files []string
|
var files []string
|
||||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
files = matches
|
// For DMS split packages, install base package
|
||||||
|
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err == nil {
|
||||||
|
for _, match := range matches {
|
||||||
|
basename := filepath.Base(match)
|
||||||
|
// Always include base package
|
||||||
|
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
||||||
|
files = append(files, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update compositor-specific packages if they're installed
|
||||||
|
if strings.HasSuffix(pkg, "-git") {
|
||||||
|
if a.packageInstalled("dms-shell-hyprland-git") {
|
||||||
|
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
||||||
|
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
||||||
|
files = append(files, hyprlandMatches[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.packageInstalled("dms-shell-niri-git") {
|
||||||
|
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
||||||
|
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
||||||
|
files = append(files, niriMatches[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For other packages, install all built packages
|
||||||
|
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||||
|
files = matches
|
||||||
|
}
|
||||||
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return fmt.Errorf("no package files found after building %s", pkg)
|
return fmt.Errorf("no package files found after building %s", pkg)
|
||||||
@@ -799,7 +727,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||||
installArgs = append(installArgs, files...)
|
installArgs = append(installArgs, files...)
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||||
|
|
||||||
fileNames := make([]string, len(files))
|
fileNames := make([]string, len(files))
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,6 +55,27 @@ func (b *BaseDistribution) logError(message string, err error) {
|
|||||||
b.log(errorMsg)
|
b.log(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||||
|
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||||
|
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||||
|
func escapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||||
|
// This helper escapes special characters in the password to prevent shell injection
|
||||||
|
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||||
|
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||||
|
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||||
|
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||||
|
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||||
|
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists(name) {
|
if b.commandExists(name) {
|
||||||
@@ -82,19 +102,6 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if installed {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: name,
|
|
||||||
Status: status,
|
|
||||||
Description: description,
|
|
||||||
Required: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||||
return b.detectCommand("git", "Version control system")
|
return b.detectCommand("git", "Version control system")
|
||||||
}
|
}
|
||||||
@@ -232,7 +239,7 @@ func (b *BaseDistribution) detectQuickshell() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
versionStr := string(output)
|
versionStr := string(output)
|
||||||
versionRegex := regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||||
matches := versionRegex.FindStringSubmatch(versionStr)
|
matches := versionRegex.FindStringSubmatch(versionStr)
|
||||||
|
|
||||||
if len(matches) < 2 {
|
if len(matches) < 2 {
|
||||||
@@ -690,7 +697,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install to /usr/local/bin
|
// Install to /usr/local/bin
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, d.detectGit())
|
dependencies = append(dependencies, d.detectGit())
|
||||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, d.detectQuickshell())
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
dependencies = append(dependencies, d.detectDMSGreeter())
|
|
||||||
dependencies = append(dependencies, d.detectXDGPortal())
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
dependencies = append(dependencies, d.detectAccountsService())
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
@@ -87,32 +86,10 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
|
||||||
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
return debianPackageInstalledPrecisely(pkg)
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
}
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
func debianPackageInstalledPrecisely(pkg string) bool {
|
|
||||||
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(output)) == "installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
func debianRepoArchitecture(arch string) string {
|
|
||||||
switch arch {
|
|
||||||
case "amd64", "x86_64":
|
|
||||||
return "amd64"
|
|
||||||
case "arm64", "aarch64":
|
|
||||||
return "arm64"
|
|
||||||
default:
|
|
||||||
return arch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -131,7 +108,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
// DMS packages from OBS with variant support
|
// DMS packages from OBS with variant support
|
||||||
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"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"},
|
||||||
@@ -183,7 +159,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -200,7 +176,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -212,12 +188,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: "Installing development dependencies...",
|
Step: "Installing development dependencies...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -397,14 +373,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-install-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
@@ -442,7 +410,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
// Create keyrings directory if it doesn't exist
|
// Create keyrings directory if it doesn't exist
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
if err := mkdirCmd.Run(); err != nil {
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
}
|
}
|
||||||
@@ -456,13 +424,13 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -472,7 +440,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
@@ -492,7 +460,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
}
|
}
|
||||||
@@ -508,46 +476,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
totalGroups := len(groups)
|
args = append(args, packages...)
|
||||||
|
|
||||||
groupIndex := 0
|
progressChan <- InstallProgressMsg{
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
Phase: PhaseSystemPackages,
|
||||||
if len(groupPackages) == 0 {
|
Progress: 0.40,
|
||||||
return nil
|
Step: "Installing system packages...",
|
||||||
}
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
groupIndex++
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
startProgress := 0.40
|
|
||||||
endProgress := 0.60
|
|
||||||
if totalGroups > 1 {
|
|
||||||
if groupIndex == 1 {
|
|
||||||
endProgress = 0.50
|
|
||||||
} else {
|
|
||||||
startProgress = 0.50
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := d.aptInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: startProgress,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -626,7 +568,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +586,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -683,7 +625,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
|
||||||
return NewFedoraDistribution(config, logChan)
|
|
||||||
})
|
|
||||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
@@ -79,7 +75,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectGit())
|
dependencies = append(dependencies, f.detectGit())
|
||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
dependencies = append(dependencies, f.detectDMSGreeter())
|
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
dependencies = append(dependencies, f.detectAccountsService())
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
@@ -125,7 +120,6 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
|
||||||
"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"},
|
||||||
@@ -197,10 +191,6 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
|
|
||||||
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) getPrerequisites() []string {
|
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"dnf-plugins-core",
|
"dnf-plugins-core",
|
||||||
@@ -255,7 +245,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logError("failed to install prerequisites", err)
|
f.logError("failed to install prerequisites", err)
|
||||||
@@ -438,7 +428,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -462,7 +452,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -485,7 +475,28 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -495,57 +506,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
if minimal {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return append(args, packages...)
|
|
||||||
}
|
args = append(args, packages...)
|
||||||
|
|
||||||
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
progressChan <- InstallProgressMsg{
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
Phase: PhaseAURPackages,
|
||||||
totalGroups := len(groups)
|
Progress: 0.70,
|
||||||
|
Step: "Installing COPR packages...",
|
||||||
groupIndex := 0
|
IsComplete: false,
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
NeedsSudo: true,
|
||||||
if len(groupPackages) == 0 {
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
groupIndex++
|
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := f.dnfInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range groups {
|
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var GentooGlobalUseFlags = []string{
|
var GentooGlobalUseFlags = []string{
|
||||||
@@ -202,9 +201,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if hasUse {
|
if hasUse {
|
||||||
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
} else {
|
} else {
|
||||||
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
@@ -282,7 +281,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
if syncErr != nil {
|
if syncErr != nil {
|
||||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
@@ -303,7 +302,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logError("failed to install prerequisites", err)
|
g.logError("failed to install prerequisites", err)
|
||||||
@@ -504,14 +503,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||||
packageUseDir := "/etc/portage/package.use"
|
packageUseDir := "/etc/portage/package.use"
|
||||||
|
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -525,7 +524,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -533,7 +532,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -558,7 +557,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable GURU repository
|
// Enable GURU repository
|
||||||
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
|
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
output, err := enableCmd.CombinedOutput()
|
output, err := enableCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -590,7 +589,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
LogOutput: "Syncing GURU repository",
|
LogOutput: "Syncing GURU repository",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
|
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -623,7 +622,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
|
|
||||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||||
|
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -637,7 +636,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -645,7 +644,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -696,6 +695,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ const (
|
|||||||
PhaseAURPackages
|
PhaseAURPackages
|
||||||
PhaseCursorTheme
|
PhaseCursorTheme
|
||||||
PhaseConfiguration
|
PhaseConfiguration
|
||||||
PhaseGreeterSetup
|
|
||||||
PhaseComplete
|
PhaseComplete
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualPackageInstaller provides methods for installing packages from source
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
@@ -144,7 +143,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
m.logError("failed to install dgop", err)
|
m.logError("failed to install dgop", err)
|
||||||
@@ -214,7 +213,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "dpkg -i niri.deb",
|
CommandInfo: "dpkg -i niri.deb",
|
||||||
}
|
}
|
||||||
|
|
||||||
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
|
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||||
|
|
||||||
output, err := installDebCmd.CombinedOutput()
|
output, err := installDebCmd.CombinedOutput()
|
||||||
@@ -325,7 +324,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -388,7 +387,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
@@ -454,7 +453,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
@@ -493,11 +492,16 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,11 +646,15 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package distros
|
|
||||||
|
|
||||||
type minimalInstallGroup struct {
|
|
||||||
packages []string
|
|
||||||
minimal bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldPreferMinimalInstall(pkg string) bool {
|
|
||||||
switch pkg {
|
|
||||||
case "niri", "niri-git":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if shouldPreferMinimalInstall(pkg) {
|
|
||||||
minimal = append(minimal, pkg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
normal = append(normal, pkg)
|
|
||||||
}
|
|
||||||
return normal, minimal
|
|
||||||
}
|
|
||||||
|
|
||||||
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
|
||||||
normal, minimal := splitMinimalInstallPackages(packages)
|
|
||||||
groups := make([]minimalInstallGroup, 0, 2)
|
|
||||||
if len(minimal) > 0 {
|
|
||||||
groups = append(groups, minimalInstallGroup{
|
|
||||||
packages: minimal,
|
|
||||||
minimal: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(normal) > 0 {
|
|
||||||
groups = append(groups, minimalInstallGroup{
|
|
||||||
packages: normal,
|
|
||||||
minimal: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -31,8 +29,6 @@ type OpenSUSEDistribution struct {
|
|||||||
config DistroConfig
|
config DistroConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
|
||||||
|
|
||||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
base := NewBaseDistribution(logChan)
|
base := NewBaseDistribution(logChan)
|
||||||
return &OpenSUSEDistribution{
|
return &OpenSUSEDistribution{
|
||||||
@@ -75,7 +71,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectGit())
|
dependencies = append(dependencies, o.detectGit())
|
||||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, o.detectQuickshell())
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
dependencies = append(dependencies, o.detectDMSGreeter())
|
|
||||||
dependencies = append(dependencies, o.detectXDGPortal())
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
dependencies = append(dependencies, o.detectAccountsService())
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
@@ -105,10 +100,6 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
|
|
||||||
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -125,7 +116,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"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"},
|
||||||
@@ -203,7 +193,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
return []string{}
|
return []string{
|
||||||
|
"make",
|
||||||
|
"unzip",
|
||||||
|
"gcc",
|
||||||
|
"gcc-c++",
|
||||||
|
"cmake",
|
||||||
|
"ninja",
|
||||||
|
"pkgconf-pkg-config",
|
||||||
|
"git",
|
||||||
|
"qt6-base-devel",
|
||||||
|
"qt6-declarative-devel",
|
||||||
|
"qt6-declarative-private-devel",
|
||||||
|
"qt6-shadertools",
|
||||||
|
"qt6-shadertools-devel",
|
||||||
|
"qt6-wayland-devel",
|
||||||
|
"qt6-waylandclient-private-devel",
|
||||||
|
"spirv-tools-devel",
|
||||||
|
"cli11-devel",
|
||||||
|
"wayland-protocols-devel",
|
||||||
|
"libgbm-devel",
|
||||||
|
"libdrm-devel",
|
||||||
|
"pipewire-devel",
|
||||||
|
"jemalloc-devel",
|
||||||
|
"wayland-utils",
|
||||||
|
"Mesa-libGLESv3-devel",
|
||||||
|
"pam-devel",
|
||||||
|
"glib2-devel",
|
||||||
|
"polkit-devel",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -251,7 +269,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
|
|||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
args := []string{"zypper", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logError("failed to install prerequisites", err)
|
o.logError("failed to install prerequisites", err)
|
||||||
@@ -273,10 +291,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting prerequisite check...",
|
LogOutput: "Starting prerequisite check...",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
@@ -307,7 +321,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +336,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,32 +426,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
|
||||||
|
|
||||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
|
||||||
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{openSUSENiriWaylandServerPackage}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
|
||||||
for _, pkg := range extraPkgs {
|
|
||||||
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
|
||||||
systemPkgs = append(systemPkgs, pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemPkgs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
names := make([]string, len(packages))
|
names := make([]string, len(packages))
|
||||||
for i, pkg := range packages {
|
for i, pkg := range packages {
|
||||||
@@ -487,7 +478,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||||
@@ -508,7 +499,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to refresh repositories: %w", err)
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
}
|
}
|
||||||
@@ -517,146 +508,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenSUSEInstallMediaURI(uri string) bool {
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
|
||||||
|
|
||||||
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "hd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "iso:/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseZypperInstallMediaAliases(output string) []string {
|
|
||||||
var aliases []string
|
|
||||||
|
|
||||||
for _, line := range strings.Split(output, "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || !strings.Contains(line, "|") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(line, "|")
|
|
||||||
if len(parts) < 7 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range parts {
|
|
||||||
parts[i] = strings.TrimSpace(parts[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
alias := parts[1]
|
|
||||||
enabled := strings.ToLower(parts[3])
|
|
||||||
uri := parts[len(parts)-1]
|
|
||||||
|
|
||||||
if alias == "" || strings.EqualFold(alias, "alias") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if enabled != "" && enabled != "yes" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !isOpenSUSEInstallMediaURI(uri) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases = append(aliases, alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
return aliases
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
|
||||||
output, err := listCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
|
||||||
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases := parseZypperInstallMediaAliases(string(output))
|
|
||||||
if len(aliases) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhasePrerequisites,
|
|
||||||
Progress: 0.055,
|
|
||||||
Step: "Disabling install media repositories...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
|
||||||
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, alias := range aliases {
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
|
|
||||||
repoOutput, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
|
||||||
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
|
||||||
}
|
|
||||||
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
args := []string{"zypper", "install", "-y"}
|
||||||
totalGroups := len(groups)
|
args = append(args, packages...)
|
||||||
|
|
||||||
groupIndex := 0
|
progressChan <- InstallProgressMsg{
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
Phase: PhaseSystemPackages,
|
||||||
if len(groupPackages) == 0 {
|
Progress: 0.40,
|
||||||
return nil
|
Step: "Installing system packages...",
|
||||||
}
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
groupIndex++
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := o.zypperInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -775,7 +647,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -799,7 +671,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
|||||||
CommandInfo: "sudo zypper install rustup",
|
CommandInfo: "sudo zypper install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -64,7 +63,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectGit())
|
dependencies = append(dependencies, u.detectGit())
|
||||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, u.detectQuickshell())
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
dependencies = append(dependencies, u.detectDMSGreeter())
|
|
||||||
dependencies = append(dependencies, u.detectXDGPortal())
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
dependencies = append(dependencies, u.detectAccountsService())
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
@@ -96,12 +94,10 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
|
||||||
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
return debianPackageInstalledPrecisely(pkg)
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -120,7 +116,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
// DMS packages from PPAs
|
// DMS packages from PPAs
|
||||||
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
|
||||||
"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"},
|
||||||
@@ -178,7 +173,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -196,7 +191,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
// Not installed, install it
|
// Not installed, install it
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -212,7 +207,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -399,7 +394,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y software-properties-common")
|
"apt-get install -y software-properties-common")
|
||||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||||
@@ -417,7 +412,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||||
@@ -438,7 +433,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||||
}
|
}
|
||||||
@@ -453,7 +448,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -462,59 +471,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
args = append(args, packages...)
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-install-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
progressChan <- InstallProgressMsg{
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
Phase: PhaseAURPackages,
|
||||||
totalGroups := len(groups)
|
Progress: 0.70,
|
||||||
|
Step: "Installing PPA packages...",
|
||||||
groupIndex := 0
|
IsComplete: false,
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
NeedsSudo: true,
|
||||||
if len(groupPackages) == 0 {
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
groupIndex++
|
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := u.aptInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -592,7 +563,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +581,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -650,7 +621,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||||
}
|
}
|
||||||
|
|
||||||
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||||
@@ -665,7 +636,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||||
}
|
}
|
||||||
@@ -679,7 +650,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package geolocation
|
|
||||||
|
|
||||||
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
|
|
||||||
func NewClient() Client {
|
|
||||||
geoclueClient, err := newGeoClueClient()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("GeoClue2 unavailable: %v", err)
|
|
||||||
return newSeededIpClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
loc, _ := geoclueClient.GetLocation()
|
|
||||||
if loc.Latitude != 0 || loc.Longitude != 0 {
|
|
||||||
log.Info("Using GeoClue2 location")
|
|
||||||
return geoclueClient
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("GeoClue2 has no fix yet, seeding with IP location")
|
|
||||||
ipLoc, err := fetchIPLocation()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("IP location seed failed: %v", err)
|
|
||||||
return geoclueClient
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Seeded GeoClue2 with IP location")
|
|
||||||
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
|
|
||||||
return geoclueClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSeededIpClient() *IpClient {
|
|
||||||
client := newIpClient()
|
|
||||||
ipLoc, err := fetchIPLocation()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("IP location also failed: %v", err)
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Using IP location")
|
|
||||||
client.currLocation.Latitude = ipLoc.Latitude
|
|
||||||
client.currLocation.Longitude = ipLoc.Longitude
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
package geolocation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
|
||||||
"github.com/godbus/dbus/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dbusGeoClueService = "org.freedesktop.GeoClue2"
|
|
||||||
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
|
|
||||||
dbusGeoClueInterface = dbusGeoClueService
|
|
||||||
|
|
||||||
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
|
|
||||||
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
|
|
||||||
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
|
|
||||||
|
|
||||||
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
|
|
||||||
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
|
|
||||||
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
|
|
||||||
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
|
|
||||||
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
|
|
||||||
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
|
|
||||||
|
|
||||||
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
|
|
||||||
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
|
|
||||||
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GeoClueClient struct {
|
|
||||||
currLocation *Location
|
|
||||||
locationMutex sync.RWMutex
|
|
||||||
|
|
||||||
dbusConn *dbus.Conn
|
|
||||||
clientPath dbus.ObjectPath
|
|
||||||
signals chan *dbus.Signal
|
|
||||||
|
|
||||||
stopChan chan struct{}
|
|
||||||
sigWG sync.WaitGroup
|
|
||||||
|
|
||||||
subscribers syncmap.Map[string, chan Location]
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGeoClueClient() (*GeoClueClient, error) {
|
|
||||||
dbusConn, err := dbus.ConnectSystemBus()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &GeoClueClient{
|
|
||||||
dbusConn: dbusConn,
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
signals: make(chan *dbus.Signal, 256),
|
|
||||||
|
|
||||||
currLocation: &Location{
|
|
||||||
Latitude: 0.0,
|
|
||||||
Longitude: 0.0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.setupClient(); err != nil {
|
|
||||||
dbusConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.startSignalPump(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) Close() {
|
|
||||||
close(c.stopChan)
|
|
||||||
|
|
||||||
c.sigWG.Wait()
|
|
||||||
|
|
||||||
if c.signals != nil {
|
|
||||||
c.dbusConn.RemoveSignal(c.signals)
|
|
||||||
close(c.signals)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
|
||||||
close(ch)
|
|
||||||
c.subscribers.Delete(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if c.dbusConn != nil {
|
|
||||||
c.dbusConn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) Subscribe(id string) chan Location {
|
|
||||||
ch := make(chan Location, 64)
|
|
||||||
c.subscribers.Store(id, ch)
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) Unsubscribe(id string) {
|
|
||||||
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) setupClient() error {
|
|
||||||
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
|
|
||||||
|
|
||||||
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
|
||||||
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
|
|
||||||
return fmt.Errorf("failed to set desktop ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
|
|
||||||
return fmt.Errorf("failed to set time threshold: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) startSignalPump() error {
|
|
||||||
c.dbusConn.Signal(c.signals)
|
|
||||||
|
|
||||||
if err := c.dbusConn.AddMatchSignal(
|
|
||||||
dbus.WithMatchObjectPath(c.clientPath),
|
|
||||||
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
|
||||||
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.sigWG.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer c.sigWG.Done()
|
|
||||||
|
|
||||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
|
||||||
clientObj.Call(dbusGeoClueClientTimeStart, 0)
|
|
||||||
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.stopChan:
|
|
||||||
return
|
|
||||||
case sig, ok := <-c.signals:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sig == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
c.handleSignal(sig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
|
|
||||||
switch sig.Name {
|
|
||||||
case dbusGeoClueClientLocationUpdated:
|
|
||||||
if len(sig.Body) != 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.handleLocationUpdated(newLocationPath); err != nil {
|
|
||||||
log.Warn("GeoClue: Failed to handle location update: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
|
|
||||||
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
|
|
||||||
|
|
||||||
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.locationMutex.Lock()
|
|
||||||
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
|
|
||||||
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
|
|
||||||
c.locationMutex.Unlock()
|
|
||||||
|
|
||||||
c.notifySubscribers()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) notifySubscribers() {
|
|
||||||
currentLocation, err := c.GetLocation()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
|
||||||
select {
|
|
||||||
case ch <- currentLocation:
|
|
||||||
default:
|
|
||||||
log.Warn("GeoClue: subscriber channel full, dropping update")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) SeedLocation(loc Location) {
|
|
||||||
c.locationMutex.Lock()
|
|
||||||
defer c.locationMutex.Unlock()
|
|
||||||
c.currLocation.Latitude = loc.Latitude
|
|
||||||
c.currLocation.Longitude = loc.Longitude
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeoClueClient) GetLocation() (Location, error) {
|
|
||||||
c.locationMutex.RLock()
|
|
||||||
defer c.locationMutex.RUnlock()
|
|
||||||
if c.currLocation == nil {
|
|
||||||
return Location{
|
|
||||||
Latitude: 0.0,
|
|
||||||
Longitude: 0.0,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
stateCopy := *c.currLocation
|
|
||||||
return stateCopy, nil
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package geolocation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IpClient struct {
|
|
||||||
currLocation *Location
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipLocationResult struct {
|
|
||||||
Location
|
|
||||||
City string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipAPIResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Lat float64 `json:"lat"`
|
|
||||||
Lon float64 `json:"lon"`
|
|
||||||
City string `json:"city"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIpClient() *IpClient {
|
|
||||||
return &IpClient{
|
|
||||||
currLocation: &Location{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *IpClient) Subscribe(id string) chan Location {
|
|
||||||
ch := make(chan Location, 1)
|
|
||||||
if location, err := c.GetLocation(); err == nil {
|
|
||||||
ch <- location
|
|
||||||
}
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *IpClient) Unsubscribe(id string) {}
|
|
||||||
|
|
||||||
func (c *IpClient) Close() {}
|
|
||||||
|
|
||||||
func (c *IpClient) GetLocation() (Location, error) {
|
|
||||||
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
|
|
||||||
return *c.currLocation, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := fetchIPLocation()
|
|
||||||
if err != nil {
|
|
||||||
return Location{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.currLocation.Latitude = result.Latitude
|
|
||||||
c.currLocation.Longitude = result.Longitude
|
|
||||||
return *c.currLocation, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIPLocation() (ipLocationResult, error) {
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
|
|
||||||
resp, err := client.Get("http://ip-api.com/json/")
|
|
||||||
if err != nil {
|
|
||||||
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data ipAPIResponse
|
|
||||||
if err := json.Unmarshal(body, &data); err != nil {
|
|
||||||
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
|
|
||||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipLocationResult{
|
|
||||||
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
|
|
||||||
City: data.City,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package geolocation
|
|
||||||
|
|
||||||
type Location struct {
|
|
||||||
Latitude float64
|
|
||||||
Longitude float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
GetLocation() (Location, error)
|
|
||||||
|
|
||||||
Subscribe(id string) chan Location
|
|
||||||
Unsubscribe(id string)
|
|
||||||
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# AppArmor profile for dms-greeter
|
|
||||||
#
|
|
||||||
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
|
||||||
# Manual edits will be overwritten on next sync.
|
|
||||||
#
|
|
||||||
# Mode: complain (denials are logged, nothing is blocked)
|
|
||||||
# To switch to enforce after validating with `aa-logprof`:
|
|
||||||
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
|
||||||
#
|
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
|
||||||
#include <abstractions/base>
|
|
||||||
#include <abstractions/bash>
|
|
||||||
|
|
||||||
# The launcher script itself
|
|
||||||
/usr/bin/dms-greeter r,
|
|
||||||
|
|
||||||
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
|
||||||
/var/cache/dms-greeter/ rw,
|
|
||||||
/var/cache/dms-greeter/** rwlk,
|
|
||||||
|
|
||||||
# DMS config — packaged path
|
|
||||||
/usr/share/quickshell/dms-greeter/ r,
|
|
||||||
/usr/share/quickshell/dms-greeter/** r,
|
|
||||||
/usr/share/quickshell/ r,
|
|
||||||
/usr/share/quickshell/** r,
|
|
||||||
|
|
||||||
# DMS config — system and user overrides
|
|
||||||
/etc/dms/ r,
|
|
||||||
/etc/dms/** r,
|
|
||||||
/usr/share/dms/ r,
|
|
||||||
/usr/share/dms/** r,
|
|
||||||
/home/*/.config/quickshell/ r,
|
|
||||||
/home/*/.config/quickshell/** r,
|
|
||||||
/root/.config/quickshell/ r,
|
|
||||||
/root/.config/quickshell/** r,
|
|
||||||
|
|
||||||
# greetd / PAM — read-only for session setup
|
|
||||||
/etc/greetd/ r,
|
|
||||||
/etc/greetd/** r,
|
|
||||||
/etc/pam.d/ r,
|
|
||||||
/etc/pam.d/** r,
|
|
||||||
/usr/lib/pam.d/ r,
|
|
||||||
/usr/lib/pam.d/** r,
|
|
||||||
|
|
||||||
# Compositor binaries — run unconfined so each compositor uses its own profile
|
|
||||||
/usr/bin/niri Ux,
|
|
||||||
/usr/bin/hyprland Ux,
|
|
||||||
/usr/bin/Hyprland Ux,
|
|
||||||
/usr/bin/sway Ux,
|
|
||||||
/usr/bin/labwc Ux,
|
|
||||||
/usr/bin/scroll Ux,
|
|
||||||
/usr/bin/miracle-wm Ux,
|
|
||||||
/usr/bin/mango Ux,
|
|
||||||
|
|
||||||
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
|
||||||
/usr/bin/qs Ux,
|
|
||||||
/usr/bin/quickshell Ux,
|
|
||||||
|
|
||||||
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
|
||||||
/run/user/[0-9]*/ rw,
|
|
||||||
/run/user/[0-9]*/** rw,
|
|
||||||
|
|
||||||
# DRM / GPU devices (required for Wayland compositor startup)
|
|
||||||
/dev/dri/ r,
|
|
||||||
/dev/dri/* rw,
|
|
||||||
/dev/udmabuf rw,
|
|
||||||
|
|
||||||
# Input devices
|
|
||||||
/dev/input/ r,
|
|
||||||
/dev/input/* r,
|
|
||||||
|
|
||||||
# Systemd journal / logging
|
|
||||||
/run/systemd/journal/socket rw,
|
|
||||||
/dev/log rw,
|
|
||||||
|
|
||||||
# Shell helper binaries invoked by the launcher script
|
|
||||||
/usr/bin/env ix,
|
|
||||||
/usr/bin/mkdir ix,
|
|
||||||
/usr/bin/cat ix,
|
|
||||||
/usr/bin/grep ix,
|
|
||||||
/usr/bin/dirname ix,
|
|
||||||
/usr/bin/basename ix,
|
|
||||||
/usr/bin/command ix,
|
|
||||||
/bin/env ix,
|
|
||||||
/bin/mkdir ix,
|
|
||||||
|
|
||||||
# Signal management (compositor lifecycle)
|
|
||||||
signal (send, receive) set=("term", "int", "hup", "kill"),
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,98 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeTestFile(t *testing.T, path string, content string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write %s: %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
settingsJSON string
|
|
||||||
sessionJSON string
|
|
||||||
wantSourcePath string
|
|
||||||
wantResolvedWallpaper string
|
|
||||||
wantDynamicOverrideUsed bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "dynamic",
|
|
||||||
"greeterWallpaperPath": "Pictures/blue.jpg",
|
|
||||||
"matugenScheme": "scheme-tonal-spot",
|
|
||||||
"iconTheme": "Papirus"
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":true}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
|
||||||
wantDynamicOverrideUsed: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dynamic theme without override uses desktop colors",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "dynamic",
|
|
||||||
"greeterWallpaperPath": ""
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":false}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: "",
|
|
||||||
wantDynamicOverrideUsed: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "purple",
|
|
||||||
"greeterWallpaperPath": "/tmp/blue.jpg"
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":false}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: "/tmp/blue.jpg",
|
|
||||||
wantDynamicOverrideUsed: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
|
||||||
writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
|
||||||
|
|
||||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
|
||||||
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
|
||||||
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
|
||||||
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
|
||||||
}
|
|
||||||
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
|
||||||
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
|
||||||
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user