1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-03 03:59:11 -04:00

Compare commits

..

5 Commits

Author SHA1 Message Date
purian23 e791337ae9 fix(lint): drop unused constraints import 2026-06-27 22:35:33 -04:00
purian23 a8c15fcde9 feat(popouts): enhance hover functionality and introduce hover dismiss features 2026-06-27 22:21:03 -04:00
purian23 601d4104a3 feat(popouts): hover & settings cleanup 2026-06-27 01:59:45 -04:00
purian23 7979fb2b0e feat(popouts): complete initial hover implementation 2026-06-26 23:34:46 -04:00
purian23 06fa21118e feat(popouts): implement hover popout functionality 2026-06-26 23:34:46 -04:00
537 changed files with 13126 additions and 37669 deletions
-1
View File
@@ -24,7 +24,6 @@ jobs:
check-updates: check-updates:
name: Check for updates name: Check for updates
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
outputs: outputs:
has_updates: ${{ steps.check.outputs.has_updates }} has_updates: ${{ steps.check.outputs.has_updates }}
-1
View File
@@ -24,7 +24,6 @@ jobs:
check-updates: check-updates:
name: Check package/series updates name: Check package/series updates
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
outputs: outputs:
has_updates: ${{ steps.check.outputs.has_updates }} has_updates: ${{ steps.check.outputs.has_updates }}
-268
View File
@@ -1,268 +0,0 @@
name: Void Linux XBPS Repository
on:
schedule:
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
release:
types: [published]
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuilding packages even if they already exist in the repository'
type: boolean
required: false
default: false
build_git:
description: 'Build dms-git package'
type: boolean
required: false
default: true
build_dms:
description: 'Build stable dms package'
type: boolean
required: false
default: true
build_greeter:
description: 'Build stable dms-greeter package'
type: boolean
required: false
default: true
permissions:
contents: write
jobs:
build-and-deploy:
name: Build & Deploy XBPS packages
runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Checkout gh-pages branch
run: |
git clone --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages-repo || {
echo "⚠️ gh-pages branch not found or empty, initializing a new one..."
mkdir gh-pages-repo
cd gh-pages-repo
git init
git checkout -b gh-pages
git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
cd ..
}
- name: Install XBPS Static Utilities
run: |
mkdir -p ${{ github.workspace }}/xbps
curl -s -L https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz | tar -xJ -C ${{ github.workspace }}/xbps
echo "${{ github.workspace }}/xbps/usr/bin" >> $GITHUB_PATH
- name: Clone void-packages
run: |
git clone --depth=1 https://github.com/void-linux/void-packages.git
- name: Inject templates
run: |
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- name: Bootstrap xbps-src
run: |
cd void-packages
./xbps-src binary-bootstrap
- name: Configure signing keys and trust
run: |
# Write private key; extract and register the public key
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
chmod 600 /tmp/xbps_privkey.pem
# Extract public key in PEM format
openssl rsa -in /tmp/xbps_privkey.pem -pubout -out /tmp/dms-key.pub
rm -f /tmp/xbps_privkey.pem
# Compute MD5 fingerprint in colon-separated hex format
FINGERPRINT=$(openssl rsa -pubin -in /tmp/dms-key.pub -outform DER 2>/dev/null | openssl dgst -md5 -c | tr '[:upper:]' '[:lower:]' | awk '{print $NF}')
# Format key in XML property list (plist) format as expected by xbps
mkdir -p /tmp/keys
cat <<EOF > "/tmp/keys/${FINGERPRINT}.plist"
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>public-key</key>
<data>$(base64 -w0 /tmp/dms-key.pub)</data>
<key>public-key-size</key>
<integer>4096</integer>
<key>signature-by</key>
<string>AvengeMedia</string>
</dict>
</plist>
EOF
# Copy keys to all host and chroot trust stores
sudo mkdir -p /var/db/xbps/keys
sudo cp "/tmp/keys/${FINGERPRINT}.plist" "/var/db/xbps/keys/${FINGERPRINT}.plist"
mkdir -p void-packages/masterdir/var/db/xbps/keys
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/masterdir/var/db/xbps/keys/${FINGERPRINT}.plist"
mkdir -p void-packages/etc/xbps.d/keys
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/etc/xbps.d/keys/${FINGERPRINT}.plist"
mkdir -p void-packages/common/repo-keys
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/common/repo-keys/${FINGERPRINT}.plist"
rm -rf /tmp/keys /tmp/dms-key.pub
- name: Configure repositories
run: |
# Append the repository to repos-remote templates so xbps-src translates it automatically
echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
# Add any existing compiled packages to the build cache directory to avoid rebuilds
if [ -d "gh-pages-repo/current" ]; then
mkdir -p void-packages/hostdir/binpkgs
cp -L gh-pages-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
fi
- name: Build packages
run: |
mkdir -p gh-pages-repo/current
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ]; then
BUILD_DMS="false"
BUILD_GREETER="false"
BUILD_GIT="true"
fi
if [ "${{ github.event_name }}" = "release" ]; then
BUILD_DMS="true"
BUILD_GREETER="true"
BUILD_GIT="false"
fi
echo "=== Starting Builds ==="
echo "DMS stable build enabled: $BUILD_DMS"
echo "Greeter stable build enabled: $BUILD_GREETER"
echo "Git build enabled: $BUILD_GIT"
echo "Force rebuild: $FORCE_REBUILD"
cd void-packages
# 1. Build dms-git (development package)
if [ "$BUILD_GIT" = "true" ]; then
# Calculate dynamic git version (tag.commits.hash)
GIT_VER=$(git -C .. describe --tags --always | sed 's/^v//; s/-/./g')
echo "🔨 Preparing dms-git version $GIT_VER"
# Stage source tarball in the xbps-src source cache.
# --transform adds a top-level directory so xbps-src can extract
# into $wrksrc (create_wrksrc=yes handles the rest).
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
mkdir -p "$SRC_CACHE"
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
--exclude=void-packages \
--exclude=gh-pages-repo \
--exclude=.git \
--exclude=danklinux \
-C .. .
CHECKSUM=$(sha256sum "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" | cut -d' ' -f1)
# Dynamically patch template version, checksum, and distfiles
sed -i "s/^version=.*/version=${GIT_VER}/" srcpkgs/dms-git/template
sed -i "s/^checksum=.*/checksum=${CHECKSUM}/" srcpkgs/dms-git/template
sed -i "s|^distfiles=.*|distfiles=\"dms-git-${GIT_VER}.tar.gz\"|" srcpkgs/dms-git/template
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
if [ -f "../gh-pages-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
else
echo "🔨 Compiling dms-git..."
./xbps-src pkg dms-git
rm -f "../gh-pages-repo/current/${EXPECTED_GIT_FILE}"
cp -L hostdir/binpkgs/dms-git-*.xbps ../gh-pages-repo/current/
fi
fi
# 2. Build stable dms package
if [ "$BUILD_DMS" = "true" ]; then
STABLE_VER=$(grep -E '^version=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
if [ -f "../gh-pages-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
else
echo "🔨 Compiling dms ($STABLE_VER)..."
./xbps-src pkg dms
rm -f "../gh-pages-repo/current/${EXPECTED_DMS_FILE}"
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../gh-pages-repo/current/
fi
fi
# 3. Build stable dms-greeter package
if [ "$BUILD_GREETER" = "true" ]; then
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
if [ -f "../gh-pages-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
else
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
./xbps-src pkg dms-greeter
rm -f "../gh-pages-repo/current/${EXPECTED_GREETER_FILE}"
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../gh-pages-repo/current/
fi
fi
- name: Index and sign repository
run: |
cd gh-pages-repo/current
# Clean up any stale or dangling signature files to prevent O_CREAT ENOENT errors
rm -f *.sig2 *.sig
# Guard: nothing to index if no .xbps files exist
if ! ls *.xbps 1>/dev/null 2>&1; then
echo "⚠️ No .xbps files found to index, skipping."
exit 0
fi
# Regenerate repo index
xbps-rindex -a $(pwd)/*.xbps
# Sign repository
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
chmod 600 /tmp/xbps_privkey.pem
xbps-rindex --sign --signedby "AvengeMedia <AvengeMedia.US@gmail.com>" --privkey /tmp/xbps_privkey.pem $(pwd)
xbps-rindex --sign-pkg --privkey /tmp/xbps_privkey.pem $(pwd)/*.xbps
rm -f /tmp/xbps_privkey.pem
- name: Deploy to gh-pages branch
run: |
cd gh-pages-repo
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add current/
git diff --quiet && git diff --staged --quiet || (git commit -m "Update XBPS packages [skip ci]" && git push origin gh-pages)
-3
View File
@@ -110,9 +110,6 @@ bin/
# Core dumps # Core dumps
core.* core.*
# prek-installed local git hooks (generated from .pre-commit-config.yaml)
.githooks/
# direnv # direnv
.envrc .envrc
.direnv/ .direnv/
-8
View File
@@ -28,14 +28,6 @@ repos:
language: system language: system
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$ files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
pass_filenames: false pass_filenames: false
- repo: local
hooks:
- id: i18n-term-freeze
name: i18n term freeze (no new I18n.tr/qsTr terms)
entry: python3 quickshell/translations/check_term_freeze.py
language: system
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
pass_filenames: false
- repo: local - repo: local
hooks: hooks:
- id: no-console-in-qml - id: no-console-in-qml
-2
View File
@@ -77,7 +77,6 @@ install-desktop:
@echo "Installing desktop entries..." @echo "Installing desktop entries..."
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop @install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop @install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.notepad.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries installed" @echo "Desktop entries installed"
@@ -121,7 +120,6 @@ uninstall-desktop:
@echo "Removing desktop entries..." @echo "Removing desktop entries..."
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop @rm -f $(APPLICATIONS_DIR)/dms-open.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop @rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries removed" @echo "Desktop entries removed"
-12
View File
@@ -1,12 +0,0 @@
[Desktop Entry]
Type=Application
Name=DMS Notepad
GenericName=Text Editor
Comment=Quick notes and text files in the dank desktop shell
Exec=dms ipc call notepad openFile %f
Icon=danklogo
Terminal=false
NoDisplay=true
Categories=Utility;TextEditor;
MimeType=text/plain;text/markdown;
StartupNotify=false
+2 -5
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control. Command-line interface and daemon for shell management and system control.
**dankinstall** **dankinstall**
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags. Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
## System Integration ## System Integration
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
## Supported Distributions ## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives) Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
**Arch Linux** **Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency. Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
@@ -214,7 +214,4 @@ Most packages available in standard repos. Minimal building required.
**Gentoo** **Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration. Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
See installer output for distribution-specific details during installation. See installer output for distribution-specific details during installation.
-8
View File
@@ -23,8 +23,6 @@ var (
replaceConfigs []string replaceConfigs []string
replaceConfigsAll bool replaceConfigsAll bool
yes bool yes bool
danksearch bool
dankcalendar bool
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -51,8 +49,6 @@ func init() {
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)") rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations") rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts") rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
} }
func main() { func main() {
@@ -78,8 +74,6 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
"replace-configs", "replace-configs",
"replace-configs-all", "replace-configs-all",
"yes", "yes",
"danksearch",
"dankcalendar",
} }
var set []string var set []string
for _, name := range headlessOnly { for _, name := range headlessOnly {
@@ -115,8 +109,6 @@ func runHeadless() error {
ReplaceConfigs: replaceConfigs, ReplaceConfigs: replaceConfigs,
ReplaceConfigsAll: replaceConfigsAll, ReplaceConfigsAll: replaceConfigsAll,
Yes: yes, Yes: yes,
DankSearch: danksearch,
DankCalendar: dankcalendar,
} }
runner := headless.NewRunner(cfg) runner := headless.NewRunner(cfg)
+1 -18
View File
@@ -63,15 +63,6 @@ var clipPasteCmd = &cobra.Command{
Run: runClipPaste, Run: runClipPaste,
} }
var clipSendPasteCmd = &cobra.Command{
Use: "send-paste",
Short: "Send a paste keystroke to the focused window",
Long: "Emulate ctrl+v (or ctrl+shift+v with --shift) via a virtual keyboard. Works without server.",
Run: runClipSendPaste,
}
var clipSendPasteShift bool
var clipWatchCmd = &cobra.Command{ var clipWatchCmd = &cobra.Command{
Use: "watch [command]", Use: "watch [command]",
Short: "Watch clipboard for changes", Short: "Watch clipboard for changes",
@@ -225,10 +216,8 @@ func init() {
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration") clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
clipSendPasteCmd.Flags().BoolVarP(&clipSendPasteShift, "shift", "s", false, "Send ctrl+shift+v (terminal paste)")
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd) clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipSendPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd) clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
} }
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
@@ -325,12 +314,6 @@ func runClipPaste(cmd *cobra.Command, args []string) {
os.Stdout.Write(data) os.Stdout.Write(data)
} }
func runClipSendPaste(cmd *cobra.Command, args []string) {
if err := clipboard.SendPasteKeystroke(clipSendPasteShift); err != nil {
log.Fatalf("send-paste: %v", err)
}
}
func runClipWatch(cmd *cobra.Command, args []string) { func runClipWatch(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
+5 -197
View File
@@ -106,8 +106,6 @@ func init() {
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
printIPCHelp() printIPCHelp()
}) })
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
} }
var debugSrvCmd = &cobra.Command{ var debugSrvCmd = &cobra.Command{
@@ -186,22 +184,10 @@ var pluginsUninstallCmd = &cobra.Command{
} }
var pluginsUpdateCmd = &cobra.Command{ var pluginsUpdateCmd = &cobra.Command{
Use: "update [plugin-id]", Use: "update <plugin-id>",
Short: "Update a plugin by ID, or all plugins", Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.", Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
Args: func(cmd *cobra.Command, args []string) error { Args: cobra.ExactArgs(1),
updateAll, _ := cmd.Flags().GetBool("all")
if updateAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
}
return nil
}
if len(args) != 1 {
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
}
return nil
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
@@ -209,26 +195,6 @@ var pluginsUpdateCmd = &cobra.Command{
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
checkOnly, _ := cmd.Flags().GetBool("check")
updateAll, _ := cmd.Flags().GetBool("all")
if checkOnly {
if updateAll {
if err := checkAllPluginsCLI(); err != nil {
log.Fatalf("Error checking updates: %v", err)
}
return
}
if err := checkPluginCLI(args[0]); err != nil {
log.Fatalf("Error checking update: %v", err)
}
return
}
if updateAll {
if err := updateAllPluginsCLI(); err != nil {
log.Fatalf("Error updating plugins: %v", err)
}
return
}
if err := updatePluginCLI(args[0]); err != nil { if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err) log.Fatalf("Error updating plugin: %v", err)
} }
@@ -404,11 +370,7 @@ func listInstalledPlugins() error {
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames)) fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
for _, id := range installedNames { for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok { if plugin, ok := pluginMap[id]; ok {
hasUpdateStr := "" fmt.Printf(" %s\n", plugin.Name)
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
hasUpdateStr = " (update available)"
}
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
fmt.Printf(" ID: %s\n", plugin.ID) fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category) fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author) fmt.Printf(" Author: %s\n", plugin.Author)
@@ -588,160 +550,6 @@ func updatePluginCLI(idOrName string) error {
return nil return nil
} }
func updateAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var errs []error
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
if plugin != nil {
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
}
} else {
fmt.Printf("Updating plugin: %s\n", pluginID)
if err := manager.UpdateByIDOrName(pluginID); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", pluginID)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
}
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return fmt.Errorf("failed to update some plugins")
}
return nil
}
func checkPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
} else {
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
}
return nil
}
dummyPlugin := plugins.Plugin{ID: idOrName}
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s\n", idOrName)
} else {
fmt.Printf("Plugin is up to date: %s\n", idOrName)
}
return nil
}
func checkAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var count int
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
var hasUpdates bool
var name string
if plugin != nil {
name = plugin.Name
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
} else {
name = pluginID
dummyPlugin := plugins.Plugin{ID: pluginID}
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
count++
}
}
if count > 0 {
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
} else {
fmt.Println("All plugins are up to date.")
}
return nil
}
func getCommonCommands() []*cobra.Command { func getCommonCommands() []*cobra.Command {
return []*cobra.Command{ return []*cobra.Command{
versionCmd, versionCmd,
-2
View File
@@ -37,11 +37,9 @@ var resolveIncludeCmd = &cobra.Command{
"cursor.lua", "cursor.lua",
"windowrules.lua", "windowrules.lua",
"cursor.kdl", "cursor.kdl",
"layout.kdl",
"outputs.kdl", "outputs.kdl",
"binds.kdl", "binds.kdl",
"cursor.conf", "cursor.conf",
"layout.conf",
"outputs.conf", "outputs.conf",
"binds.conf", "binds.conf",
}, cobra.ShellCompDirectiveNoFileComp }, cobra.ShellCompDirectiveNoFileComp
-1
View File
@@ -886,7 +886,6 @@ func checkOptionalDependencies() []checkResult {
{"cava", "cava", "Audio visualizer", true}, {"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "Calendar events", false}, {"khal", "khal", "Calendar events", false},
{"danksearch", "dsearch", "File search", false}, {"danksearch", "dsearch", "File search", false},
{"dankcalendar", "dcal", "Calendar app", false},
{"fprintd", "fprintd-list", "Fingerprint auth", false}, {"fprintd", "fprintd-list", "Fingerprint auth", false},
} }
+11 -128
View File
@@ -32,14 +32,13 @@ var greeterCmd = &cobra.Command{
var ( var (
greeterConfigSyncFn = greeter.SyncDMSConfigs greeterConfigSyncFn = greeter.SyncDMSConfigs
sharedAuthSyncFn = sharedpam.SyncAuthConfig sharedAuthSyncFn = sharedpam.SyncAuthConfig
greeterIsNixOSFn = greeter.IsNixOS
) )
var greeterInstallCmd = &cobra.Command{ var greeterInstallCmd = &cobra.Command{
Use: "install", Use: "install",
Short: "Install and configure DMS greeter", Short: "Install and configure DMS greeter",
Long: "Install greetd and configure it to use DMS as the greeter interface", Long: "Install greetd and configure it to use DMS as the greeter interface",
PreRunE: preRunGreeterMutation, PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
@@ -65,9 +64,6 @@ var greeterSyncCmd = &cobra.Command{
Short: "Sync DMS theme and settings with greeter", Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.", Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.",
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
if err := rejectNixOSGreeterMutation(cmd); err != nil {
return err
}
profile, _ := cmd.Flags().GetBool("profile") profile, _ := cmd.Flags().GetBool("profile")
if profile { if profile {
return nil return nil
@@ -144,7 +140,7 @@ var greeterEnableCmd = &cobra.Command{
Use: "enable", Use: "enable",
Short: "Enable DMS greeter in greetd config", Short: "Enable DMS greeter in greetd config",
Long: "Configure greetd to use DMS as the greeter", Long: "Configure greetd to use DMS as the greeter",
PreRunE: preRunGreeterMutation, PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
@@ -180,7 +176,7 @@ var greeterUninstallCmd = &cobra.Command{
Use: "uninstall", Use: "uninstall",
Short: "Remove DMS greeter configuration and restore previous display manager", Short: "Remove DMS greeter configuration and restore previous display manager",
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state", Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
PreRunE: preRunGreeterMutation, PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
@@ -210,21 +206,6 @@ func init() {
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)") greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
} }
func rejectNixOSGreeterMutation(cmd *cobra.Command) error {
if !greeterIsNixOSFn() {
return nil
}
return fmt.Errorf("dms %s is disabled on NixOS because the greeter is managed declaratively\nConfigure the DMS greeter in your NixOS module, then apply the change with your normal nixos-rebuild workflow", normalizeCommandSpec(cmd.CommandPath()))
}
func preRunGreeterMutation(cmd *cobra.Command, args []string) error {
if err := rejectNixOSGreeterMutation(cmd); err != nil {
return err
}
return preRunPrivileged(cmd, args)
}
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error { func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil { if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
return err return err
@@ -1434,36 +1415,24 @@ func readDefaultSessionCommand(configPath string) string {
return "" return ""
} }
func explicitGreeterCacheDirFromCommand(command string) (string, bool) { func extractGreeterCacheDirFromCommand(command string) string {
if command == "" {
return greeter.GreeterCacheDir
}
tokens := strings.Fields(command) tokens := strings.Fields(command)
for i := 0; i < len(tokens); i++ { for i := 0; i < len(tokens); i++ {
token := strings.Trim(tokens[i], "\"") token := strings.Trim(tokens[i], "\"")
if token == "--cache-dir" && i+1 < len(tokens) { if token == "--cache-dir" && i+1 < len(tokens) {
value := strings.Trim(tokens[i+1], "\"") return strings.Trim(tokens[i+1], "\"")
if value != "" {
return value, true
}
} }
if strings.HasPrefix(token, "--cache-dir=") { if strings.HasPrefix(token, "--cache-dir=") {
value := strings.TrimPrefix(token, "--cache-dir=") value := strings.TrimPrefix(token, "--cache-dir=")
value = strings.Trim(value, "\"") value = strings.Trim(value, "\"")
if value != "" { if value != "" {
return value, true return value
} }
} }
} }
return "", false
}
const nixOSGreeterStateDir = "/var/lib/dms-greeter"
func greeterStatusStateDir(command string, isNixOS bool) string {
if cacheDir, ok := explicitGreeterCacheDirFromCommand(command); ok {
return cacheDir
}
if isNixOS {
return nixOSGreeterStateDir
}
return greeter.GreeterCacheDir return greeter.GreeterCacheDir
} }
@@ -1534,8 +1503,6 @@ func packageInstallHint() string {
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)" return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch: case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'" return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
case distros.FamilyVoid:
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf)"
default: default:
return "Run 'dms greeter install' to install greeter" return "Run 'dms greeter install' to install greeter"
} }
@@ -1574,8 +1541,7 @@ func isPackageOnlyGreeterDistro() bool {
config.Family == distros.FamilySUSE || config.Family == distros.FamilySUSE ||
config.Family == distros.FamilyUbuntu || config.Family == distros.FamilyUbuntu ||
config.Family == distros.FamilyFedora || config.Family == distros.FamilyFedora ||
config.Family == distros.FamilyArch || config.Family == distros.FamilyArch
config.Family == distros.FamilyVoid
} }
func promptCompositorChoice(compositors []string) (string, error) { func promptCompositorChoice(compositors []string) (string, error) {
@@ -1603,10 +1569,6 @@ func checkGreeterStatus() error {
fmt.Println("=== DMS Greeter Status ===") fmt.Println("=== DMS Greeter Status ===")
fmt.Println() fmt.Println()
if greeterIsNixOSFn() {
return checkNixOSGreeterStatus()
}
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)
@@ -1670,7 +1632,7 @@ func checkGreeterStatus() error {
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions") fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
} }
cacheDir := greeterStatusStateDir(configuredCommand, false) cacheDir := extractGreeterCacheDirFromCommand(configuredCommand)
fmt.Println("\nGreeter Cache Directory:") fmt.Println("\nGreeter Cache Directory:")
fmt.Printf(" Effective cache dir: %s\n", cacheDir) fmt.Printf(" Effective cache dir: %s\n", cacheDir)
if cacheDir != greeter.GreeterCacheDir { if cacheDir != greeter.GreeterCacheDir {
@@ -1933,85 +1895,6 @@ func checkGreeterStatus() error {
return nil return nil
} }
func checkNixOSGreeterStatus() error {
const configPath = "/etc/greetd/config.toml"
configuredCommand := readDefaultSessionCommand(configPath)
allGood := true
fmt.Println("Greeter Configuration:")
switch {
case strings.Contains(configuredCommand, "dms-greeter"):
fmt.Println(" ✓ DMS greeter command found")
if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
fmt.Printf(" Wrapper: %s\n", wrapper)
}
case configuredCommand != "":
fmt.Println(" ⚠ greetd default session does not reference dms-greeter")
allGood = false
default:
fmt.Printf(" No readable DMS command found in %s\n", configPath)
}
fmt.Println(" NixOS manages greeter configuration declaratively; apply changes through your NixOS module.")
stateDir := greeterStatusStateDir(configuredCommand, true)
fmt.Println("\nGreeter State Directory:")
fmt.Printf(" Effective state dir: %s\n", stateDir)
if stateDir == nixOSGreeterStateDir {
fmt.Println(" ✓ Using the NixOS module state path")
}
if stat, err := os.Stat(stateDir); err == nil && stat.IsDir() {
fmt.Printf(" ✓ %s exists\n", stateDir)
} else if os.IsNotExist(err) {
fmt.Printf(" ✗ %s not found\n", stateDir)
fmt.Println(" Rebuild your NixOS configuration after enabling the DMS greeter module.")
allGood = false
} else if err != nil {
fmt.Printf(" ✗ Could not inspect %s: %v\n", stateDir, err)
allGood = false
} else {
fmt.Printf(" ✗ %s is not a directory\n", stateDir)
allGood = false
}
fmt.Println("\nDeclarative Configuration Files:")
configFiles := []struct {
name string
path string
}{
{name: "Settings", path: filepath.Join(stateDir, "settings.json")},
{name: "Session state", path: filepath.Join(stateDir, "session.json")},
{name: "Color theme", path: filepath.Join(stateDir, "colors.json")},
}
for _, configFile := range configFiles {
if stat, err := os.Stat(configFile.path); err == nil && !stat.IsDir() {
fmt.Printf(" ✓ %s: %s\n", configFile.name, configFile.path)
} else if os.IsNotExist(err) {
fmt.Printf(" %s not present (optional; configure configHome/configFiles in the NixOS module)\n", configFile.name)
} else if err != nil {
fmt.Printf(" ⚠ %s could not be inspected: %v\n", configFile.name, err)
} else {
fmt.Printf(" ⚠ %s path is not a regular file: %s\n", configFile.name, configFile.path)
}
}
fmt.Println("\nGroup Membership:")
fmt.Println(" User group membership is managed by NixOS and is not required for declarative theme copies.")
fmt.Println("\nGreeter PAM Authentication:")
fmt.Println(" PAM is managed by NixOS modules.")
fmt.Println(" Configure fingerprint/U2F through security.pam.services.greetd.")
fmt.Println()
if allGood {
fmt.Println("✓ NixOS greeter state looks healthy and is managed declaratively.")
} else {
fmt.Println("⚠ Some issues detected. Update the DMS greeter module and rebuild NixOS; do not run 'dms greeter sync'.")
}
return nil
}
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) { func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
if sampleLimit <= 0 { if sampleLimit <= 0 {
sampleLimit = 3 sampleLimit = 3
-56
View File
@@ -3,11 +3,9 @@ package main
import ( import (
"errors" "errors"
"reflect" "reflect"
"strings"
"testing" "testing"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/spf13/cobra"
) )
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) { func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
@@ -87,57 +85,3 @@ func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
t.Fatal("expected auth sync not to run after config sync failure") t.Fatal("expected auth sync not to run after config sync failure")
} }
} }
func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) {
if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir {
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir)
}
}
func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) {
command := "dms-greeter --cache-dir /srv/dms-greeter --command niri"
if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" {
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter")
}
}
func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) {
origGreeterIsNixOSFn := greeterIsNixOSFn
greeterIsNixOSFn = func() bool { return true }
t.Cleanup(func() {
greeterIsNixOSFn = origGreeterIsNixOSFn
})
for _, commandName := range []string{"install", "enable", "sync", "uninstall"} {
t.Run(commandName, func(t *testing.T) {
root := &cobra.Command{Use: "dms"}
greeterCommand := &cobra.Command{Use: "greeter"}
mutationCommand := &cobra.Command{Use: commandName}
root.AddCommand(greeterCommand)
greeterCommand.AddCommand(mutationCommand)
err := rejectNixOSGreeterMutation(mutationCommand)
if err == nil {
t.Fatalf("expected NixOS greeter %s to be rejected", commandName)
}
if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") {
t.Fatalf("unexpected error: %v", err)
}
if strings.Contains(err.Error(), "/var/cache/dms-greeter") {
t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err)
}
})
}
}
func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) {
origGreeterIsNixOSFn := greeterIsNixOSFn
greeterIsNixOSFn = func() bool { return false }
t.Cleanup(func() {
greeterIsNixOSFn = origGreeterIsNixOSFn
})
if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil {
t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err)
}
}
-23
View File
@@ -38,17 +38,10 @@ var matugenCheckCmd = &cobra.Command{
Run: runMatugenCheck, Run: runMatugenCheck,
} }
var matugenPreviewCmd = &cobra.Command{
Use: "preview",
Short: "Preview Matugen scheme colors without applying them",
Run: runMatugenPreview,
}
func init() { func init() {
matugenCmd.AddCommand(matugenGenerateCmd) matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd) matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd) matugenCmd.AddCommand(matugenCheckCmd)
matugenCmd.AddCommand(matugenPreviewCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} { for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files") cmd.Flags().String("state-dir", "", "State directory for cache files")
@@ -69,8 +62,6 @@ func init() {
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion") matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting") matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
} }
func buildMatugenOptions(cmd *cobra.Command) matugen.Options { func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
@@ -209,17 +200,3 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
} }
fmt.Println(string(data)) fmt.Println(string(data))
} }
func runMatugenPreview(cmd *cobra.Command, args []string) {
sourceColor, _ := cmd.Flags().GetString("source-color")
contrast, _ := cmd.Flags().GetFloat64("contrast")
previews, err := matugen.PreviewSchemes(sourceColor, contrast)
if err != nil {
log.Fatalf("Failed to generate Matugen previews: %v", err)
}
data, err := json.Marshal(previews)
if err != nil {
log.Fatalf("Failed to marshal Matugen previews: %v", err)
}
fmt.Println(string(data))
}
-13
View File
@@ -10,7 +10,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
@@ -299,9 +298,6 @@ func runSetup() error {
if wmSelected { if wmSelected {
if wm == deps.WindowManagerMango { if wm == deps.WindowManagerMango {
useSystemd = false useSystemd = false
} else if isVoidSetup() {
useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
} else { } else {
useSystemd = promptSystemd() useSystemd = promptSystemd()
} }
@@ -376,15 +372,6 @@ func runSetup() error {
return nil return nil
} }
func isVoidSetup() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
// Add user to the input group for the evdev manager for inut state tracking. // Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator. // Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() { func ensureInputGroup() {
-24
View File
@@ -1,24 +0,0 @@
package main
import (
"encoding/json"
"testing"
)
func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) {
var policyFile cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil {
t.Fatalf("failed to parse embedded CLI policy: %v", err)
}
if policyFile.BlockedCommands == nil {
t.Fatal("embedded CLI policy has no blocked_commands")
}
blocked := normalizeBlockedCommands(*policyFile.BlockedCommands)
if !commandBlockedByPolicy("greeter enable", blocked) {
t.Fatal("expected greeter enable to remain blocked on immutable/image-based systems")
}
if commandBlockedByPolicy("greeter sync", blocked) {
t.Fatal("expected greeter sync to remain available on immutable/image-based systems")
}
}
+2 -9
View File
@@ -582,11 +582,7 @@ func runShellDaemon(session bool) {
} }
var qsHasAnyDisplay = sync.OnceValue(func() bool { var qsHasAnyDisplay = sync.OnceValue(func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) out, err := exec.Command("qs", "ipc", "--help").Output()
defer cancel()
cmd := exec.CommandContext(ctx, "qs", "ipc", "--help")
cmd.WaitDelay = 500 * time.Millisecond
out, err := cmd.Output()
if err != nil { if err != nil {
return false return false
} }
@@ -646,10 +642,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil return nil
} }
cmdArgs := append(baseArgs, "show") cmdArgs := append(baseArgs, "show")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) cmd := exec.Command("qs", cmdArgs...)
defer cancel()
cmd := exec.CommandContext(ctx, "qs", cmdArgs...)
cmd.WaitDelay = 500 * time.Millisecond
var targets ipcTargets var targets ipcTargets
if output, err := cmd.Output(); err == nil { if output, err := cmd.Output(); err == nil {
+316 -144
View File
@@ -9,6 +9,7 @@ import (
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
const envServe = "_DMS_CLIPBOARD_SERVE" const envServe = "_DMS_CLIPBOARD_SERVE"
@@ -16,37 +17,6 @@ const envMime = "_DMS_CLIPBOARD_MIME"
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE" const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
const envCacheFile = "_DMS_CLIPBOARD_CACHE" const envCacheFile = "_DMS_CLIPBOARD_CACHE"
type Offer struct {
MimeType string
Data []byte
}
// textMimeAliases are offered alongside plain-text content so legacy X11
// clients bridged through XWayland find a target they can convert.
var textMimeAliases = []string{
"text/plain",
"text/plain;charset=utf-8",
"UTF8_STRING",
"STRING",
"TEXT",
}
// ExpandOffers turns raw clipboard data into the full offer list to serve,
// adding the standard alias set for text content.
func ExpandOffers(data []byte, mimeType string) []Offer {
offers := []Offer{{MimeType: mimeType, Data: data}}
if mimeType != "text/plain" && mimeType != "text/plain;charset=utf-8" {
return offers
}
for _, alias := range textMimeAliases {
if alias == mimeType {
continue
}
offers = append(offers, Offer{MimeType: alias, Data: data})
}
return offers
}
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard // MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
// child. Reads source data into memory, deletes any cache file, then serves. // child. Reads source data into memory, deletes any cache file, then serves.
func MaybeServeAndExit() { func MaybeServeAndExit() {
@@ -74,7 +44,7 @@ func MaybeServeAndExit() {
os.Exit(1) os.Exit(1)
} }
if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil { if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err) fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -85,33 +55,22 @@ func Copy(data []byte, mimeType string) error {
return copyForkCached(data, mimeType, false) return copyForkCached(data, mimeType, false)
} }
func CopyText(text string) error {
return Copy([]byte(text), "text/plain;charset=utf-8")
}
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground { if foreground {
return serveOffers(ExpandOffers(data, mimeType), pasteOnce) return serveClipboard(data, mimeType, pasteOnce)
} }
return copyForkCached(data, mimeType, pasteOnce) return copyForkCached(data, mimeType, pasteOnce)
} }
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error { func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground {
return copyFork(data, mimeType, pasteOnce)
}
buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
}
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
if foreground { if foreground {
return serveOffers(offers, pasteOnce) buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
} }
return copyMultiFork(offers, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd { func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
@@ -173,70 +132,39 @@ func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
cmd := newForkCmd(mimeType, pasteOnce) cmd := newForkCmd(mimeType, pasteOnce)
if src, ok := data.(*os.File); ok { switch src := data.(type) {
case *os.File:
cmd.Stdin = src cmd.Stdin = src
return waitReady(cmd) return waitReady(cmd)
}
stdin, err := cmd.StdinPipe() default:
if err != nil { stdin, err := cmd.StdinPipe()
return fmt.Errorf("stdin pipe: %w", err) if err != nil {
} return fmt.Errorf("stdin pipe: %w", err)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := io.Copy(stdin, data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
func copyMultiFork(offers []Offer, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
if pasteOnce {
args = append(args, "--paste-once")
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
for _, offer := range offers {
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
if _, err := stdin.Write(offer.Data); err != nil {
stdin.Close()
return fmt.Errorf("write offer data: %w", err)
} }
}
stdin.Close()
return nil stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := io.Copy(stdin, data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
} }
func signalReady() { func signalReady() {
@@ -266,25 +194,57 @@ func createClipboardCacheFile() (*os.File, error) {
return os.CreateTemp("", "dms-clipboard-*") return os.CreateTemp("", "dms-clipboard-*")
} }
// serveOffers owns the Wayland selection until cancelled (or first paste when func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
// pasteOnce is set), answering every offered mime type with its data. display, err := wlclient.Connect("")
func serveOffers(offers []Offer, pasteOnce bool) error {
if len(offers) == 0 {
return fmt.Errorf("no offers to serve")
}
s, err := connectSession()
if err != nil { if err != nil {
return err return fmt.Errorf("wayland connect: %w", err)
} }
defer s.Close() defer display.Destroy()
dataControlMgr, err := s.requireDataControl() ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil { if err != nil {
return err return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
} }
device, err := dataControlMgr.GetDataDevice(s.seat) device, err := dataControlMgr.GetDataDevice(seat)
if err != nil { if err != nil {
return fmt.Errorf("get data device: %w", err) return fmt.Errorf("get data device: %w", err)
} }
@@ -295,12 +255,25 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
return fmt.Errorf("create data source: %w", err) return fmt.Errorf("create data source: %w", err)
} }
offerData := make(map[string][]byte, len(offers)) if err := source.Offer(mimeType); err != nil {
for _, offer := range offers { return fmt.Errorf("offer mime type: %w", err)
if err := source.Offer(offer.MimeType); err != nil { }
return fmt.Errorf("offer %s: %w", offer.MimeType, err) if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
if err := source.Offer("text/plain"); err != nil {
return fmt.Errorf("offer text/plain: %w", err)
}
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
}
if err := source.Offer("UTF8_STRING"); err != nil {
return fmt.Errorf("offer UTF8_STRING: %w", err)
}
if err := source.Offer("STRING"); err != nil {
return fmt.Errorf("offer STRING: %w", err)
}
if err := source.Offer("TEXT"); err != nil {
return fmt.Errorf("offer TEXT: %w", err)
} }
offerData[offer.MimeType] = offer.Data
} }
cancelled := make(chan struct{}) cancelled := make(chan struct{})
@@ -310,11 +283,7 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
_ = syscall.SetNonblock(e.Fd, false) _ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
_, _ = file.Write(data)
if data, ok := offerData[e.MimeType]; ok {
_, _ = file.Write(data)
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -329,7 +298,7 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
return fmt.Errorf("set selection: %w", err) return fmt.Errorf("set selection: %w", err)
} }
s.display.Roundtrip() display.Roundtrip()
signalReady() signalReady()
for { for {
@@ -341,26 +310,70 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
return nil return nil
} }
default: default:
if err := s.ctx.Dispatch(); err != nil { if err := ctx.Dispatch(); err != nil {
return nil return nil
} }
} }
} }
} }
func CopyText(text string) error {
return Copy([]byte(text), "text/plain;charset=utf-8")
}
func Paste() ([]byte, string, error) { func Paste() ([]byte, string, error) {
s, err := connectSession() display, err := wlclient.Connect("")
if err != nil { if err != nil {
return nil, "", err return nil, "", fmt.Errorf("wayland connect: %w", err)
} }
defer s.Close() defer display.Destroy()
dataControlMgr, err := s.requireDataControl() ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil { if err != nil {
return nil, "", err return nil, "", fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
} }
device, err := dataControlMgr.GetDataDevice(s.seat) if dataControlMgr == nil {
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return nil, "", fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("get data device: %w", err) return nil, "", fmt.Errorf("get data device: %w", err)
} }
@@ -386,14 +399,15 @@ func Paste() ([]byte, string, error) {
gotSelection = true gotSelection = true
}) })
s.display.Roundtrip() display.Roundtrip()
s.display.Roundtrip() display.Roundtrip()
if !gotSelection || selectionOffer == nil { if !gotSelection || selectionOffer == nil {
return nil, "", fmt.Errorf("no clipboard data") return nil, "", fmt.Errorf("no clipboard data")
} }
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer]) mimeTypes := offerMimeTypes[selectionOffer]
selectedMime := selectPreferredMimeType(mimeTypes)
if selectedMime == "" { if selectedMime == "" {
return nil, "", fmt.Errorf("no supported mime type") return nil, "", fmt.Errorf("no supported mime type")
} }
@@ -410,7 +424,7 @@ func Paste() ([]byte, string, error) {
} }
w.Close() w.Close()
s.display.Roundtrip() display.Roundtrip()
data, err := io.ReadAll(r) data, err := io.ReadAll(r)
if err != nil { if err != nil {
@@ -456,3 +470,161 @@ func selectPreferredMimeType(mimes []string) string {
func IsImageMimeType(mime string) bool { func IsImageMimeType(mime string) bool {
return len(mime) > 6 && mime[:6] == "image/" return len(mime) > 6 && mime[:6] == "image/"
} }
type Offer struct {
MimeType string
Data []byte
}
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
if !foreground {
return copyMultiFork(offers, pasteOnce)
}
return copyMultiServe(offers, pasteOnce)
}
func copyMultiFork(offers []Offer, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
if pasteOnce {
args = append(args, "--paste-once")
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
for _, offer := range offers {
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
if _, err := stdin.Write(offer.Data); err != nil {
stdin.Close()
return fmt.Errorf("write offer data: %w", err)
}
}
stdin.Close()
return nil
}
func copyMultiServe(offers []Offer, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
source, err := dataControlMgr.CreateDataSource()
if err != nil {
return fmt.Errorf("create data source: %w", err)
}
offerMap := make(map[string][]byte)
for _, offer := range offers {
if err := source.Offer(offer.MimeType); err != nil {
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
}
offerMap[offer.MimeType] = offer.Data
}
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if data, ok := offerMap[e.MimeType]; ok {
_, _ = file.Write(data)
}
select {
case pasted <- struct{}{}:
default:
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
close(cancelled)
})
if err := device.SetSelection(source); err != nil {
return fmt.Errorf("set selection: %w", err)
}
display.Roundtrip()
for {
select {
case <-cancelled:
return nil
case <-pasted:
if pasteOnce {
return nil
}
default:
if err := ctx.Dispatch(); err != nil {
return nil
}
}
}
}
-206
View File
@@ -1,206 +0,0 @@
package clipboard
import (
"fmt"
"regexp"
"strconv"
"strings"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
const (
xkbKeymapFormatV1 = 1
keyStateReleased = 0
keyStatePressed = 1
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
shiftModMask = 1 << 0
ctrlModMask = 1 << 2
// evdev fallbacks for a standard pc105 map
fallbackCtrlKey = 29 // KEY_LEFTCTRL
fallbackShiftKey = 42 // KEY_LEFTSHIFT
fallbackVKey = 47 // KEY_V
)
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
// using the seat's own keymap, so keycodes stay valid for XWayland clients
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
// selects ctrl+shift+v for terminal targets.
func SendPasteKeystroke(withShift bool) error {
s, err := connectSession()
if err != nil {
return err
}
defer s.Close()
if s.virtualKeyboardMgr == nil {
return fmt.Errorf("compositor does not support zwp_virtual_keyboard_manager_v1")
}
if s.seat == nil {
return fmt.Errorf("no seat available")
}
keyboard, err := s.seat.GetKeyboard()
if err != nil {
return fmt.Errorf("get keyboard: %w", err)
}
defer keyboard.Release()
var keymap *wlclient.KeyboardKeymapEvent
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
if keymap == nil {
keymap = &e
}
})
s.display.Roundtrip()
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
return fmt.Errorf("no xkb keymap from seat")
}
defer unix.Close(keymap.Fd)
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
if err != nil {
return fmt.Errorf("read keymap: %w", err)
}
keys := resolveKeycodes(keymapText)
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
if err != nil {
return fmt.Errorf("create virtual keyboard: %w", err)
}
defer vk.Destroy()
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
return fmt.Errorf("set keymap: %w", err)
}
mods := uint32(ctrlModMask)
held := []uint32{keys.ctrl}
if withShift {
mods |= shiftModMask
held = append(held, keys.shift)
}
t := uint32(0)
press := func(key, state uint32) error {
t++
return vk.Key(t, key, state)
}
for _, key := range held {
if err := press(key, keyStatePressed); err != nil {
return fmt.Errorf("key press: %w", err)
}
}
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
return fmt.Errorf("set modifiers: %w", err)
}
if err := press(keys.v, keyStatePressed); err != nil {
return fmt.Errorf("key press: %w", err)
}
if err := press(keys.v, keyStateReleased); err != nil {
return fmt.Errorf("key release: %w", err)
}
for i := len(held) - 1; i >= 0; i-- {
if err := press(held[i], keyStateReleased); err != nil {
return fmt.Errorf("key release: %w", err)
}
}
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
return fmt.Errorf("clear modifiers: %w", err)
}
s.display.Roundtrip()
return nil
}
func readKeymap(fd int, size uint32) (string, error) {
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
if err != nil {
return "", err
}
text := strings.TrimRight(string(data), "\x00")
return text, unix.Munmap(data)
}
type pasteKeycodes struct {
ctrl uint32
shift uint32
v uint32
}
var (
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
)
// xkbcommon may serialize keysyms as hex escapes instead of names
// (e.g. "0x76" for v, "0xffe3" for Control_L).
var keysymNames = map[uint32]string{
0x76: "v",
0xffe3: "Control_L",
0xffe1: "Shift_L",
}
func canonicalKeysym(sym string) string {
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
return sym
}
value, err := strconv.ParseUint(sym[2:], 16, 32)
if err != nil {
return sym
}
if name, ok := keysymNames[uint32(value)]; ok {
return name
}
return sym
}
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
// the seat keymap's first group, falling back to pc105 positions.
func resolveKeycodes(keymap string) pasteKeycodes {
codes := map[string]uint32{}
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
if code, err := strconv.Atoi(m[2]); err == nil {
codes[m[1]] = uint32(code)
}
}
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
want := map[string]*uint32{
"Control_L": &keys.ctrl,
"Shift_L": &keys.shift,
"v": &keys.v,
}
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
if group == nil {
continue
}
xkbCode, ok := codes[m[1]]
if !ok || xkbCode < 8 {
continue
}
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
target, wanted := want[level1]
if !wanted {
continue
}
*target = xkbCode - 8
delete(want, level1)
if len(want) == 0 {
break
}
}
return keys
}
@@ -1,65 +0,0 @@
package clipboard
import (
"os"
"testing"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
func TestLiveSeatKeymapResolution(t *testing.T) {
if os.Getenv("DMS_LIVE_TEST") == "" {
t.Skip("set DMS_LIVE_TEST=1 to run against the live compositor")
}
s, err := connectSession()
if err != nil {
t.Fatalf("connect: %v", err)
}
defer s.Close()
if s.virtualKeyboardMgr == nil {
t.Fatal("compositor does not advertise zwp_virtual_keyboard_manager_v1")
}
if s.seat == nil {
t.Fatal("no seat")
}
keyboard, err := s.seat.GetKeyboard()
if err != nil {
t.Fatalf("get keyboard: %v", err)
}
defer keyboard.Release()
var keymap *wlclient.KeyboardKeymapEvent
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
if keymap == nil {
keymap = &e
}
})
s.display.Roundtrip()
if keymap == nil {
t.Fatal("no keymap event")
}
defer unix.Close(keymap.Fd)
text, err := readKeymap(keymap.Fd, keymap.Size)
if err != nil {
t.Fatalf("read keymap: %v", err)
}
if dump := os.Getenv("DMS_LIVE_DUMP"); dump != "" {
if err := os.WriteFile(dump, []byte(text), 0o644); err != nil {
t.Fatalf("dump keymap: %v", err)
}
}
keys := resolveKeycodes(text)
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
}
}
-105
View File
@@ -1,105 +0,0 @@
package clipboard
import "testing"
const qwertyKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
minimum = 8;
maximum = 708;
<ESC> = 9;
<AB04> = 55;
<LCTL> = 37;
<LFSH> = 50;
alias <AL01> = <AC01>;
indicator 1 = "Caps Lock";
};
xkb_types "(unnamed)" {
type "ALPHABETIC" {
modifiers = Shift+Lock;
map[Shift] = Level2;
level_name[Level1] = "Base";
};
};
xkb_symbols "(unnamed)" {
key <ESC> { [ Escape ] };
key <AB04> { type= "ALPHABETIC", [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
const hexKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB04> = 56;
<LCTL> = 38;
<LFSH> = 51;
};
xkb_symbols "(unnamed)" {
key <LCTL> { [ 0xffe3 ] };
key <LFSH> {
type= "PC_ALT_LEVEL2",
symbols[1]= [ 0xffe1, 0xfe08 ]
};
key <AB04> { [ 0x76, 0x56 ] };
};
};`
const dvorakKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB09> = 60;
<LCTL> = 37;
<LFSH> = 50;
};
xkb_symbols "(unnamed)" {
key <AB09> { [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
func TestResolveKeycodes(t *testing.T) {
tests := []struct {
name string
keymap string
want pasteKeycodes
}{
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveKeycodes(tt.keymap)
if got != tt.want {
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
}
})
}
}
func TestExpandOffers(t *testing.T) {
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
if len(text) != 5 {
t.Fatalf("expected 5 text offers, got %d", len(text))
}
seen := map[string]bool{}
for _, o := range text {
if string(o.Data) != "hi" {
t.Errorf("offer %s has wrong data", o.MimeType)
}
if seen[o.MimeType] {
t.Errorf("duplicate offer %s", o.MimeType)
}
seen[o.MimeType] = true
}
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
t.Errorf("missing X11 alias offers: %v", seen)
}
img := ExpandOffers([]byte{1}, "image/png")
if len(img) != 1 || img[0].MimeType != "image/png" {
t.Errorf("non-text mime should not expand, got %+v", img)
}
}
-93
View File
@@ -1,93 +0,0 @@
package clipboard
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type session struct {
display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry
seat *wlclient.Seat
dataControlMgr *ext_data_control.ExtDataControlManagerV1
virtualKeyboardMgr *virtual_keyboard.ZwpVirtualKeyboardManagerV1
}
// connectSession opens a short-lived Wayland connection and binds the seat
// plus whichever clipboard-related globals the compositor advertises.
func connectSession() (*session, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
s := &session{display: display, ctx: display.Context()}
registry, err := display.GetRegistry()
if err != nil {
display.Destroy()
return nil, fmt.Errorf("get registry: %w", err)
}
s.registry = registry
var bindErr error
bind := func(name uint32, iface string, version uint32, proxy wlclient.Proxy) {
if err := registry.Bind(name, iface, version, proxy); err != nil {
bindErr = fmt.Errorf("bind %s: %w", iface, err)
}
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case ext_data_control.ExtDataControlManagerV1InterfaceName:
mgr := ext_data_control.NewExtDataControlManagerV1(s.ctx)
bind(e.Name, e.Interface, e.Version, mgr)
s.dataControlMgr = mgr
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
mgr := virtual_keyboard.NewZwpVirtualKeyboardManagerV1(s.ctx)
bind(e.Name, e.Interface, e.Version, mgr)
s.virtualKeyboardMgr = mgr
case "wl_seat":
if s.seat != nil {
return
}
seat := wlclient.NewSeat(s.ctx)
bind(e.Name, e.Interface, e.Version, seat)
s.seat = seat
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
s.Close()
return nil, bindErr
}
return s, nil
}
func (s *session) requireDataControl() (*ext_data_control.ExtDataControlManagerV1, error) {
switch {
case s.dataControlMgr == nil:
return nil, fmt.Errorf("compositor does not support ext_data_control_manager_v1")
case s.seat == nil:
return nil, fmt.Errorf("no seat available")
default:
return s.dataControlMgr, nil
}
}
func (s *session) Close() {
if s.dataControlMgr != nil {
s.dataControlMgr.Destroy()
}
if s.registry != nil {
s.registry.Destroy()
}
s.display.Destroy()
}
-4
View File
@@ -61,10 +61,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true) return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
+2 -2
View File
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
copy-on-select = false copy-on-select = false
confirm-close-surface = false confirm-close-surface = false
# Disable in-app Ghostty toast notifications # Disable annoying copied to clipboard
app-notifications = false app-notifications = no-clipboard-copy,no-config-reload
# Key bindings for common actions # Key bindings for common actions
#keybind = ctrl+c=copy_to_clipboard #keybind = ctrl+c=copy_to_clipboard
+1 -1
View File
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle")) hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle")) hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper")) hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle")) hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
@@ -13,7 +13,6 @@ hl.config({
input = { input = {
kb_layout = "us", kb_layout = "us",
numlock_by_default = true, numlock_by_default = true,
follow_mouse = 0,
touchpad = { touchpad = {
tap_to_click = true, tap_to_click = true,
natural_scroll = true, natural_scroll = true,
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
# Notepad # Notepad
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
# Browse Wallpapers # Browse Wallpapers
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
# Power Menu # Power Menu
bind=SUPER,x,spawn,dms ipc call powermenu toggle bind=SUPER,x,spawn,dms ipc call powermenu toggle
# Cycle Display Profile # Cycle Display Profile
+1 -1
View File
@@ -24,7 +24,7 @@ binds {
spawn "dms" "ipc" "call" "settings" "focusOrToggle"; spawn "dms" "ipc" "call" "settings" "focusOrToggle";
} }
Mod+Y hotkey-overlay-title="Browse Wallpapers" { Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper"; spawn "dms" "ipc" "call" "dankdash" "wallpaper";
} }
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; } Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; } Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
@@ -20,10 +20,3 @@ window-rule {
tiled-state true tiled-state true
draw-border-with-background false draw-border-with-background false
} }
layer-rule {
exclude namespace="^dms:bar$"
background-effect {
xray false
}
}
+6 -6
View File
@@ -265,9 +265,9 @@ recent-windows {
} }
// Include dms files // Include dms files
include optional=true "dms/colors.kdl" include "dms/colors.kdl"
include optional=true "dms/layout.kdl" include "dms/layout.kdl"
include optional=true "dms/alttab.kdl" include "dms/alttab.kdl"
include optional=true "dms/binds.kdl" include "dms/binds.kdl"
include optional=true "dms/outputs.kdl" include "dms/outputs.kdl"
include optional=true "dms/cursor.kdl" include "dms/cursor.kdl"
+2 -51
View File
@@ -119,30 +119,10 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectMatugen()) dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop()) dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectDanksearch())
dependencies = append(dependencies, a.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
func (a *ArchDistribution) detectDanksearch() deps.Dependency {
dep := a.BaseDistribution.detectDanksearch()
dep.CanToggle = true
if a.packageInstalled("dsearch-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) detectDankCalendar() deps.Dependency {
dep := a.BaseDistribution.detectDankCalendar()
dep.CanToggle = true
if a.packageInstalled("dankcalendar-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) detectXDGPortal() deps.Dependency { func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk")) return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
} }
@@ -152,13 +132,7 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
} }
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency { func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
installed := a.packageInstalled("greetd-dms-greeter-git") || a.packageInstalled("greetd-dms-greeter-bin") return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
dep := a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", installed)
dep.CanToggle = true
if a.packageInstalled("greetd-dms-greeter-git") {
dep.Variant = deps.VariantGit
}
return dep
} }
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {
@@ -217,7 +191,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]), "quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": a.getDMSGreeterMapping(variants["dms-greeter"]), "dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"matugen": a.getMatugenMapping(variants["matugen"]), "matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem}, "dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem}, "ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
@@ -225,8 +199,6 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"danksearch": a.getDanksearchMapping(variants["danksearch"]),
"dankcalendar": a.getDankCalendarMapping(variants["dankcalendar"]),
} }
switch wm { switch wm {
@@ -281,27 +253,6 @@ func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) Packag
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem} return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
} }
func (a *ArchDistribution) getDanksearchMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dsearch-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dsearch-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDankCalendarMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dankcalendar-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dankcalendar-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDMSGreeterMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "greetd-dms-greeter-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping { func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
if forceDMSGit || variant == deps.VariantGit { if forceDMSGit || variant == deps.VariantGit {
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
-8
View File
@@ -107,14 +107,6 @@ func (b *BaseDistribution) detectDgop() deps.Dependency {
return b.detectCommand("dgop", "Desktop portal management tool") return b.detectCommand("dgop", "Desktop portal management tool")
} }
func (b *BaseDistribution) detectDanksearch() deps.Dependency {
return b.detectOptionalPackage("danksearch", "File indexing and search service", b.commandExists("dsearch") || b.commandExists("danksearch"))
}
func (b *BaseDistribution) detectDankCalendar() deps.Dependency {
return b.detectOptionalPackage("dankcalendar", "Calendar application", b.commandExists("dcal") || b.commandExists("dankcalendar"))
}
func (b *BaseDistribution) detectDMS() deps.Dependency { func (b *BaseDistribution) detectDMS() deps.Dependency {
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms") dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
-4
View File
@@ -71,8 +71,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen()) dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop()) dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectDanksearch())
dependencies = append(dependencies, d.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -137,8 +135,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
} }
if wm == deps.WindowManagerNiri { if wm == deps.WindowManagerNiri {
-34
View File
@@ -1,34 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
)
// SetupDsearchService enables the dsearch.service user unit. Enablement failures
// are returned for the caller to surface as a non-fatal warning.
func SetupDsearchService(ctx context.Context, logf func(string)) error {
if logf == nil {
logf = func(string) {}
}
if err := runSystemctlUser(ctx, "daemon-reload"); err != nil {
return err
}
if err := runSystemctlUser(ctx, "enable", "--now", "dsearch.service"); err != nil {
return err
}
logf("Enabled dsearch.service")
return nil
}
func runSystemctlUser(ctx context.Context, args ...string) error {
cmd := exec.CommandContext(ctx, "systemctl", append([]string{"--user"}, args...)...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("systemctl --user %v failed: %w: %s", args, err, string(output))
}
return nil
}
-4
View File
@@ -104,8 +104,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectMatugen()) dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop()) dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectDanksearch())
dependencies = append(dependencies, f.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -140,8 +138,6 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
} }
switch wm { switch wm {
-2
View File
@@ -113,7 +113,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectMatugen()) dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop()) dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectDanksearch())
return dependencies, nil return dependencies, nil
} }
@@ -172,7 +171,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, "matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"dms (DankMaterialShell)": g.getDmsMapping(), "dms (DankMaterialShell)": g.getDmsMapping(),
"dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, "dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"danksearch": {Name: "gui-apps/danksearch", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
} }
switch wm { switch wm {
-3
View File
@@ -17,7 +17,6 @@ const (
FamilyDebian DistroFamily = "debian" FamilyDebian DistroFamily = "debian"
FamilyNix DistroFamily = "nix" FamilyNix DistroFamily = "nix"
FamilyGentoo DistroFamily = "gentoo" FamilyGentoo DistroFamily = "gentoo"
FamilyVoid DistroFamily = "void"
) )
// PackageManagerType defines the package manager a distro uses // PackageManagerType defines the package manager a distro uses
@@ -30,7 +29,6 @@ const (
PackageManagerZypper PackageManagerType = "zypper" PackageManagerZypper PackageManagerType = "zypper"
PackageManagerNix PackageManagerType = "nix" PackageManagerNix PackageManagerType = "nix"
PackageManagerPortage PackageManagerType = "portage" PackageManagerPortage PackageManagerType = "portage"
PackageManagerXBPS PackageManagerType = "xbps"
) )
// RepositoryType defines the type of repository for a package // RepositoryType defines the type of repository for a package
@@ -44,7 +42,6 @@ const (
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE) RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
RepoTypeManual RepositoryType = "manual" // Manual build from source RepoTypeManual RepositoryType = "manual" // Manual build from source
) )
-4
View File
@@ -91,8 +91,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectMatugen()) dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop()) dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectDanksearch())
dependencies = append(dependencies, o.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -131,8 +129,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
} }
switch wm { switch wm {
-4
View File
@@ -80,8 +80,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectMatugen()) dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDgop()) dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectDanksearch())
dependencies = append(dependencies, u.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -126,8 +124,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
} }
switch wm { switch wm {
-534
View File
@@ -1,534 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
const (
VoidDMSRepo = "https://avengemedia.github.io/DankMaterialShell/current"
VoidDankLinuxRepo = "https://avengemedia.github.io/DankLinux/current"
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
voidRunitSvDir = "/etc/sv"
voidRunitServiceDir = "/var/service"
)
func init() {
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
return NewVoidDistribution(config, logChan)
})
}
type VoidDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
return &VoidDistribution{
BaseDistribution: NewBaseDistribution(logChan),
config: config,
}
}
func (v *VoidDistribution) GetID() string {
return v.config.ID
}
func (v *VoidDistribution) GetColorHex() string {
return v.config.ColorHex
}
func (v *VoidDistribution) GetFamily() DistroFamily {
return v.config.Family
}
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
return PackageManagerXBPS
}
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, v.detectDMS())
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
dependencies = append(dependencies, v.detectGit())
dependencies = append(dependencies, v.detectWindowManager(wm))
dependencies = append(dependencies, v.detectQuickshell())
dependencies = append(dependencies, v.detectDMSGreeter())
dependencies = append(dependencies, v.detectXDGPortal())
dependencies = append(dependencies, v.detectAccountsService())
dependencies = append(dependencies, v.detectDBus())
dependencies = append(dependencies, v.detectElogind())
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, v.detectHyprlandTools()...)
}
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
dependencies = append(dependencies, v.detectXwaylandSatellite())
}
dependencies = append(dependencies, v.detectMatugen())
dependencies = append(dependencies, v.detectDgop())
dependencies = append(dependencies, v.detectDanksearch())
dependencies = append(dependencies, v.detectDankCalendar())
return dependencies, nil
}
func (v *VoidDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
version := ""
variant := deps.VariantStable
if v.packageInstalled("dms-git") {
status = deps.StatusInstalled
version = v.packageVersion("dms-git")
variant = deps.VariantGit
} else if v.packageInstalled("dms") {
status = deps.StatusInstalled
version = v.packageVersion("dms")
} else if v.commandExists("dms") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Version: version,
Description: "Desktop Management System package",
Required: true,
Variant: variant,
CanToggle: true,
}
}
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
dep := v.BaseDistribution.detectQuickshell()
dep.CanToggle = false
return dep
}
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
}
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
}
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
}
func (v *VoidDistribution) detectDBus() deps.Dependency {
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
}
func (v *VoidDistribution) detectElogind() deps.Dependency {
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
}
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
}
func (v *VoidDistribution) packageInstalled(pkg string) bool {
return exec.Command("xbps-query", pkg).Run() == nil
}
func (v *VoidDistribution) packageVersion(pkg string) string {
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(output))
}
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
"dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
case deps.WindowManagerMango:
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantStable {
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking XBPS...",
IsComplete: false,
LogOutput: "Checking for xbps-install",
}
if _, err := exec.LookPath("xbps-install"); err != nil {
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
}
return nil
}
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if wm == deps.WindowManagerHyprland {
arch, err := v.xbpsArch(ctx)
if err != nil {
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
}
if arch != "x86_64" {
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
}
}
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if len(xbpsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling DMS XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
LogOutput: "Setting up custom XBPS repositories for DMS packages",
}
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
}
}
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
if len(allPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
}
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install XBPS packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable Void session services: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if !v.isRunitSystem() {
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
return nil
}
for _, service := range []string{"dbus", "elogind"} {
if !v.runitServiceInstalled(service) {
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
continue
}
if v.runitServiceEnabled(service) {
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.92,
Step: fmt.Sprintf("Enabling %s runit service...", service),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
}
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
}
return nil
}
func (v *VoidDistribution) isRunitSystem() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
return err == nil && fi.IsDir()
}
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
return err == nil
}
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
systemPkgs := []string{}
xbpsPkgs := []PackageMapping{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeXBPS:
xbpsPkgs = append(xbpsPkgs, pkgInfo)
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
}
}
return systemPkgs, xbpsPkgs
}
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
enabledRepoURLs := []string{}
for _, pkg := range xbpsPkgs {
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
continue
}
repoName := v.xbpsRepoName(pkg.RepoURL)
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
}
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
}
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
}
if len(enabledRepos) > 0 {
syncArgs := []string{"xbps-install", "-Sy", "-i"}
for _, repoURL := range enabledRepoURLs {
syncArgs = append(syncArgs, "--repository", repoURL)
}
syncCommand := strings.Join(syncArgs, " ")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Synchronizing XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
LogOutput: "Synchronizing XBPS repository indexes",
}
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
}
}
return nil
}
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
switch repoURL {
case VoidDMSRepo:
return "dms"
case VoidDankLinuxRepo:
return "danklinux"
case VoidHyprlandRepo:
return "hyprland"
default:
name := strings.TrimPrefix(repoURL, "https://")
name = strings.TrimPrefix(name, "http://")
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
return name
}
}
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
args := append([]string{"xbps-install", "-Sy"}, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing XBPS packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
}
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
seen := make(map[string]bool)
var unique []string
for _, group := range groups {
for _, pkg := range group {
if pkg == "" || seen[pkg] {
continue
}
seen[pkg] = true
unique = append(unique, pkg)
}
}
return unique
}
-150
View File
@@ -30,11 +30,6 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
const GreeterCacheDir = "/var/cache/dms-greeter" const GreeterCacheDir = "/var/cache/dms-greeter"
const (
runitSvDir = "/etc/sv"
runitServiceDir = "/var/service"
)
func DetectDMSPath() (string, error) { func DetectDMSPath() (string, error) {
return config.LocateDMSConfig() return config.LocateDMSConfig()
} }
@@ -46,96 +41,6 @@ func IsNixOS() bool {
return err == nil return err == nil
} }
func IsVoidLinux() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
func isRunit() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(runitSvDir, name))
return err == nil && fi.IsDir()
}
func runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(runitServiceDir, name))
return err == nil
}
func enableRunitService(name, sudoPassword string) error {
if !runitServiceInstalled(name) {
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
}
if runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "ln", "-sf",
filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name))
}
func disableRunitService(name, sudoPassword string) error {
if !runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "rm", "-f",
filepath.Join(runitServiceDir, name))
}
func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) {
if runitServiceInstalled("seatd") {
if err := enableRunitService("seatd", sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err))
} else {
logFunc("✓ seatd enabled")
}
} else {
logFunc("⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
}
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err))
} else {
logFunc(fmt.Sprintf("✓ %s added to seat groups (_seatd, video, input)", greeterUser))
}
}
func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) {
const pamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(pamPath)
if err != nil {
logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err))
return
}
if strings.Contains(string(data), "pam_rundir") {
logFunc("✓ pam_rundir already present in greetd PAM")
return
}
line := "session optional pam_rundir.so"
if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err))
return
}
logFunc("✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
}
func DetectGreeterGroup() string { func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group") data, err := os.ReadFile("/etc/group")
if err != nil { if err != nil {
@@ -861,8 +766,6 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd") installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
case distros.FamilyGentoo: case distros.FamilyGentoo:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd") installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
case distros.FamilyVoid:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy greetd")
case distros.FamilyNix: case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix") return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
default: default:
@@ -989,14 +892,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
} }
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper) failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git") installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
case distros.FamilyVoid:
failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter"
logFunc("Adding DMS XBPS repository...")
if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil {
logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err))
}
privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run()
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter")
default: default:
return false return false
} }
@@ -1014,20 +909,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
return true return true
} }
func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error {
confPath := filepath.Join("/etc/xbps.d", name+".conf")
repoLine := fmt.Sprintf("repository=%s", repoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
return nil
}
if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil {
return err
}
return privesc.Run(ctx, sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath))
}
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if IsGreeterPackaged() { if IsGreeterPackaged() {
@@ -2394,19 +2275,6 @@ func checkSystemdEnabled(service string) (string, error) {
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error { func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
for _, dm := range conflictingDMs { for _, dm := range conflictingDMs {
if isRunit() {
if !runitServiceEnabled(dm) {
continue
}
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
if err := disableRunitService(dm, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
} else {
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
}
continue
}
state, err := checkSystemdEnabled(dm) state, err := checkSystemdEnabled(dm)
if err != nil || state == "" || state == "not-found" { if err != nil || state == "" || state == "not-found" {
continue continue
@@ -2426,19 +2294,6 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
// EnableGreetd unmasks and enables greetd, forcing it over any other DM. // EnableGreetd unmasks and enables greetd, forcing it over any other DM.
func EnableGreetd(sudoPassword string, logFunc func(string)) error { func EnableGreetd(sudoPassword string, logFunc func(string)) error {
if isRunit() {
if !runitServiceInstalled("greetd") {
return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir)
}
ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc)
ensureGreetdPamRundir(sudoPassword, logFunc)
if err := enableRunitService("greetd", sudoPassword); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
logFunc(fmt.Sprintf("✓ greetd enabled (%s)", runitServiceDir))
return nil
}
state, err := checkSystemdEnabled("greetd") state, err := checkSystemdEnabled("greetd")
if err != nil { if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err) return fmt.Errorf("failed to check greetd state: %w", err)
@@ -2462,11 +2317,6 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
} }
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error { func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
if isRunit() {
logFunc("✓ runit detected; no graphical target is needed")
return nil
}
cmd := exec.Command("systemctl", "get-default") cmd := exec.Command("systemctl", "get-default")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
+7 -40
View File
@@ -41,8 +41,6 @@ type Config struct {
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty") ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
ReplaceConfigsAll bool // deploy/replace all configurations ReplaceConfigsAll bool // deploy/replace all configurations
Yes bool Yes bool
DankSearch bool // install danksearch and enable its user service
DankCalendar bool // install dankcalendar
} }
// Runner orchestrates unattended (headless) installation. // Runner orchestrates unattended (headless) installation.
@@ -216,11 +214,6 @@ func (r *Runner) Run() error {
return fmt.Errorf("package installation failed: %w", err) return fmt.Errorf("package installation failed: %w", err)
} }
useSystemd := true
if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid {
useSystemd = false
}
// 9. Greeter setup (if dms-greeter was included) // 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") { if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri" compositorName := "niri"
@@ -238,32 +231,18 @@ func (r *Runner) Run() error {
} }
} }
// 9b. danksearch service setup (if danksearch was included)
if useSystemd && !disabledItems["danksearch"] && r.depExists(dependencies, "danksearch") {
fmt.Fprintln(os.Stdout, "Enabling danksearch service...")
logFunc := func(line string) {
r.log(line)
fmt.Fprintf(os.Stdout, " danksearch: %s\n", line)
}
if err := distros.SetupDsearchService(context.Background(), logFunc); err != nil {
// Non-fatal, matching greeter behavior
fmt.Fprintf(os.Stderr, "Warning: danksearch service setup issue (non-fatal): %v\n", err)
}
}
// 10. Deploy configurations // 10. Deploy configurations
fmt.Fprintln(os.Stdout, "Deploying configurations...") fmt.Fprintln(os.Stdout, "Deploying configurations...")
r.log("Starting configuration deployment") r.log("Starting configuration deployment")
deployer := config.NewConfigDeployer(r.logChan) deployer := config.NewConfigDeployer(r.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd( results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
context.Background(), context.Background(),
wm, wm,
terminal, terminal,
dependencies, dependencies,
replaceConfigs, replaceConfigs,
reinstallItems, reinstallItems,
useSystemd,
) )
if err != nil { if err != nil {
return fmt.Errorf("configuration deployment failed: %w", err) return fmt.Errorf("configuration deployment failed: %w", err)
@@ -288,31 +267,19 @@ func (r *Runner) Run() error {
} }
// buildDisabledItems computes the set of dependencies that should be skipped // buildDisabledItems computes the set of dependencies that should be skipped
// during installation. Optional components are opt-in (disabled by default), // during installation, applying the --include-deps and --exclude-deps filters.
// then re-enabled by the dedicated flags and --include-deps. // dms-greeter is disabled by default (opt-in), matching TUI behavior.
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) { func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
disabledItems := make(map[string]bool) disabledItems := make(map[string]bool)
// dms-greeter is opt-in (disabled by default), matching TUI behavior
for i := range dependencies { for i := range dependencies {
if !dependencies[i].Required { if dependencies[i].Name == "dms-greeter" {
disabledItems[dependencies[i].Name] = true disabledItems["dms-greeter"] = true
break
} }
} }
// Dedicated flags resolve before include/exclude
if r.cfg.DankSearch {
if !r.depExists(dependencies, "danksearch") {
return nil, fmt.Errorf("--danksearch: not available on this distribution")
}
delete(disabledItems, "danksearch")
}
if r.cfg.DankCalendar {
if !r.depExists(dependencies, "dankcalendar") {
return nil, fmt.Errorf("--dankcalendar: not available on this distribution")
}
delete(disabledItems, "dankcalendar")
}
// Process --include-deps (enable items that are disabled by default) // Process --include-deps (enable items that are disabled by default)
for _, name := range r.cfg.IncludeDeps { for _, name := range r.cfg.IncludeDeps {
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
+17 -51
View File
@@ -342,21 +342,17 @@ func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
func TestBuildDisabledItems(t *testing.T) { func TestBuildDisabledItems(t *testing.T) {
dependencies := []deps.Dependency{ dependencies := []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled, Required: true}, {Name: "niri", Status: deps.StatusInstalled},
{Name: "ghostty", Status: deps.StatusMissing, Required: true}, {Name: "ghostty", Status: deps.StatusMissing},
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled, Required: true}, {Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
{Name: "dms-greeter", Status: deps.StatusMissing}, {Name: "dms-greeter", Status: deps.StatusMissing},
{Name: "danksearch", Status: deps.StatusMissing}, {Name: "waybar", Status: deps.StatusMissing},
{Name: "dankcalendar", Status: deps.StatusMissing},
{Name: "waybar", Status: deps.StatusMissing, Required: true},
} }
tests := []struct { tests := []struct {
name string name string
includeDeps []string includeDeps []string
excludeDeps []string excludeDeps []string
dankSearch bool
dankCalendar bool
deps []deps.Dependency // nil means use the shared fixture deps []deps.Dependency // nil means use the shared fixture
wantErr bool wantErr bool
errContains string // substring expected in error message errContains string // substring expected in error message
@@ -364,20 +360,19 @@ func TestBuildDisabledItems(t *testing.T) {
wantEnabled []string // dep names that should NOT be in disabledItems (extra check) wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
}{ }{
{ {
name: "no flags set, optional deps disabled by default", name: "no flags set, dms-greeter disabled by default",
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar"}, wantDisabled: []string{"dms-greeter"},
wantEnabled: []string{"niri", "ghostty", "waybar"}, wantEnabled: []string{"niri", "ghostty", "waybar"},
}, },
{ {
name: "include dms-greeter enables it", name: "include dms-greeter enables it",
includeDeps: []string{"dms-greeter"}, includeDeps: []string{"dms-greeter"},
wantEnabled: []string{"dms-greeter"}, wantEnabled: []string{"dms-greeter"},
wantDisabled: []string{"danksearch", "dankcalendar"},
}, },
{ {
name: "exclude a regular dep", name: "exclude a regular dep",
excludeDeps: []string{"waybar"}, excludeDeps: []string{"waybar"},
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar", "waybar"}, wantDisabled: []string{"dms-greeter", "waybar"},
}, },
{ {
name: "include unknown dep returns error", name: "include unknown dep returns error",
@@ -404,53 +399,24 @@ func TestBuildDisabledItems(t *testing.T) {
wantDisabled: []string{"dms-greeter"}, wantDisabled: []string{"dms-greeter"},
}, },
{ {
name: "whitespace entries are skipped", name: "whitespace entries are skipped",
includeDeps: []string{" ", "dms-greeter"}, includeDeps: []string{" ", "dms-greeter"},
wantEnabled: []string{"dms-greeter"}, wantEnabled: []string{"dms-greeter"},
wantDisabled: []string{"danksearch", "dankcalendar"},
}, },
{ {
name: "no optional deps present, nothing disabled by default", name: "no dms-greeter in deps, nothing disabled by default",
deps: []deps.Dependency{ deps: []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled, Required: true}, {Name: "niri", Status: deps.StatusInstalled},
}, },
wantEnabled: []string{"niri"}, wantEnabled: []string{"niri"},
}, },
{
name: "danksearch flag enables it",
dankSearch: true,
wantEnabled: []string{"danksearch"},
wantDisabled: []string{"dms-greeter", "dankcalendar"},
},
{
name: "dankcalendar flag enables it",
dankCalendar: true,
wantEnabled: []string{"dankcalendar"},
wantDisabled: []string{"dms-greeter", "danksearch"},
},
{
name: "danksearch flag when unavailable errors",
dankSearch: true,
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
wantErr: true,
errContains: "--danksearch",
},
{
name: "dankcalendar flag when unavailable errors",
dankCalendar: true,
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
wantErr: true,
errContains: "--dankcalendar",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{ r := NewRunner(Config{
IncludeDeps: tt.includeDeps, IncludeDeps: tt.includeDeps,
ExcludeDeps: tt.excludeDeps, ExcludeDeps: tt.excludeDeps,
DankSearch: tt.dankSearch,
DankCalendar: tt.dankCalendar,
}) })
d := tt.deps d := tt.deps
if d == nil { if d == nil {
+1 -1
View File
@@ -1154,7 +1154,7 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
if err != nil { if err != nil {
return nil, err return nil, err
} }
lines := expandLuaConfigLines(strings.Split(string(data), "\n")) lines := strings.Split(string(data), "\n")
parser := NewHyprlandParser("") parser := NewHyprlandParser("")
pendingUnbinds := make(map[string]string) pendingUnbinds := make(map[string]string)
for _, line := range lines { for _, line := range lines {
@@ -1,414 +0,0 @@
package providers
import (
"maps"
"regexp"
"strconv"
"strings"
)
// Lua configs can express binds dynamically: variables (mainMod .. " + C"),
// tostring() calls, and numeric for loops (workspace binds). This resolves such
// expressions to literal key combos so the static bind parser can read them.
const luaMaxLoopIterations = 1000
var (
luaAssignRE = regexp.MustCompile(`^(?:local\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$`)
luaForRE = regexp.MustCompile(`^for\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(-?\d+)\s*,\s*(-?\d+)\s*(?:,\s*(-?\d+)\s*)?do\b(.*)$`)
luaTostringRE = regexp.MustCompile(`to(?:string|number)\s*\(\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|-?\d+(?:\.\d+)?)\s*\)`)
luaBlockOpenRE = regexp.MustCompile(`\b(?:function|for|while|if)\b`)
luaBlockCloseRE = regexp.MustCompile(`\bend\b`)
luaNumberRE = regexp.MustCompile(`^-?\d+(?:\.\d+)?$`)
)
type luaVarEnv map[string]string
type luaForHeader struct {
varName string
start int
stop int
step int
inline string
}
func expandLuaConfigLines(lines []string) []string {
return expandLuaBlock(lines, luaVarEnv{})
}
func expandLuaBlock(lines []string, env luaVarEnv) []string {
out := make([]string, 0, len(lines))
for i := 0; i < len(lines); i++ {
code := strings.TrimSpace(luaStripLineComment(lines[i]))
if name, value, ok := parseLuaStringAssignment(code, env); ok {
env[name] = value
out = append(out, lines[i])
continue
}
header, ok := parseLuaForHeader(code)
if !ok {
out = append(out, resolveLuaDynamicLine(lines[i], env))
continue
}
body, consumed, complete := collectLuaForBody(header, lines, i)
if !complete {
out = append(out, resolveLuaDynamicLine(lines[i], env))
continue
}
out = append(out, expandLuaForLoop(header, body, env)...)
i = consumed
}
return out
}
func parseLuaStringAssignment(code string, env luaVarEnv) (name, value string, ok bool) {
m := luaAssignRE.FindStringSubmatch(code)
if m == nil {
return "", "", false
}
value, ok = evalLuaConcat(m[2], env)
if !ok {
return "", "", false
}
return m[1], value, true
}
func parseLuaForHeader(code string) (luaForHeader, bool) {
m := luaForRE.FindStringSubmatch(code)
if m == nil {
return luaForHeader{}, false
}
start, _ := strconv.Atoi(m[2])
stop, _ := strconv.Atoi(m[3])
step := 1
if m[4] != "" {
step, _ = strconv.Atoi(m[4])
}
if step == 0 {
return luaForHeader{}, false
}
return luaForHeader{varName: m[1], start: start, stop: stop, step: step, inline: strings.TrimSpace(m[5])}, true
}
func collectLuaForBody(header luaForHeader, lines []string, headerIdx int) (body []string, consumed int, complete bool) {
depth := 1
if header.inline != "" {
delta := luaBlockDelta(header.inline)
if depth+delta <= 0 {
if stmt := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(header.inline), "end")); stmt != "" {
body = append(body, stmt)
}
return body, headerIdx, true
}
depth += delta
body = append(body, header.inline)
}
for j := headerIdx + 1; j < len(lines); j++ {
delta := luaBlockDelta(lines[j])
if depth+delta <= 0 {
return body, j, true
}
depth += delta
body = append(body, lines[j])
}
return nil, headerIdx, false
}
func expandLuaForLoop(header luaForHeader, body []string, env luaVarEnv) []string {
var out []string
inRange := func(v int) bool {
if header.step > 0 {
return v <= header.stop
}
return v >= header.stop
}
count := 0
for v := header.start; inRange(v); v += header.step {
if count++; count > luaMaxLoopIterations {
break
}
value := strconv.Itoa(v)
iterLines := make([]string, len(body))
for k, bl := range body {
iterLines[k] = substituteLuaIdent(bl, header.varName, value)
}
out = append(out, expandLuaBlock(iterLines, cloneLuaEnv(env))...)
}
return out
}
func luaBlockDelta(line string) int {
masked := luaMaskStrings(line)
return len(luaBlockOpenRE.FindAllString(masked, -1)) - len(luaBlockCloseRE.FindAllString(masked, -1))
}
func resolveLuaDynamicLine(line string, env luaVarEnv) string {
if !strings.Contains(line, "hl.bind") && !strings.Contains(line, "hl.unbind") {
return line
}
line = normalizeLuaToString(line)
return rewriteLuaBindKeyArg(line, env)
}
func normalizeLuaToString(line string) string {
return luaTostringRE.ReplaceAllStringFunc(line, func(m string) string {
inner := luaTostringRE.FindStringSubmatch(m)[1]
if inner[0] == '"' || inner[0] == '\'' {
return inner
}
return strconv.Quote(inner)
})
}
func rewriteLuaBindKeyArg(line string, env luaVarEnv) string {
for _, fn := range []string{"hl.bind", "hl.unbind"} {
idx := strings.Index(line, fn)
if idx < 0 {
continue
}
open := skipLuaWS(line, idx+len(fn))
if open >= len(line) || line[open] != '(' {
continue
}
argStart := skipLuaWS(line, open+1)
expr, end, ok := parseLuaFirstArgExpr(line, argStart)
if !ok || isLuaPlainStringArg(expr) {
continue
}
value, ok := evalLuaConcat(expr, env)
if !ok {
continue
}
return line[:argStart] + strconv.Quote(value) + line[end:]
}
return line
}
func evalLuaConcat(expr string, env luaVarEnv) (string, bool) {
parts := splitLuaConcat(expr)
var sb strings.Builder
for _, part := range parts {
value, ok := evalLuaOperand(part, env)
if !ok {
return "", false
}
sb.WriteString(value)
}
return sb.String(), true
}
func evalLuaOperand(op string, env luaVarEnv) (string, bool) {
op = strings.TrimSpace(op)
if op == "" {
return "", false
}
switch op[0] {
case '"', '\'':
s, next, ok := parseLuaStringLiteral(op, 0)
return s, next == len(op) && ok
}
if luaNumberRE.MatchString(op) {
return op, true
}
if inner, ok := luaUnwrapCall(op, "tostring"); ok {
return evalLuaConcat(inner, env)
}
if inner, ok := luaUnwrapCall(op, "tonumber"); ok {
return evalLuaConcat(inner, env)
}
if value, ok := env[op]; ok {
return value, true
}
return "", false
}
func splitLuaConcat(expr string) []string {
var parts []string
parenDepth, braceDepth, bracketDepth := 0, 0, 0
inStr := byte(0)
esc := false
start := 0
for i := 0; i < len(expr); i++ {
c := expr[i]
if inStr != 0 {
switch {
case esc:
esc = false
case c == '\\' && inStr == '"':
esc = true
case c == inStr:
inStr = 0
}
continue
}
switch c {
case '"', '\'':
inStr = c
case '(':
parenDepth++
case ')':
if parenDepth > 0 {
parenDepth--
}
case '{':
braceDepth++
case '}':
if braceDepth > 0 {
braceDepth--
}
case '[':
bracketDepth++
case ']':
if bracketDepth > 0 {
bracketDepth--
}
case '.':
if parenDepth == 0 && braceDepth == 0 && bracketDepth == 0 && i+1 < len(expr) && expr[i+1] == '.' {
parts = append(parts, expr[start:i])
i++
start = i + 1
}
}
}
return append(parts, expr[start:])
}
func substituteLuaIdent(line, name, value string) string {
if !strings.Contains(line, name) {
return line
}
var sb strings.Builder
inStr := byte(0)
esc := false
for i := 0; i < len(line); {
c := line[i]
if inStr != 0 {
sb.WriteByte(c)
switch {
case esc:
esc = false
case c == '\\' && inStr == '"':
esc = true
case c == inStr:
inStr = 0
}
i++
continue
}
if c == '"' || c == '\'' {
inStr = c
sb.WriteByte(c)
i++
continue
}
if isLuaIdentStart(c) {
j := i + 1
for j < len(line) && isLuaIdentByte(line[j]) {
j++
}
word := line[i:j]
if word == name && (i == 0 || line[i-1] != '.') {
sb.WriteString(value)
} else {
sb.WriteString(word)
}
i = j
continue
}
sb.WriteByte(c)
i++
}
return sb.String()
}
func luaMaskStrings(line string) string {
b := []byte(line)
inStr := byte(0)
esc := false
for i := 0; i < len(b); i++ {
c := b[i]
if inStr != 0 {
wasEnd := !esc && c == inStr
esc = !esc && c == '\\' && inStr == '"'
b[i] = ' '
if wasEnd {
inStr = 0
}
continue
}
switch c {
case '"', '\'':
inStr = c
b[i] = ' '
case '-':
if i+1 < len(b) && b[i+1] == '-' {
for ; i < len(b); i++ {
b[i] = ' '
}
}
}
}
return string(b)
}
func luaStripLineComment(line string) string {
inStr := byte(0)
esc := false
for i := 0; i+1 < len(line); i++ {
c := line[i]
if inStr != 0 {
switch {
case esc:
esc = false
case c == '\\' && inStr == '"':
esc = true
case c == inStr:
inStr = 0
}
continue
}
switch c {
case '"', '\'':
inStr = c
case '-':
if line[i+1] == '-' {
return line[:i]
}
}
}
return line
}
func luaUnwrapCall(op, fn string) (string, bool) {
op = strings.TrimSpace(op)
if !strings.HasPrefix(op, fn) {
return "", false
}
rest := strings.TrimSpace(op[len(fn):])
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
return "", false
}
return rest[1 : len(rest)-1], true
}
func isLuaPlainStringArg(expr string) bool {
expr = strings.TrimSpace(expr)
if expr == "" || (expr[0] != '"' && expr[0] != '\'') {
return false
}
_, next, ok := parseLuaStringLiteral(expr, 0)
return ok && next == len(expr)
}
func isLuaIdentStart(c byte) bool {
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
func cloneLuaEnv(env luaVarEnv) luaVarEnv {
clone := make(luaVarEnv, len(env))
maps.Copy(clone, env)
return clone
}
@@ -1,134 +0,0 @@
package providers
import (
"strings"
"testing"
)
func TestExpandLuaConfigLinesVariableConcat(t *testing.T) {
lines := []string{
`local mainMod = "SUPER"`,
`hl.bind(mainMod .. " + C", hl.dsp.window.close())`,
`hl.bind(mainMod .. " + H", hl.dsp.focus({direction = "l"}))`,
`hl.bind("ALT + TAB", hl.dsp.window.cycle_next({}))`,
}
got := expandLuaConfigLines(lines)
want := []string{
`hl.bind("SUPER + C",`,
`hl.bind("SUPER + H",`,
`hl.bind("ALT + TAB",`,
}
joined := strings.Join(got, "\n")
for _, w := range want {
if !strings.Contains(joined, w) {
t.Errorf("expanded output missing %q\n---\n%s", w, joined)
}
}
}
func TestExpandLuaConfigLinesForLoop(t *testing.T) {
lines := []string{
`local mainMod = "SUPER"`,
`for i = 1, 3 do`,
` hl.bind(mainMod .. " + " .. i, hl.dsp.focus({workspace = tostring(i)}))`,
` hl.bind(mainMod .. " SHIFT + " .. i, hl.dsp.window.move({workspace = tostring(i)}))`,
`end`,
}
got := strings.Join(expandLuaConfigLines(lines), "\n")
for _, w := range []string{
`hl.bind("SUPER + 1",`,
`hl.bind("SUPER + 2",`,
`hl.bind("SUPER + 3",`,
`hl.bind("SUPER SHIFT + 3",`,
`{workspace = "1"}`,
} {
if !strings.Contains(got, w) {
t.Errorf("expanded loop missing %q\n---\n%s", w, got)
}
}
}
func TestParseLuaLinesDynamicBinds(t *testing.T) {
content := strings.Join([]string{
`local mainMod = "SUPER"`,
`hl.bind(mainMod .. " + C", hl.dsp.window.close())`,
`hl.bind("ALT + TAB", hl.dsp.window.cycle_next({}))`,
`for i = 1, 2 do`,
` hl.bind(mainMod .. " + " .. i, hl.dsp.focus({workspace = tostring(i)}))`,
`end`,
}, "\n")
parser := NewHyprlandParser("")
section, err := parser.parseLuaLines(content, "", "test.lua", "")
if err != nil {
t.Fatalf("parseLuaLines: %v", err)
}
keys := map[string]*HyprlandKeyBinding{}
for i := range section.Keybinds {
kb := &section.Keybinds[i]
keys[parser.formatBindKey(kb)] = kb
}
for _, want := range []string{"SUPER+C", "ALT+TAB", "SUPER+1", "SUPER+2"} {
if _, ok := keys[want]; !ok {
t.Errorf("missing bind %q; got %v", want, keysList(keys))
}
}
if kb := keys["SUPER+C"]; kb != nil && kb.Dispatcher != "killactive" {
t.Errorf("SUPER+C dispatcher = %q, want killactive", kb.Dispatcher)
}
if kb := keys["SUPER+1"]; kb != nil {
if kb.Dispatcher != "workspace" || kb.Params != "1" {
t.Errorf("SUPER+1 = %q %q, want workspace 1", kb.Dispatcher, kb.Params)
}
}
}
func keysList(m map[string]*HyprlandKeyBinding) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func TestEvalLuaConcat(t *testing.T) {
env := luaVarEnv{"mainMod": "SUPER", "i": "5"}
tests := []struct {
expr string
want string
ok bool
}{
{`mainMod .. " + C"`, "SUPER + C", true},
{`mainMod .. " + " .. i`, "SUPER + 5", true},
{`mainMod .. " + " .. tostring(i)`, "SUPER + 5", true},
{`"ALT + TAB"`, "ALT + TAB", true},
{`mainMod .. someFunc()`, "", false},
{`unknownVar .. "x"`, "", false},
}
for _, tt := range tests {
got, ok := evalLuaConcat(tt.expr, env)
if ok != tt.ok || (ok && got != tt.want) {
t.Errorf("evalLuaConcat(%q) = %q,%v want %q,%v", tt.expr, got, ok, tt.want, tt.ok)
}
}
}
func TestSubstituteLuaIdent(t *testing.T) {
tests := []struct {
line, name, value, want string
}{
{`hl.bind(m .. " + " .. i, x)`, "i", "3", `hl.bind(m .. " + " .. 3, x)`},
{`hl.dsp.exec("light -i")`, "i", "3", `hl.dsp.exec("light -i")`},
{`foo.i`, "i", "3", `foo.i`},
{`tostring(i)`, "i", "3", `tostring(3)`},
}
for _, tt := range tests {
if got := substituteLuaIdent(tt.line, tt.name, tt.value); got != tt.want {
t.Errorf("substituteLuaIdent(%q,%q,%q) = %q, want %q", tt.line, tt.name, tt.value, got, tt.want)
}
}
}
@@ -623,7 +623,7 @@ func (p *HyprlandParser) parseLuaLines(content string, baseDir, absPath, section
prevSource := p.currentSource prevSource := p.currentSource
p.currentSource = absPath p.currentSource = absPath
lines := expandLuaConfigLines(strings.Split(content, "\n")) lines := strings.Split(content, "\n")
boundInFile := make(map[string]bool) boundInFile := make(map[string]bool)
for _, line := range lines { for _, line := range lines {
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
+9 -36
View File
@@ -19,20 +19,6 @@ type NiriProvider struct {
parsed bool parsed bool
} }
type niriActionPart struct {
value string
quoted bool
}
var niriActionPropertyOrder = []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"}
var niriActionProperties = map[string]struct{}{
"focus": {},
"show-pointer": {},
"write-to-disk": {},
"skip-confirmation": {},
"delay-ms": {},
}
func NewNiriProvider(configDir string) *NiriProvider { func NewNiriProvider(configDir string) *NiriProvider {
if configDir == "" { if configDir == "" {
configDir = defaultNiriConfigDir() configDir = defaultNiriConfigDir()
@@ -69,7 +55,6 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
sheet := &keybinds.CheatSheet{ sheet := &keybinds.CheatSheet{
Title: "Niri Keybinds", Title: "Niri Keybinds",
Provider: n.Name(), Provider: n.Name(),
ModKey: result.ModKey,
Binds: categorizedBinds, Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded, DMSBindsIncluded: result.DMSBindsIncluded,
} }
@@ -367,7 +352,7 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
} }
if actionNode.Properties != nil { if actionNode.Properties != nil {
for _, propName := range niriActionPropertyOrder { for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
if val, ok := actionNode.Properties.Get(propName); ok { if val, ok := actionNode.Properties.Get(propName); ok {
parts = append(parts, propName+"="+val.String()) parts = append(parts, propName+"="+val.String())
} }
@@ -455,10 +440,10 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
return node return node
} }
node.SetName(parts[0].value) node.SetName(parts[0])
for _, arg := range parts[1:] { for _, arg := range parts[1:] {
if n.isNiriActionPropertyToken(arg) { if strings.Contains(arg, "=") {
kv := strings.SplitN(arg.value, "=", 2) kv := strings.SplitN(arg, "=", 2)
switch kv[1] { switch kv[1] {
case "true": case "true":
node.AddProperty(kv[0], true, "") node.AddProperty(kv[0], true, "")
@@ -469,25 +454,13 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
} }
continue continue
} }
node.AddArgument(arg.value, "") node.AddArgument(arg, "")
} }
return node return node
} }
func (n *NiriProvider) isNiriActionPropertyToken(part niriActionPart) bool { func (n *NiriProvider) parseActionParts(action string) []string {
if part.quoted || !strings.Contains(part.value, "=") { var parts []string
return false
}
key, _, ok := strings.Cut(part.value, "=")
if !ok {
return false
}
_, ok = niriActionProperties[key]
return ok
}
func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
var parts []niriActionPart
var current strings.Builder var current strings.Builder
var inQuote, escaped, wasQuoted bool var inQuote, escaped, wasQuoted bool
@@ -503,7 +476,7 @@ func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
inQuote = !inQuote inQuote = !inQuote
case r == ' ' && !inQuote: case r == ' ' && !inQuote:
if current.Len() > 0 || wasQuoted { if current.Len() > 0 || wasQuoted {
parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted}) parts = append(parts, current.String())
current.Reset() current.Reset()
wasQuoted = false wasQuoted = false
} }
@@ -512,7 +485,7 @@ func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
} }
} }
if current.Len() > 0 || wasQuoted { if current.Len() > 0 || wasQuoted {
parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted}) parts = append(parts, current.String())
} }
return parts return parts
} }
@@ -33,7 +33,6 @@ type NiriSection struct {
type NiriParser struct { type NiriParser struct {
configDir string configDir string
modKey string
processedFiles map[string]bool processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding bindMap map[string]*NiriKeyBinding
bindOrder []string bindOrder []string
@@ -238,7 +237,6 @@ func isBraceAdjacentSpace(b byte) bool {
func NewNiriParser(configDir string) *NiriParser { func NewNiriParser(configDir string) *NiriParser {
return &NiriParser{ return &NiriParser{
configDir: configDir, configDir: configDir,
modKey: "Super",
processedFiles: make(map[string]bool), processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding), bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{}, bindOrder: []string{},
@@ -379,8 +377,6 @@ func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection,
switch name { switch name {
case "include": case "include":
p.handleInclude(node, section, baseDir) p.handleInclude(node, section, baseDir)
case "input":
p.handleInput(node)
case "binds": case "binds":
p.extractBinds(node, section, "") p.extractBinds(node, section, "")
case "recent-windows": case "recent-windows":
@@ -389,19 +385,6 @@ func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection,
} }
} }
func (p *NiriParser) handleInput(node *document.Node) {
for _, child := range node.Children {
if child.Name.String() != "mod-key" || len(child.Arguments) == 0 {
continue
}
modKey := strings.Trim(strings.TrimSpace(child.Arguments[0].String()), "\"")
if modKey != "" {
p.modKey = modKey
}
}
}
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) { func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
if len(node.Arguments) == 0 { if len(node.Arguments) == 0 {
return return
@@ -551,7 +534,6 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
type NiriParseResult struct { type NiriParseResult struct {
Section *NiriSection Section *NiriSection
ModKey string
DMSBindsIncluded bool DMSBindsIncluded bool
DMSStatus *DMSBindsStatusInfo DMSStatus *DMSBindsStatusInfo
ConflictingConfigs map[string]*NiriKeyBinding ConflictingConfigs map[string]*NiriKeyBinding
@@ -604,7 +586,6 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
} }
return &NiriParseResult{ return &NiriParseResult{
Section: section, Section: section,
ModKey: parser.modKey,
DMSBindsIncluded: parser.HasDMSBindsIncluded(), DMSBindsIncluded: parser.HasDMSBindsIncluded(),
DMSStatus: parser.buildDMSStatus(), DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs, ConflictingConfigs: parser.conflictingConfigs,
@@ -7,28 +7,6 @@ import (
"testing" "testing"
) )
func TestNiriParseModKey(t *testing.T) {
config := `input {
mod-key "Alt"
}
binds {
Mod+T { spawn "kitty"; }
}
`
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if result.ModKey != "Alt" {
t.Errorf("ModKey = %q, want %q", result.ModKey, "Alt")
}
}
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) { func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
config := `recent-windows { config := `recent-windows {
binds { binds {
+1 -60
View File
@@ -17,10 +17,7 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl") configFile := filepath.Join(tmpDir, "config.kdl")
content := `input { content := `binds {
mod-key "Alt"
}
binds {
Mod+Q { close-window; } Mod+Q { close-window; }
Mod+F { fullscreen-window; } Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
@@ -48,10 +45,6 @@ binds {
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri") t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
} }
if cheatSheet.ModKey != "Alt" {
t.Errorf("ModKey = %q, want %q", cheatSheet.ModKey, "Alt")
}
windowBinds := cheatSheet.Binds["Window"] windowBinds := cheatSheet.Binds["Window"]
if len(windowBinds) < 2 { if len(windowBinds) < 2 {
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds)) t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
@@ -234,58 +227,6 @@ func TestNiriGenerateBindsContent(t *testing.T) {
expected: `binds { expected: `binds {
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
} }
`,
},
{
name: "spawn with equals arg",
binds: map[string]*overrideBind{
"Mod+B": {
Key: "Mod+B",
Action: `spawn /opt/browser --profile-directory=Default`,
},
},
expected: `binds {
Mod+B { spawn "/opt/browser" "--profile-directory=Default"; }
}
`,
},
{
name: "spawn shell command with quoted equals args",
binds: map[string]*overrideBind{
"Mod+C": {
Key: "Mod+C",
Action: `spawn sh -c "chrome --profile-directory=Default --app=x"`,
},
},
expected: `binds {
Mod+C { spawn "sh" "-c" "chrome --profile-directory=Default --app=x"; }
}
`,
},
{
name: "spawn env assignment stays arg",
binds: map[string]*overrideBind{
"Mod+E": {
Key: "Mod+E",
Action: `spawn env FOO=bar mycmd`,
},
},
expected: `binds {
Mod+E { spawn "env" "FOO=bar" "mycmd"; }
}
`,
},
{
name: "niri action property remains property",
binds: map[string]*overrideBind{
"Print": {
Key: "Print",
Action: `screenshot show-pointer=false`,
},
},
expected: `binds {
Print { screenshot show-pointer=false; }
}
`, `,
}, },
{ {
-1
View File
@@ -32,7 +32,6 @@ type DMSBindsStatus struct {
type CheatSheet struct { type CheatSheet struct {
Title string `json:"title"` Title string `json:"title"`
Provider string `json:"provider"` Provider string `json:"provider"`
ModKey string `json:"modKey,omitempty"`
Binds map[string][]Keybind `json:"binds"` Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"` DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"` DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
+3 -5
View File
@@ -68,8 +68,6 @@ func GetQtLoggingRules() string {
level = "info" level = "info"
} }
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
// probes); suppressed except at debug level
var rules []string var rules []string
switch strings.ToLower(level) { switch strings.ToLower(level) {
case "fatal": case "fatal":
@@ -77,13 +75,13 @@ func GetQtLoggingRules() string {
case "error": case "error":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"} rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
case "warn", "warning": case "warn", "warning":
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"} rules = []string{"*.debug=false", "*.info=false"}
case "info": case "info":
rules = []string{"*.debug=false", "scene.warning=false"} rules = []string{"*.debug=false"}
case "debug": case "debug":
return "" return ""
default: default:
rules = []string{"*.debug=false", "scene.warning=false"} rules = []string{"*.debug=false"}
} }
return strings.Join(rules, ";") return strings.Join(rules, ";")
+1 -73
View File
@@ -118,50 +118,6 @@ type ColorsOutput struct {
} `json:"colors"` } `json:"colors"`
} }
type SchemePreview struct {
Dark string `json:"dark"`
Light string `json:"light"`
}
var previewSchemeTypes = []string{
"scheme-tonal-spot",
"scheme-vibrant",
"scheme-content",
"scheme-expressive",
"scheme-fidelity",
"scheme-fruit-salad",
"scheme-monochrome",
"scheme-neutral",
"scheme-rainbow",
}
func PreviewSchemes(sourceColor string, contrast float64) (map[string]SchemePreview, error) {
if sourceColor == "" {
return nil, fmt.Errorf("source color is required")
}
previews := make(map[string]SchemePreview, len(previewSchemeTypes))
for _, schemeType := range previewSchemeTypes {
output, err := runMatugenDryRun(&Options{
Kind: "hex",
Value: sourceColor,
MatugenType: schemeType,
Contrast: contrast,
})
if err != nil {
return nil, fmt.Errorf("preview %s: %w", schemeType, err)
}
dark := extractMatugenColor(output, "primary", "dark")
light := extractMatugenColor(output, "primary", "light")
if dark == "" || light == "" {
return nil, fmt.Errorf("preview %s: primary colors missing from matugen output", schemeType)
}
previews[schemeType] = SchemePreview{Dark: dark, Light: light}
}
return previews, nil
}
func (o *Options) ColorsOutput() string { func (o *Options) ColorsOutput() string {
return filepath.Join(o.StateDir, "dms-colors.json") return filepath.Join(o.StateDir, "dms-colors.json")
} }
@@ -699,7 +655,6 @@ func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
func detectMatugenVersionLocked() (matugenFlags, error) { func detectMatugenVersionLocked() (matugenFlags, error) {
cmd := exec.Command("matugen", "--version") cmd := exec.Command("matugen", "--version")
cmd.Env = utils.EnvWithUserBinPath(nil)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err) return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
@@ -756,7 +711,6 @@ func runMatugen(baseArgs []string) error {
args := buildMatugenArgs(baseArgs, flags) args := buildMatugenArgs(baseArgs, flags)
cmd := exec.Command("matugen", args...) cmd := exec.Command("matugen", args...)
cmd.Env = utils.EnvWithUserBinPath(nil)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
runErr := cmd.Run() runErr := cmd.Run()
@@ -774,7 +728,6 @@ func runMatugen(baseArgs []string) error {
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4) log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
args = buildMatugenArgs(baseArgs, newFlags) args = buildMatugenArgs(baseArgs, newFlags)
retryCmd := exec.Command("matugen", args...) retryCmd := exec.Command("matugen", args...)
retryCmd.Env = utils.EnvWithUserBinPath(nil)
retryCmd.Stdout = os.Stdout retryCmd.Stdout = os.Stdout
retryCmd.Stderr = os.Stderr retryCmd.Stderr = os.Stderr
return retryCmd.Run() return retryCmd.Run()
@@ -817,7 +770,6 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
} }
cmd := exec.Command("matugen", baseArgs...) cmd := exec.Command("matugen", baseArgs...)
cmd.Env = utils.EnvWithUserBinPath(nil)
var stderr strings.Builder var stderr strings.Builder
cmd.Stderr = &stderr cmd.Stderr = &stderr
output, err := cmd.Output() output, err := cmd.Output()
@@ -916,26 +868,7 @@ func refreshGTK(mode ColorMode) {
} }
} }
var colorSchemeEchoHook func(scheme string)
func SetColorSchemeEchoHook(hook func(scheme string)) {
colorSchemeEchoHook = hook
}
func expectColorSchemeEcho(scheme string) {
if colorSchemeEchoHook != nil {
colorSchemeEchoHook(scheme)
}
}
// The color-scheme round trip is the only mechanism that makes running GTK4
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
// following the portal color-scheme (Chromium) can drop the restore signal
// mid-repaint and latch the wrong mode, so this is opt-in.
func refreshGTK4() { func refreshGTK4() {
if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" {
return
}
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil { if err != nil {
return return
@@ -949,13 +882,11 @@ func refreshGTK4() {
toggle = "prefer-dark" toggle = "prefer-dark"
} }
expectColorSchemeEcho(toggle)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err) log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return return
} }
time.Sleep(400 * time.Millisecond) time.Sleep(50 * time.Millisecond)
expectColorSchemeEcho(current)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err) log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
} }
@@ -1056,9 +987,6 @@ func closestAdwaitaAccent(primaryHex string) string {
func syncAccentColor(primaryHex string) { func syncAccentColor(primaryHex string) {
accent := closestAdwaitaAccent(primaryHex) accent := closestAdwaitaAccent(primaryHex)
if cur, err := utils.GsettingsGet("org.gnome.desktop.interface", "accent-color"); err == nil && strings.Trim(cur, "'") == accent {
return
}
log.Infof("Setting GNOME accent color: %s", accent) log.Infof("Setting GNOME accent color: %s", accent)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
log.Warnf("Failed to set accent-color: %v", err) log.Warnf("Failed to set accent-color: %v", err)
+8 -23
View File
@@ -554,50 +554,35 @@ func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
return "", nil return "", nil
} }
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (hasUpdates bool, diffURL string, err error) { func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
pluginPath, err := m.findInstalledPath(pluginID) pluginPath, err := m.findInstalledPath(pluginID)
if err != nil { if err != nil {
return false, "", fmt.Errorf("failed to find plugin: %w", err) return false, fmt.Errorf("failed to find plugin: %w", err)
} }
if pluginPath == "" { if pluginPath == "" {
return false, "", fmt.Errorf("plugin not installed: %s", pluginID) return false, fmt.Errorf("plugin not installed: %s", pluginID)
} }
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return false, "", nil return false, nil
} }
metaPath := pluginPath + ".meta" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
return false, "", fmt.Errorf("failed to check metadata: %w", err) return false, fmt.Errorf("failed to check metadata: %w", err)
} }
var hasUp bool
var localHash, remoteHash string
if metaExists { if metaExists {
// Plugin is from a monorepo, check the repo directory // Plugin is from a monorepo, check the repo directory
reposDir := filepath.Join(m.pluginsDir, ".repos") reposDir := filepath.Join(m.pluginsDir, ".repos")
repoName := m.getRepoName(plugin.Repo) repoName := m.getRepoName(plugin.Repo)
repoPath := filepath.Join(reposDir, repoName) repoPath := filepath.Join(reposDir, repoName)
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(repoPath) return m.gitClient.HasUpdates(repoPath)
} else {
// Plugin is a standalone repo
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(pluginPath)
} }
if err != nil { // Plugin is a standalone repo
return false, "", err return m.gitClient.HasUpdates(pluginPath)
}
diffURL = plugin.Repo
if diffURL != "" {
diffURL = strings.TrimSuffix(diffURL, ".git")
if hasUp && len(localHash) >= 7 && len(remoteHash) >= 7 {
diffURL = fmt.Sprintf("%s/compare/%s...%s", diffURL, localHash[:7], remoteHash[:7])
}
}
return hasUp, diffURL, nil
} }
+9 -10
View File
@@ -33,7 +33,7 @@ type Plugin struct {
type GitClient interface { type GitClient interface {
PlainClone(path string, url string) error PlainClone(path string, url string) error
Pull(path string) error Pull(path string) error
HasUpdates(path string) (hasUpdates bool, localHash string, remoteHash string, err error) HasUpdates(path string) (bool, error)
} }
type realGitClient struct{} type realGitClient struct{}
@@ -65,10 +65,10 @@ func (g *realGitClient) Pull(path string) error {
return nil return nil
} }
func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) { func (g *realGitClient) HasUpdates(path string) (bool, error) {
repo, err := git.PlainOpen(path) repo, err := git.PlainOpen(path)
if err != nil { if err != nil {
return false, "", "", err return false, err
} }
// Fetch remote changes // Fetch remote changes
@@ -76,24 +76,24 @@ func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
if err != nil && err.Error() != "already up-to-date" { if err != nil && err.Error() != "already up-to-date" {
// If fetch fails, we can't determine if there are updates // If fetch fails, we can't determine if there are updates
// Return false and the error // Return false and the error
return false, "", "", err return false, err
} }
// Get the HEAD reference // Get the HEAD reference
head, err := repo.Head() head, err := repo.Head()
if err != nil { if err != nil {
return false, "", "", err return false, err
} }
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master) // Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
remote, err := repo.Remote("origin") remote, err := repo.Remote("origin")
if err != nil { if err != nil {
return false, "", "", err return false, err
} }
refs, err := remote.List(&git.ListOptions{}) refs, err := remote.List(&git.ListOptions{})
if err != nil { if err != nil {
return false, "", "", err return false, err
} }
// Find the default branch remote ref // Find the default branch remote ref
@@ -108,14 +108,13 @@ func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
} }
} }
localHash := head.Hash().String()
// If we couldn't find a remote HEAD, assume no updates // If we couldn't find a remote HEAD, assume no updates
if remoteHead == "" { if remoteHead == "" {
return false, localHash, "", nil return false, nil
} }
// Compare local HEAD with remote HEAD // Compare local HEAD with remote HEAD
return localHash != remoteHead, localHash, remoteHead, nil return head.Hash().String() != remoteHead, nil
} }
type Registry struct { type Registry struct {
+3 -3
View File
@@ -13,7 +13,7 @@ import (
type mockGitClient struct { type mockGitClient struct {
cloneFunc func(path string, url string) error cloneFunc func(path string, url string) error
pullFunc func(path string) error pullFunc func(path string) error
hasUpdatesFunc func(path string) (bool, string, string, error) hasUpdatesFunc func(path string) (bool, error)
} }
func (m *mockGitClient) PlainClone(path string, url string) error { func (m *mockGitClient) PlainClone(path string, url string) error {
@@ -30,11 +30,11 @@ func (m *mockGitClient) Pull(path string) error {
return nil return nil
} }
func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) { func (m *mockGitClient) HasUpdates(path string) (bool, error) {
if m.hasUpdatesFunc != nil { if m.hasUpdatesFunc != nil {
return m.hasUpdatesFunc(path) return m.hasUpdatesFunc(path)
} }
return false, "", "", nil return false, nil
} }
func TestNewRegistry(t *testing.T) { func TestNewRegistry(t *testing.T) {
@@ -1,289 +0,0 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/virtual-keyboard-unstable-v1.xml
//
// virtual_keyboard_unstable_v1 Protocol Copyright:
//
// Copyright © 2008-2011 Kristian Høgsberg
// Copyright © 2010-2013 Intel Corporation
// Copyright © 2012-2013 Collabora, Ltd.
// Copyright © 2018 Purism SPC
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package virtual_keyboard
import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
// ZwpVirtualKeyboardV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwpVirtualKeyboardV1InterfaceName = "zwp_virtual_keyboard_v1"
// ZwpVirtualKeyboardV1 : virtual keyboard
//
// The virtual keyboard provides an application with requests which emulate
// the behaviour of a physical keyboard.
//
// This interface can be used by clients on its own to provide raw input
// events, or it can accompany the input method protocol.
type ZwpVirtualKeyboardV1 struct {
client.BaseProxy
}
// NewZwpVirtualKeyboardV1 : virtual keyboard
//
// The virtual keyboard provides an application with requests which emulate
// the behaviour of a physical keyboard.
//
// This interface can be used by clients on its own to provide raw input
// events, or it can accompany the input method protocol.
func NewZwpVirtualKeyboardV1(ctx *client.Context) *ZwpVirtualKeyboardV1 {
zwpVirtualKeyboardV1 := &ZwpVirtualKeyboardV1{}
ctx.Register(zwpVirtualKeyboardV1)
return zwpVirtualKeyboardV1
}
// Keymap : keyboard mapping
//
// Provide a file descriptor to the compositor which can be
// memory-mapped to provide a keyboard mapping description.
//
// Format carries a value from the keymap_format enumeration.
//
// format: keymap format
// fd: keymap file descriptor
// size: keymap size, in bytes
func (i *ZwpVirtualKeyboardV1) Keymap(format uint32, fd int, size uint32) error {
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(format))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(size))
l += 4
oob := unix.UnixRights(int(fd))
err := i.Context().WriteMsg(_reqBuf[:], oob)
return err
}
// Key : key event
//
// A key was pressed or released.
// The time argument is a timestamp with millisecond granularity, with an
// undefined base. All requests regarding a single object must share the
// same clock.
//
// Keymap must be set before issuing this request.
//
// State carries a value from the key_state enumeration.
//
// time: timestamp with millisecond granularity
// key: key that produced the event
// state: physical state of the key
func (i *ZwpVirtualKeyboardV1) Key(time, key, state uint32) error {
const opcode = 1
const _reqBufLen = 8 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(time))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(key))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(state))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Modifiers : modifier and group state
//
// Notifies the compositor that the modifier and/or group state has
// changed, and it should update state.
//
// The client should use wl_keyboard.modifiers event to synchronize its
// internal state with seat state.
//
// Keymap must be set before issuing this request.
//
// modsDepressed: depressed modifiers
// modsLatched: latched modifiers
// modsLocked: locked modifiers
// group: keyboard layout
func (i *ZwpVirtualKeyboardV1) Modifiers(modsDepressed, modsLatched, modsLocked, group uint32) error {
const opcode = 2
const _reqBufLen = 8 + 4 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(modsDepressed))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(modsLatched))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(modsLocked))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(group))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Destroy : destroy the virtual keyboard keyboard object
func (i *ZwpVirtualKeyboardV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 3
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwpVirtualKeyboardV1Error uint32
// ZwpVirtualKeyboardV1Error :
const (
// ZwpVirtualKeyboardV1ErrorNoKeymap : No keymap was set
ZwpVirtualKeyboardV1ErrorNoKeymap ZwpVirtualKeyboardV1Error = 0
)
func (e ZwpVirtualKeyboardV1Error) Name() string {
switch e {
case ZwpVirtualKeyboardV1ErrorNoKeymap:
return "no_keymap"
default:
return ""
}
}
func (e ZwpVirtualKeyboardV1Error) Value() string {
switch e {
case ZwpVirtualKeyboardV1ErrorNoKeymap:
return "0"
default:
return ""
}
}
func (e ZwpVirtualKeyboardV1Error) String() string {
return e.Name() + "=" + e.Value()
}
// ZwpVirtualKeyboardManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwpVirtualKeyboardManagerV1InterfaceName = "zwp_virtual_keyboard_manager_v1"
// ZwpVirtualKeyboardManagerV1 : virtual keyboard manager
//
// A virtual keyboard manager allows an application to provide keyboard
// input events as if they came from a physical keyboard.
type ZwpVirtualKeyboardManagerV1 struct {
client.BaseProxy
}
// NewZwpVirtualKeyboardManagerV1 : virtual keyboard manager
//
// A virtual keyboard manager allows an application to provide keyboard
// input events as if they came from a physical keyboard.
func NewZwpVirtualKeyboardManagerV1(ctx *client.Context) *ZwpVirtualKeyboardManagerV1 {
zwpVirtualKeyboardManagerV1 := &ZwpVirtualKeyboardManagerV1{}
ctx.Register(zwpVirtualKeyboardManagerV1)
return zwpVirtualKeyboardManagerV1
}
// CreateVirtualKeyboard : Create a new virtual keyboard
//
// Creates a new virtual keyboard associated to a seat.
//
// If the compositor enables a keyboard to perform arbitrary actions, it
// should present an error when an untrusted client requests a new
// keyboard.
func (i *ZwpVirtualKeyboardManagerV1) CreateVirtualKeyboard(seat *client.Seat) (*ZwpVirtualKeyboardV1, error) {
id := NewZwpVirtualKeyboardV1(i.Context())
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], seat.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
func (i *ZwpVirtualKeyboardManagerV1) Destroy() error {
i.Context().Unregister(i)
return nil
}
type ZwpVirtualKeyboardManagerV1Error uint32
// ZwpVirtualKeyboardManagerV1Error :
const (
// ZwpVirtualKeyboardManagerV1ErrorUnauthorized : client not authorized to use the interface
ZwpVirtualKeyboardManagerV1ErrorUnauthorized ZwpVirtualKeyboardManagerV1Error = 0
)
func (e ZwpVirtualKeyboardManagerV1Error) Name() string {
switch e {
case ZwpVirtualKeyboardManagerV1ErrorUnauthorized:
return "unauthorized"
default:
return ""
}
}
func (e ZwpVirtualKeyboardManagerV1Error) Value() string {
switch e {
case ZwpVirtualKeyboardManagerV1ErrorUnauthorized:
return "0"
default:
return ""
}
}
func (e ZwpVirtualKeyboardManagerV1Error) String() string {
return e.Name() + "=" + e.Value()
}
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="virtual_keyboard_unstable_v1">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2013 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2018 Purism SPC
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<interface name="zwp_virtual_keyboard_v1" version="1">
<description summary="virtual keyboard">
The virtual keyboard provides an application with requests which emulate
the behaviour of a physical keyboard.
This interface can be used by clients on its own to provide raw input
events, or it can accompany the input method protocol.
</description>
<request name="keymap">
<description summary="keyboard mapping">
Provide a file descriptor to the compositor which can be
memory-mapped to provide a keyboard mapping description.
Format carries a value from the keymap_format enumeration.
</description>
<arg name="format" type="uint" summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</request>
<enum name="error">
<entry name="no_keymap" value="0" summary="No keymap was set"/>
</enum>
<request name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base. All requests regarding a single object must share the
same clock.
Keymap must be set before issuing this request.
State carries a value from the key_state enumeration.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" summary="physical state of the key"/>
</request>
<request name="modifiers">
<description summary="modifier and group state">
Notifies the compositor that the modifier and/or group state has
changed, and it should update state.
The client should use wl_keyboard.modifiers event to synchronize its
internal state with seat state.
Keymap must be set before issuing this request.
</description>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</request>
<request name="destroy" type="destructor" since="1">
<description summary="destroy the virtual keyboard keyboard object"/>
</request>
</interface>
<interface name="zwp_virtual_keyboard_manager_v1" version="1">
<description summary="virtual keyboard manager">
A virtual keyboard manager allows an application to provide keyboard
input events as if they came from a physical keyboard.
</description>
<enum name="error">
<entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
</enum>
<request name="create_virtual_keyboard">
<description summary="Create a new virtual keyboard">
Creates a new virtual keyboard associated to a seat.
If the compositor enables a keyboard to perform arbitrary actions, it
should present an error when an untrusted client requests a new
keyboard.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
</request>
</interface>
</protocol>
+1 -8
View File
@@ -68,12 +68,6 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
activeBuses[i] = true activeBuses[i] = true
id := fmt.Sprintf("ddc:i2c-%d", i) id := fmt.Sprintf("ddc:i2c-%d", i)
// Don't re-probe identified monitors: DDC traffic during a wake
// sequence can disturb some monitors' own brightness handling.
if _, ok := b.devices.Load(id); ok {
continue
}
dev, err := b.probeDDCDevice(i) dev, err := b.probeDDCDevice(i)
if err != nil || dev == nil { if err != nil || dev == nil {
continue continue
@@ -113,8 +107,7 @@ func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) {
dummy := make([]byte, 32) dummy := make([]byte, 32)
syscall.Read(fd, dummy) //nolint:errcheck syscall.Read(fd, dummy) //nolint:errcheck
writebuf := []byte{DDC_SOURCE_ADDR, 0x80} writebuf := []byte{0x00}
writebuf = append(writebuf, ddcciChecksum(writebuf))
n, err := syscall.Write(fd, writebuf) n, err := syscall.Write(fd, writebuf)
if err == nil && n == len(writebuf) { if err == nil && n == len(writebuf) {
name := b.getDDCName(bus) name := b.getDDCName(bus)
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net" "net"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
) )
@@ -29,10 +28,6 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
handleCopyEntry(conn, req, m) handleCopyEntry(conn, req, m)
case "clipboard.paste": case "clipboard.paste":
handlePaste(conn, req, m) handlePaste(conn, req, m)
case "clipboard.sendPaste":
handleSendPaste(conn, req)
case "clipboard.pasteSupported":
models.Respond(conn, req.ID, map[string]bool{"supported": m.pasteSupported})
case "clipboard.subscribe": case "clipboard.subscribe":
handleSubscribe(conn, req, m) handleSubscribe(conn, req, m)
case "clipboard.search": case "clipboard.search":
@@ -181,17 +176,6 @@ func handlePaste(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, map[string]string{"text": text}) models.Respond(conn, req.ID, map[string]string{"text": text})
} }
func handleSendPaste(conn net.Conn, req models.Request) {
shift, _ := models.Get[bool](req, "shift")
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) { func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("clipboard-%d", req.ID) clientID := fmt.Sprintf("clipboard-%d", req.ID)
+76 -31
View File
@@ -31,7 +31,6 @@ import (
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
@@ -165,8 +164,6 @@ func (m *Manager) setupRegistry() error {
m.seat = seat m.seat = seat
m.seatName = e.Name m.seatName = e.Name
log.Info("Bound wl_seat") log.Info("Bound wl_seat")
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
m.pasteSupported = true
} }
}) })
@@ -1057,13 +1054,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
dataCopy := make([]byte, len(data)) dataCopy := make([]byte, len(data))
copy(dataCopy, data) copy(dataCopy, data)
m.takeSelection(clipboardstore.ExpandOffers(dataCopy, mimeType))
return nil
}
// takeSelection makes the daemon the selection owner, serving the given
// offers until another client claims the clipboard.
func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
m.post(func() { m.post(func() {
if m.dataControlMgr == nil || m.dataDevice == nil { if m.dataControlMgr == nil || m.dataDevice == nil {
log.Error("Data control manager or device not initialized") log.Error("Data control manager or device not initialized")
@@ -1078,13 +1068,9 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
return return
} }
offerData := make(map[string][]byte, len(offers)) if err := source.Offer(mimeType); err != nil {
for _, offer := range offers { log.Errorf("Failed to offer mime type: %v", err)
if err := source.Offer(offer.MimeType); err != nil { return
log.Errorf("Failed to offer %s: %v", offer.MimeType, err)
return
}
offerData[offer.MimeType] = offer.Data
} }
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
@@ -1094,11 +1080,7 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
file := os.NewFile(uintptr(fd), "clipboard-pipe") file := os.NewFile(uintptr(fd), "clipboard-pipe")
defer file.Close() defer file.Close()
data, ok := offerData[e.MimeType] if _, err := file.Write(dataCopy); err != nil {
if !ok {
return
}
if _, err := file.Write(data); err != nil {
log.Errorf("Failed to write clipboard data: %v", err) log.Errorf("Failed to write clipboard data: %v", err)
} }
}) })
@@ -1111,6 +1093,9 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
m.releaseCurrentSource() m.releaseCurrentSource()
m.currentSource = source m.currentSource = source
m.sourceMutex.Lock()
m.sourceMimeTypes = []string{mimeType}
m.sourceMutex.Unlock()
m.ownerLock.Lock() m.ownerLock.Lock()
m.isOwner = true m.isOwner = true
@@ -1121,6 +1106,8 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
log.Errorf("Failed to set selection: %v", err) log.Errorf("Failed to set selection: %v", err)
} }
}) })
return nil
} }
func (m *Manager) CopyText(text string) error { func (m *Manager) CopyText(text string) error {
@@ -1792,16 +1779,74 @@ func (m *Manager) CopyFile(filePath string) error {
m.updateState() m.updateState()
m.notifySubscribers() m.notifySubscribers()
offers := []clipboardstore.Offer{ _, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData))
{MimeType: "x-special/gnome-copied-files", Data: []byte("copy\n" + fileURI)},
{MimeType: "text/uri-list", Data: []byte(fileURI + "\r\n")}, m.post(func() {
{MimeType: "text/plain", Data: []byte(filePath)}, if m.dataControlMgr == nil || m.dataDevice == nil {
} log.Error("Data control manager or device not initialized")
if _, imgMime, err := image.DecodeConfig(bytes.NewReader(fileData)); err == nil { return
offers = append(offers, clipboardstore.Offer{MimeType: "image/" + imgMime, Data: fileData}) }
}
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
source, err := dataMgr.CreateDataSource()
if err != nil {
log.Errorf("Failed to create data source: %v", err)
return
}
type offer struct {
mime string
data []byte
}
offers := []offer{
{"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
{"text/uri-list", []byte(fileURI + "\r\n")},
{"text/plain", []byte(filePath)},
}
if imgErr == nil {
imgMimeType := "image/" + imgMime
offers = append(offers, offer{imgMimeType, fileData})
}
offerData := make(map[string][]byte)
for _, o := range offers {
if err := source.Offer(o.mime); err != nil {
log.Errorf("Failed to offer %s: %v", o.mime, err)
return
}
offerData[o.mime] = o.data
}
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
fd := e.Fd
defer syscall.Close(fd)
file := os.NewFile(uintptr(fd), "clipboard-pipe")
defer file.Close()
if data, ok := offerData[e.MimeType]; ok {
file.Write(data)
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
m.ownerLock.Lock()
m.isOwner = false
m.ownerLock.Unlock()
})
m.releaseCurrentSource()
m.currentSource = source
m.ownerLock.Lock()
m.isOwner = true
m.ownerLock.Unlock()
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
if err := device.SetSelection(source); err != nil {
log.Errorf("Failed to set selection: %v", err)
}
})
m.takeSelection(offers)
return nil return nil
} }
@@ -531,6 +531,47 @@ func TestManager_ConcurrentOfferAccess(t *testing.T) {
wg.Wait() wg.Wait()
} }
func TestManager_ConcurrentPersistAccess(t *testing.T) {
m := &Manager{
persistData: make(map[string][]byte),
persistMimeTypes: []string{},
}
var wg sync.WaitGroup
const goroutines = 20
const iterations = 50
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.RLock()
_ = m.persistData
_ = m.persistMimeTypes
m.persistMutex.RUnlock()
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.Lock()
m.persistMimeTypes = []string{"text/plain", "text/html"}
m.persistData = map[string][]byte{
"text/plain": []byte("test"),
}
m.persistMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentOwnerAccess(t *testing.T) { func TestManager_ConcurrentOwnerAccess(t *testing.T) {
m := &Manager{} m := &Manager{}
+9 -3
View File
@@ -132,9 +132,15 @@ type Manager struct {
offerMutex sync.RWMutex offerMutex sync.RWMutex
offerRegistry map[uint32]any offerRegistry map[uint32]any
isOwner bool sourceMimeTypes []string
ownerLock sync.Mutex sourceMutex sync.RWMutex
pasteSupported bool
persistData map[string][]byte
persistMimeTypes []string
persistMutex sync.RWMutex
isOwner bool
ownerLock sync.Mutex
initialized bool initialized bool
+1 -39
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"sync" "sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
@@ -101,41 +100,6 @@ func (m *Manager) initializeSettings() error {
return nil return nil
} }
// ExpectColorSchemeEcho registers a self-write so the watcher swallows its SettingChanged echo instead of forwarding it as an external change.
func (m *Manager) ExpectColorSchemeEcho(scheme string) {
var value uint32
switch scheme {
case "prefer-dark":
value = 1
case "prefer-light":
value = 2
}
m.selfEchoMu.Lock()
m.selfEchoes = append(m.selfEchoes, colorSchemeEcho{value: value, expires: time.Now().Add(10 * time.Second)})
m.selfEchoMu.Unlock()
}
func (m *Manager) consumeSelfEcho(value uint32) bool {
m.selfEchoMu.Lock()
defer m.selfEchoMu.Unlock()
now := time.Now()
kept := m.selfEchoes[:0]
consumed := false
for _, echo := range m.selfEchoes {
if now.After(echo.expires) {
continue
}
if !consumed && echo.value == value {
consumed = true
continue
}
kept = append(kept, echo)
}
m.selfEchoes = kept
return consumed
}
func (m *Manager) watchSettingsChanges() { func (m *Manager) watchSettingsChanges() {
conn, err := dbus.ConnectSessionBus() conn, err := dbus.ConnectSessionBus()
if err != nil { if err != nil {
@@ -178,15 +142,13 @@ func (m *Manager) watchSettingsChanges() {
continue continue
} }
selfInflicted := m.consumeSelfEcho(colorScheme)
m.stateMutex.Lock() m.stateMutex.Lock()
changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available
m.state.Settings.ColorScheme = colorScheme m.state.Settings.ColorScheme = colorScheme
m.state.Settings.Available = true m.state.Settings.Available = true
m.stateMutex.Unlock() m.stateMutex.Unlock()
if changed && !selfInflicted { if changed {
m.NotifySubscribers() m.NotifySubscribers()
} }
} }
@@ -141,45 +141,3 @@ func TestManager_SettingsState_Modification(t *testing.T) {
original := manager.GetState() original := manager.GetState()
assert.Equal(t, uint32(0), original.Settings.ColorScheme) assert.Equal(t, uint32(0), original.Settings.ColorScheme)
} }
func TestManager_SelfEcho_ConsumesRegisteredWrites(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-dark")
manager.ExpectColorSchemeEcho("default")
assert.True(t, manager.consumeSelfEcho(1))
assert.True(t, manager.consumeSelfEcho(0))
assert.False(t, manager.consumeSelfEcho(1))
assert.False(t, manager.consumeSelfEcho(0))
}
func TestManager_SelfEcho_ExternalChangePassesThrough(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-dark")
assert.False(t, manager.consumeSelfEcho(2))
assert.True(t, manager.consumeSelfEcho(1))
}
func TestManager_SelfEcho_ConsumesOnePerRegistration(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-dark")
manager.ExpectColorSchemeEcho("prefer-dark")
assert.True(t, manager.consumeSelfEcho(1))
assert.True(t, manager.consumeSelfEcho(1))
assert.False(t, manager.consumeSelfEcho(1))
}
func TestManager_SelfEcho_SchemeMapping(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-light")
assert.True(t, manager.consumeSelfEcho(2))
manager.ExpectColorSchemeEcho("default")
assert.True(t, manager.consumeSelfEcho(0))
}
@@ -2,17 +2,11 @@ package freedesktop
import ( import (
"sync" "sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
type colorSchemeEcho struct {
value uint32
expires time.Time
}
type AccountsState struct { type AccountsState struct {
Available bool `json:"available"` Available bool `json:"available"`
UserPath string `json:"userPath"` UserPath string `json:"userPath"`
@@ -69,6 +63,4 @@ type Manager struct {
screensaverCookieCounter uint32 screensaverCookieCounter uint32
screensaverFreedesktopClaimed bool screensaverFreedesktopClaimed bool
screensaverGnomeClaimed bool screensaverGnomeClaimed bool
selfEchoMu sync.Mutex
selfEchoes []colorSchemeEcho
} }
-13
View File
@@ -46,19 +46,6 @@ func (m *Manager) Activate() error {
return nil return nil
} }
func (m *Manager) SetLockedHint(locked bool) error {
err := m.sessionObj.Call(dbusSessionInterface+".SetLockedHint", 0, locked).Err
if err != nil {
if refreshErr := m.refreshSessionBinding(); refreshErr == nil {
err = m.sessionObj.Call(dbusSessionInterface+".SetLockedHint", 0, locked).Err
}
if err != nil {
return fmt.Errorf("failed to set locked hint: %w", err)
}
}
return nil
}
func (m *Manager) SetIdleHint(idle bool) error { func (m *Manager) SetIdleHint(idle bool) error {
err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err
if err != nil { if err != nil {
-16
View File
@@ -21,8 +21,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleActivate(conn, req, manager) handleActivate(conn, req, manager)
case "loginctl.setIdleHint": case "loginctl.setIdleHint":
handleSetIdleHint(conn, req, manager) handleSetIdleHint(conn, req, manager)
case "loginctl.setLockedHint":
handleSetLockedHint(conn, req, manager)
case "loginctl.setLockBeforeSuspend": case "loginctl.setLockBeforeSuspend":
handleSetLockBeforeSuspend(conn, req, manager) handleSetLockBeforeSuspend(conn, req, manager)
case "loginctl.setSleepInhibitorEnabled": case "loginctl.setSleepInhibitorEnabled":
@@ -80,20 +78,6 @@ func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
} }
func handleSetLockedHint(conn net.Conn, req models.Request, manager *Manager) {
locked, err := params.Bool(req.Params, "locked")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.SetLockedHint(locked); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked hint set"})
}
func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) { func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled") enabled, err := params.Bool(req.Params, "enabled")
if err != nil { if err != nil {
@@ -545,18 +545,12 @@ func (a *SecretAgent) GetSecrets(
out[settingName] = sec out[settingName] = sec
} }
if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) { if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) {
secrets := make(map[string]string) pw := reply.Secrets["password"]
for k, v := range reply.Secrets {
if k != "username" {
secrets[k] = v
}
}
a.backend.pendingVPNSaveMu.Lock() a.backend.pendingVPNSaveMu.Lock()
a.backend.pendingVPNSave = &pendingVPNCredentials{ a.backend.pendingVPNSave = &pendingVPNCredentials{
ConnectionPath: string(path), ConnectionPath: string(path),
Username: vpnUsername, Username: vpnUsername,
Password: reply.Secrets["password"], Password: pw,
Secrets: secrets,
SavePassword: reply.Save, SavePassword: reply.Save,
} }
a.backend.pendingVPNSaveMu.Unlock() a.backend.pendingVPNSaveMu.Unlock()
@@ -796,23 +790,7 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
fields = []string{"username", "password"} fields = []string{"username", "password"}
} }
case strings.Contains(vpnService, "openvpn"): case strings.Contains(vpnService, "openvpn"):
switch connType { if connType == "password" || connType == "password-tls" {
case "tls":
// Cert auth wants the private-key passphrase, not a password.
if secretFlagsNotRequired(dataMap["cert-pass-flags"]) {
return nil
}
return []string{"cert-pass"}
case "password-tls":
fields = []string{}
if dataMap["username"] == "" {
fields = append(fields, "username")
}
fields = append(fields, "password")
if !secretFlagsNotRequired(dataMap["cert-pass-flags"]) {
fields = append(fields, "cert-pass")
}
case "password":
if dataMap["username"] == "" { if dataMap["username"] == "" {
fields = []string{"username", "password"} fields = []string{"username", "password"}
} }
@@ -827,19 +805,6 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
return fields return fields
} }
// NetworkManager secret flags: bit 1 (value 2) marks a secret as not-saved,
// bit 2 (value 4) as not-required. A not-required secret must not be prompted.
func secretFlagsNotRequired(flags string) bool {
if flags == "" {
return false
}
n, err := strconv.Atoi(flags)
if err != nil {
return false
}
return n&0x4 != 0
}
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool { func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
if method, ok := data["saml-auth-method"]; ok { if method, ok := data["saml-auth-method"]; ok {
if method == "REDIRECT" || method == "POST" { if method == "REDIRECT" || method == "POST" {
@@ -315,44 +315,6 @@ func TestInferVPNFields_GPSaml(t *testing.T) {
expectedLen: 1, expectedLen: 1,
shouldHave: []string{"password"}, shouldHave: []string{"password"},
}, },
{
name: "OpenVPN cert auth (tls) - private-key passphrase",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "tls",
},
expectedLen: 1,
shouldHave: []string{"cert-pass"},
},
{
name: "OpenVPN cert auth (tls) with passphrase-less key",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "tls",
"cert-pass-flags": "4",
},
expectedLen: 0,
},
{
name: "OpenVPN password-tls no username",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password-tls",
},
expectedLen: 3,
shouldHave: []string{"username", "password", "cert-pass"},
},
{
name: "OpenVPN password-tls with username, passphrase-less key",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password-tls",
"username": "john",
"cert-pass-flags": "4",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
} }
for _, tt := range tests { for _, tt := range tests {
+1 -25
View File
@@ -114,14 +114,6 @@ func (b *IWDBackend) discoverDevices() error {
return fmt.Errorf("failed to get managed objects: %w", err) return fmt.Errorf("failed to get managed objects: %w", err)
} }
return b.applyManagedObjects(objects)
}
func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant) error {
b.stationPath = ""
b.devicePath = ""
b.adapterPath = ""
for path, interfaces := range objects { for path, interfaces := range objects {
if _, hasStation := interfaces[iwdStationInterface]; hasStation { if _, hasStation := interfaces[iwdStationInterface]; hasStation {
b.stationPath = path b.stationPath = path
@@ -137,13 +129,6 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]
b.stateMutex.Unlock() b.stateMutex.Unlock()
} }
} }
if poweredVar, ok := devProps["Powered"]; ok {
if powered, ok := poweredVar.Value().(bool); ok {
b.stateMutex.Lock()
b.state.WiFiEnabled = powered
b.stateMutex.Unlock()
}
}
} }
} }
if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter { if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter {
@@ -151,18 +136,9 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]
} }
} }
if b.devicePath == "" { if b.stationPath == "" || b.devicePath == "" {
return fmt.Errorf("no WiFi device found") return fmt.Errorf("no WiFi device found")
} }
if b.stationPath == "" {
b.stateMutex.Lock()
b.state.WiFiEnabled = false
b.state.WiFiConnected = false
b.state.NetworkStatus = StatusDisconnected
b.state.WiFiNetworks = nil
b.stateMutex.Unlock()
log.Infof("iwd device %s has no station interface; treating WiFi as disabled", b.devicePath)
}
return nil return nil
} }
@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -36,7 +35,12 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
} }
if b.stationPath != "" { if b.stationPath != "" {
if err := b.addStationSignalMatch(b.stationPath); err != nil { err := b.conn.AddMatchSignal(
dbus.WithMatchObjectPath(b.stationPath),
dbus.WithMatchInterface(dbusPropertiesInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
if err != nil {
return fmt.Errorf("failed to add station signal match: %w", err) return fmt.Errorf("failed to add station signal match: %w", err)
} }
} }
@@ -77,48 +81,6 @@ func (b *IWDBackend) refreshWiFiNetworkState() bool {
return b.updateSavedWiFiNetworks() == nil return b.updateSavedWiFiNetworks() == nil
} }
func (b *IWDBackend) addStationSignalMatch(path dbus.ObjectPath) error {
return b.conn.AddMatchSignal(
dbus.WithMatchObjectPath(path),
dbus.WithMatchInterface(dbusPropertiesInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
}
func (b *IWDBackend) handleStationAdded(path dbus.ObjectPath) bool {
if path == "" || path == b.stationPath {
return false
}
b.stationPath = path
if err := b.addStationSignalMatch(path); err != nil {
log.Warnf("Failed to add iwd station signal match for %s: %v", path, err)
}
if err := b.updateState(); err != nil {
log.Warnf("Failed to update iwd state after station appeared: %v", err)
}
return b.refreshWiFiNetworkState()
}
func (b *IWDBackend) handleStationRemoved(path dbus.ObjectPath) bool {
if path == "" || path != b.stationPath {
return false
}
b.stationPath = ""
b.stateMutex.Lock()
b.state.WiFiEnabled = false
b.state.WiFiConnected = false
b.state.WiFiSSID = ""
b.state.WiFiSignal = 0
b.state.NetworkStatus = StatusDisconnected
b.state.WiFiNetworks = nil
b.stateMutex.Unlock()
return true
}
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
defer b.sigWG.Done() defer b.sigWG.Done()
@@ -136,13 +98,7 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
if sig.Name == dbusObjectManager+".InterfacesAdded" { if sig.Name == dbusObjectManager+".InterfacesAdded" {
if len(sig.Body) >= 2 { if len(sig.Body) >= 2 {
path, _ := sig.Body[0].(dbus.ObjectPath)
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok { if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
if _, ok := interfaces[iwdStationInterface]; ok {
if b.handleStationAdded(path) && b.onStateChange != nil {
b.onStateChange()
}
}
if _, ok := interfaces[iwdKnownNetworkInterface]; ok { if _, ok := interfaces[iwdKnownNetworkInterface]; ok {
if b.refreshWiFiNetworkState() && b.onStateChange != nil { if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange() b.onStateChange()
@@ -155,15 +111,8 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
if sig.Name == dbusObjectManager+".InterfacesRemoved" { if sig.Name == dbusObjectManager+".InterfacesRemoved" {
if len(sig.Body) >= 2 { if len(sig.Body) >= 2 {
path, _ := sig.Body[0].(dbus.ObjectPath)
if interfaces, ok := sig.Body[1].([]string); ok { if interfaces, ok := sig.Body[1].([]string); ok {
for _, iface := range interfaces { for _, iface := range interfaces {
if iface == iwdStationInterface {
if b.handleStationRemoved(path) && b.onStateChange != nil {
b.onStateChange()
}
break
}
if iface == iwdKnownNetworkInterface { if iface == iwdKnownNetworkInterface {
if b.refreshWiFiNetworkState() && b.onStateChange != nil { if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange() b.onStateChange()
@@ -169,41 +169,6 @@ func TestIWDBackend_MapIwdDBusError(t *testing.T) {
} }
} }
func TestIWDBackend_ApplyManagedObjects_DeviceWithoutStation(t *testing.T) {
backend, _ := NewIWDBackend()
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/0/1": {
iwdDeviceInterface: {
"Name": dbus.MakeVariant("wlan0"),
"Powered": dbus.MakeVariant(false),
},
},
})
assert.NoError(t, err)
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/1"), backend.devicePath)
assert.Empty(t, backend.stationPath)
state, err := backend.GetCurrentState()
assert.NoError(t, err)
assert.Equal(t, "wlan0", state.WiFiDevice)
assert.False(t, state.WiFiEnabled)
assert.False(t, state.WiFiConnected)
}
func TestIWDBackend_ApplyManagedObjects_NoDevice(t *testing.T) {
backend, _ := NewIWDBackend()
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/0": {
iwdAdapterInterface: {},
},
})
assert.ErrorContains(t, err, "no WiFi device found")
}
func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) { func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) {
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{ objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/known_network/1": { "/net/connman/iwd/known_network/1": {
@@ -90,10 +90,7 @@ type pendingVPNCredentials struct {
ConnectionPath string ConnectionPath string
Username string Username string
Password string Password string
// Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass"); SavePassword bool
// falls back to Password under the "password" key when empty.
Secrets map[string]string
SavePassword bool
} }
type cachedVPNCredentials struct { type cachedVPNCredentials struct {
@@ -863,17 +863,13 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials)
log.Infof("[saveVPNCredentials] Saving username") log.Infof("[saveVPNCredentials] Saving username")
} }
// Save secrets if requested // Save password if requested
if creds.SavePassword { if creds.SavePassword {
secs := creds.Secrets data["password-flags"] = "0"
if len(secs) == 0 { secs := make(map[string]string)
secs = map[string]string{"password": creds.Password} secs["password"] = creds.Password
}
for field := range secs {
data[field+"-flags"] = "0"
}
vpn["secrets"] = dbus.MakeVariant(secs) vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs)) log.Infof("[saveVPNCredentials] Saving password with password-flags=0")
} }
vpn["data"] = dbus.MakeVariant(data) vpn["data"] = dbus.MakeVariant(data)
@@ -42,15 +42,12 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
for _, id := range installedNames { for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok { if plugin, ok := pluginMap[id]; ok {
hasUpdate := false hasUpdate := false
diffURL := plugin.Repo if hasUpdates, err := manager.HasUpdates(id, plugin); err == nil {
if hasUpdates, dURL, err := manager.HasUpdates(id, plugin); err == nil {
hasUpdate = hasUpdates hasUpdate = hasUpdates
diffURL = dURL
} }
info := pluginInfoFromPlugin(plugin) info := pluginInfoFromPlugin(plugin)
info.HasUpdate = hasUpdate info.HasUpdate = hasUpdate
info.DiffURL = diffURL
result = append(result, info) result = append(result, info)
} else { } else {
result = append(result, PluginInfo{ result = append(result, PluginInfo{
-1
View File
@@ -18,7 +18,6 @@ type PluginInfo struct {
Note string `json:"note,omitempty"` Note string `json:"note,omitempty"`
HasUpdate bool `json:"hasUpdate,omitempty"` HasUpdate bool `json:"hasUpdate,omitempty"`
RequiresDMS string `json:"requires_dms,omitempty"` RequiresDMS string `json:"requires_dms,omitempty"`
DiffURL string `json:"diffUrl,omitempty"`
Upvotes int `json:"upvotes,omitempty"` Upvotes int `json:"upvotes,omitempty"`
Status []string `json:"status,omitempty"` Status []string `json:"status,omitempty"`
IssueURL string `json:"issueUrl,omitempty"` IssueURL string `json:"issueUrl,omitempty"`
-10
View File
@@ -23,7 +23,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes" serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wallpaper"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
) )
@@ -57,15 +56,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "wallpaper.") {
if wallpaperManager == nil {
models.RespondError(conn, req.ID, "wallpaper manager not initialized")
return
}
wallpaper.HandleRequest(conn, req, wallpaperManager)
return
}
if strings.HasPrefix(req.Method, "loginctl.") { if strings.HasPrefix(req.Method, "loginctl.") {
if loginctlManager == nil { if loginctlManager == nil {
models.RespondError(conn, req.ID, "loginctl manager not initialized") models.RespondError(conn, req.ID, "loginctl manager not initialized")
+1 -68
View File
@@ -17,7 +17,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
@@ -34,14 +33,13 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wallpaper"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 27 const APIVersion = 26
var CLIVersion = "dev" var CLIVersion = "dev"
@@ -75,7 +73,6 @@ var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var wallpaperManager *wallpaper.Manager
var trayRecoveryManager *trayrecovery.Manager var trayRecoveryManager *trayrecovery.Manager
var locationManager *location.Manager var locationManager *location.Manager
var sysUpdateManager *sysupdate.Manager var sysUpdateManager *sysupdate.Manager
@@ -191,7 +188,6 @@ func InitializeFreedeskManager() error {
} }
freedesktopManager = manager freedesktopManager = manager
matugen.SetColorSchemeEchoHook(manager.ExpectColorSchemeEcho)
log.Info("Freedesktop manager initialized") log.Info("Freedesktop manager initialized")
return nil return nil
@@ -351,13 +347,6 @@ func InitializeThemeModeManager() error {
return nil return nil
} }
func InitializeWallpaperManager() error {
wallpaperManager = wallpaper.NewManager()
log.Info("Wallpaper rotation scheduler initialized")
return nil
}
func InitializeTrayRecoveryManager() error { func InitializeTrayRecoveryManager() error {
manager, err := trayrecovery.NewManager() manager, err := trayrecovery.NewManager()
if err != nil { if err != nil {
@@ -474,10 +463,6 @@ func getCapabilities() Capabilities {
caps = append(caps, "theme.auto") caps = append(caps, "theme.auto")
} }
if wallpaperManager != nil {
caps = append(caps, "wallpaper")
}
if dbusManager != nil { if dbusManager != nil {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
@@ -544,10 +529,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "theme.auto") caps = append(caps, "theme.auto")
} }
if wallpaperManager != nil {
caps = append(caps, "wallpaper")
}
if locationManager != nil { if locationManager != nil {
caps = append(caps, "location") caps = append(caps, "location")
} }
@@ -860,38 +841,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("wallpaper") && wallpaperManager != nil {
wg.Add(1)
wallpaperChan := wallpaperManager.Subscribe(clientID + "-wallpaper")
go func() {
defer wg.Done()
defer wallpaperManager.Unsubscribe(clientID + "-wallpaper")
initialState := wallpaperManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "wallpaper", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-wallpaperChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "wallpaper", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("bluetooth") && bluezManager != nil { if shouldSubscribe("bluetooth") && bluezManager != nil {
wg.Add(1) wg.Add(1)
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth") bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
@@ -1337,9 +1286,6 @@ func cleanupManagers() {
if themeModeManager != nil { if themeModeManager != nil {
themeModeManager.Close() themeModeManager.Close()
} }
if wallpaperManager != nil {
wallpaperManager.Close()
}
if trayRecoveryManager != nil { if trayRecoveryManager != nil {
trayRecoveryManager.Close() trayRecoveryManager.Close()
} }
@@ -1632,19 +1578,6 @@ func Start(printDocs bool) error {
}() }()
} }
if err := InitializeWallpaperManager(); err != nil {
log.Warnf("Wallpaper scheduler unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
wallpaperManager.WatchLoginctl(loginctlManager)
}()
}
go func() { go func() {
<-loginctlReady <-loginctlReady
if loginctlManager == nil { if loginctlManager == nil {
@@ -1,84 +0,0 @@
package wallpaper
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "wallpaper manager not initialized")
return
}
switch req.Method {
case "wallpaper.getState":
handleGetState(conn, req, manager)
case "wallpaper.setConfig":
handleSetConfig(conn, req, manager)
case "wallpaper.trigger":
handleTrigger(conn, req, manager)
case "wallpaper.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetConfig(conn net.Conn, req models.Request, manager *Manager) {
raw, ok := params.Any(req.Params, "config")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'config' parameter")
return
}
data, err := json.Marshal(raw)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetConfig(config)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "wallpaper schedule set"})
}
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
manager.ResetSchedule(params.StringOpt(req.Params, "target", ""))
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "wallpaper schedule reset"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range stateChan {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
Result: &state,
}); err != nil {
return
}
}
}
-374
View File
@@ -1,374 +0,0 @@
package wallpaper
import (
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type activeSchedule struct {
cfg ScheduleConfig
nextFire time.Time
}
// Grace period before a missed-while-off schedule fires, so a freshly
// connected frontend has its cycle-seq baseline before the bump arrives.
var catchUpDelay = 3 * time.Second
type Manager struct {
config Config
configMutex sync.RWMutex
state *State
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
stopChan chan struct{}
updateTrigger chan struct{}
resetReq chan string
wg sync.WaitGroup
// Owned by schedulerLoop after start; keyed like schedules ("" = global).
lastFires map[string]time.Time
persistFires func(map[string]time.Time)
}
func NewManager() *Manager {
return newManager(loadLastFires(), saveLastFires)
}
func newManager(lastFires map[string]time.Time, persistFires func(map[string]time.Time)) *Manager {
if lastFires == nil {
lastFires = map[string]time.Time{}
}
m := &Manager{
config: Config{
Global: ScheduleConfig{Mode: "interval", IntervalSec: 300, Time: "06:00"},
Monitors: map[string]ScheduleConfig{},
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
resetReq: make(chan string, 8),
lastFires: lastFires,
persistFires: persistFires,
}
m.state = &State{Config: m.getConfig()}
m.wg.Add(1)
go m.schedulerLoop()
return m
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{Config: m.getConfig()}
}
return *m.state
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) SetConfig(config Config) {
if config.Monitors == nil {
config.Monitors = map[string]ScheduleConfig{}
}
m.configMutex.Lock()
if reflect.DeepEqual(m.config, config) {
m.configMutex.Unlock()
return
}
m.config = config
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) ResetSchedule(target string) {
select {
case m.resetReq <- target:
default:
}
}
func (m *Manager) TriggerUpdate() {
select {
case m.updateTrigger <- struct{}{}:
default:
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.wg.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("wallpaper")
m.wg.Go(func() {
defer lm.Unsubscribe("wallpaper")
for {
select {
case <-m.stopChan:
return
case state, ok := <-ch:
if !ok {
return
}
if state.PreparingForSleep {
continue
}
m.TriggerUpdate()
}
}
})
}
func (m *Manager) schedulerLoop() {
defer m.wg.Done()
schedules := map[string]*activeSchedule{}
resets := map[string]bool{}
var seq uint64
var timer *time.Timer
for {
now := time.Now()
config := m.getConfig()
active := activeSchedules(config)
for key := range schedules {
if _, ok := active[key]; !ok {
delete(schedules, key)
}
}
firesDirty := false
for key, cfg := range active {
s, ok := schedules[key]
switch {
case !ok:
s = &activeSchedule{cfg: cfg, nextFire: computeNext(now, cfg)}
schedules[key] = s
if cfg.Mode == "time" {
last := m.lastFires[key]
prev, valid := prevDailyTime(now, cfg.Time)
switch {
case last.IsZero():
// First enable (or no history): seed instead of firing.
m.lastFires[key] = now
firesDirty = true
case valid && last.Before(prev):
// Scheduled time passed while dms wasn't running.
s.nextFire = now.Add(catchUpDelay)
}
}
case s.cfg != cfg || resets[key]:
s.cfg = cfg
s.nextFire = computeNext(now, cfg)
}
delete(resets, key)
}
var dueKeys []string
for key, s := range schedules {
if !s.nextFire.After(now) {
dueKeys = append(dueKeys, key)
s.nextFire = computeNext(now, s.cfg)
if s.cfg.Mode == "time" {
m.lastFires[key] = now
firesDirty = true
}
}
}
if firesDirty {
for key := range m.lastFires {
if _, ok := schedules[key]; !ok {
delete(m.lastFires, key)
}
}
m.persistFires(m.lastFires)
}
next, hasNext := soonest(schedules)
if len(dueKeys) == 0 {
m.setState(config, next, seq, "")
}
for _, key := range dueKeys {
seq++
m.setState(config, next, seq, key)
}
waitDur := 24 * time.Hour
if hasNext {
waitDur = max(time.Until(next), time.Second)
}
if timer != nil {
timer.Stop()
}
timer = time.NewTimer(waitDur)
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.updateTrigger:
timer.Stop()
case key := <-m.resetReq:
timer.Stop()
resets[key] = true
case <-timer.C:
}
}
}
func (m *Manager) setState(config Config, next time.Time, seq uint64, target string) {
newState := State{Config: config, NextRotation: next, CycleSeq: seq, Target: target}
m.stateMutex.Lock()
if m.state != nil && statesEqual(m.state, &newState) {
m.stateMutex.Unlock()
return
}
m.state = &newState
m.stateMutex.Unlock()
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
state := m.GetState()
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- state:
default:
}
return true
})
}
func (m *Manager) getConfig() Config {
m.configMutex.RLock()
defer m.configMutex.RUnlock()
return m.config
}
func activeSchedules(config Config) map[string]ScheduleConfig {
out := map[string]ScheduleConfig{}
if config.PerMonitor {
for name, cfg := range config.Monitors {
if cfg.Enabled {
out[name] = cfg
}
}
return out
}
if config.Global.Enabled {
out[""] = config.Global
}
return out
}
func computeNext(now time.Time, cfg ScheduleConfig) time.Time {
switch cfg.Mode {
case "time":
return nextDailyTime(now, cfg.Time)
default:
sec := max(cfg.IntervalSec, 1)
return now.Add(time.Duration(sec) * time.Second)
}
}
func nextDailyTime(now time.Time, hhmm string) time.Time {
hour, minute, ok := parseHHMM(hhmm)
if !ok {
return now.Add(24 * time.Hour)
}
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
if !next.After(now) {
next = next.Add(24 * time.Hour)
}
return next
}
func prevDailyTime(now time.Time, hhmm string) (time.Time, bool) {
hour, minute, ok := parseHHMM(hhmm)
if !ok {
return time.Time{}, false
}
prev := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
if prev.After(now) {
prev = prev.Add(-24 * time.Hour)
}
return prev, true
}
func parseHHMM(hhmm string) (int, int, bool) {
parts := strings.Split(hhmm, ":")
if len(parts) != 2 {
return 0, 0, false
}
hour, err := strconv.Atoi(parts[0])
if err != nil || hour < 0 || hour > 23 {
return 0, 0, false
}
minute, err := strconv.Atoi(parts[1])
if err != nil || minute < 0 || minute > 59 {
return 0, 0, false
}
return hour, minute, true
}
func soonest(schedules map[string]*activeSchedule) (time.Time, bool) {
var best time.Time
found := false
for _, s := range schedules {
if !found || s.nextFire.Before(best) {
best = s.nextFire
found = true
}
}
return best, found
}
func statesEqual(a, b *State) bool {
switch {
case a == nil || b == nil:
return a == b
case a.CycleSeq != b.CycleSeq:
return false
case a.Target != b.Target:
return false
case !a.NextRotation.Equal(b.NextRotation):
return false
}
return reflect.DeepEqual(a.Config, b.Config)
}
@@ -1,179 +0,0 @@
package wallpaper
import (
"testing"
"time"
)
func TestParseHHMM(t *testing.T) {
cases := []struct {
in string
hour int
minute int
ok bool
}{
{"06:00", 6, 0, true},
{"23:59", 23, 59, true},
{"00:00", 0, 0, true},
{"24:00", 0, 0, false},
{"6:5", 6, 5, true},
{"bad", 0, 0, false},
{"12", 0, 0, false},
{"12:60", 0, 0, false},
}
for _, c := range cases {
hour, minute, ok := parseHHMM(c.in)
if ok != c.ok || (ok && (hour != c.hour || minute != c.minute)) {
t.Errorf("parseHHMM(%q) = (%d, %d, %v), want (%d, %d, %v)", c.in, hour, minute, ok, c.hour, c.minute, c.ok)
}
}
}
func TestComputeNextInterval(t *testing.T) {
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
got := computeNext(now, ScheduleConfig{Mode: "interval", IntervalSec: 300})
if want := now.Add(300 * time.Second); !got.Equal(want) {
t.Errorf("interval next = %v, want %v", got, want)
}
clamped := computeNext(now, ScheduleConfig{Mode: "interval", IntervalSec: 0})
if want := now.Add(time.Second); !clamped.Equal(want) {
t.Errorf("interval clamp = %v, want %v", clamped, want)
}
}
func TestComputeNextTime(t *testing.T) {
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
later := computeNext(now, ScheduleConfig{Mode: "time", Time: "18:00"})
if want := time.Date(2026, 6, 30, 18, 0, 0, 0, time.UTC); !later.Equal(want) {
t.Errorf("time next (today) = %v, want %v", later, want)
}
tomorrow := computeNext(now, ScheduleConfig{Mode: "time", Time: "06:00"})
if want := time.Date(2026, 7, 1, 6, 0, 0, 0, time.UTC); !tomorrow.Equal(want) {
t.Errorf("time next (tomorrow) = %v, want %v", tomorrow, want)
}
}
func TestActiveSchedules(t *testing.T) {
global := activeSchedules(Config{
Global: ScheduleConfig{Enabled: true, Mode: "interval", IntervalSec: 60},
})
if len(global) != 1 {
t.Fatalf("global active = %d, want 1", len(global))
}
if _, ok := global[""]; !ok {
t.Errorf("global active missing global key")
}
perMonitor := activeSchedules(Config{
PerMonitor: true,
Global: ScheduleConfig{Enabled: true},
Monitors: map[string]ScheduleConfig{
"DP-1": {Enabled: true, Mode: "interval", IntervalSec: 60},
"DP-2": {Enabled: false},
},
})
if len(perMonitor) != 1 {
t.Fatalf("per-monitor active = %d, want 1", len(perMonitor))
}
if _, ok := perMonitor["DP-1"]; !ok {
t.Errorf("per-monitor active missing DP-1")
}
}
func TestPrevDailyTime(t *testing.T) {
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
earlier, ok := prevDailyTime(now, "06:00")
if !ok || !earlier.Equal(time.Date(2026, 6, 30, 6, 0, 0, 0, time.UTC)) {
t.Errorf("prev (today) = %v, %v", earlier, ok)
}
later, ok := prevDailyTime(now, "18:00")
if !ok || !later.Equal(time.Date(2026, 6, 29, 18, 0, 0, 0, time.UTC)) {
t.Errorf("prev (yesterday) = %v, %v", later, ok)
}
if _, ok := prevDailyTime(now, "bad"); ok {
t.Error("prev (invalid) should not be ok")
}
}
func TestTimeScheduleCatchUpAfterRestart(t *testing.T) {
origDelay := catchUpDelay
catchUpDelay = 100 * time.Millisecond
defer func() { catchUpDelay = origDelay }()
now := time.Now()
hhmm := now.Add(-2 * time.Hour).Format("15:04")
seed := map[string]time.Time{"": now.Add(-26 * time.Hour)}
m := newManager(seed, func(map[string]time.Time) {})
defer m.Close()
sub := m.Subscribe("test")
defer m.Unsubscribe("test")
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "time", Time: hhmm}})
deadline := time.After(3 * time.Second)
for {
select {
case state := <-sub:
if state.CycleSeq > 0 && state.Target == "" {
return
}
case <-deadline:
t.Fatal("missed time schedule did not catch up within 3s")
}
}
}
func TestTimeScheduleNoFireOnFirstEnable(t *testing.T) {
now := time.Now()
hhmm := now.Add(-2 * time.Hour).Format("15:04")
m := newManager(map[string]time.Time{}, func(map[string]time.Time) {})
defer m.Close()
sub := m.Subscribe("test")
defer m.Unsubscribe("test")
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "time", Time: hhmm}})
deadline := time.After(1500 * time.Millisecond)
for {
select {
case state := <-sub:
if state.CycleSeq > 0 {
t.Fatal("first enable must not fire immediately")
}
case <-deadline:
return
}
}
}
func TestSchedulerEmitsCycle(t *testing.T) {
m := newManager(map[string]time.Time{}, func(map[string]time.Time) {})
defer m.Close()
sub := m.Subscribe("test")
defer m.Unsubscribe("test")
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "interval", IntervalSec: 1}})
deadline := time.After(3 * time.Second)
for {
select {
case state := <-sub:
if state.CycleSeq > 0 && state.Target == "" {
return
}
case <-deadline:
t.Fatal("scheduler did not emit a cycle event within 3s")
}
}
}
-45
View File
@@ -1,45 +0,0 @@
package wallpaper
import (
"encoding/json"
"os"
"path/filepath"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type persistentState struct {
LastFires map[string]time.Time `json:"lastFires"`
}
func stateFilePath() string {
return filepath.Join(utils.XDGCacheHome(), "dms", "wallpaper-schedule.json")
}
func loadLastFires() map[string]time.Time {
data, err := os.ReadFile(stateFilePath())
if err != nil {
return map[string]time.Time{}
}
var state persistentState
if err := json.Unmarshal(data, &state); err != nil || state.LastFires == nil {
return map[string]time.Time{}
}
return state.LastFires
}
func saveLastFires(fires map[string]time.Time) {
path := stateFilePath()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return
}
data, err := json.Marshal(persistentState{LastFires: fires})
if err != nil {
return
}
if err := os.WriteFile(path, data, 0o644); err != nil {
log.Warnf("wallpaper: failed to persist schedule state: %v", err)
}
}
-23
View File
@@ -1,23 +0,0 @@
package wallpaper
import "time"
type ScheduleConfig struct {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
IntervalSec int `json:"intervalSec"`
Time string `json:"time"`
}
type Config struct {
PerMonitor bool `json:"perMonitor"`
Global ScheduleConfig `json:"global"`
Monitors map[string]ScheduleConfig `json:"monitors"`
}
type State struct {
Config Config `json:"config"`
NextRotation time.Time `json:"nextRotation"`
CycleSeq uint64 `json:"cycleSeq"`
Target string `json:"target"`
}
+6 -22
View File
@@ -74,9 +74,6 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
if config.Enabled { if config.Enabled {
m.post(func() { m.post(func() {
if m.controlsInitialized {
return
}
log.Info("Gamma control enabled at startup") log.Info("Gamma control enabled at startup")
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
m.availOutputsMu.RLock() m.availOutputsMu.RLock()
@@ -187,26 +184,13 @@ func (m *Manager) setupRegistry() error {
enabled := m.config.Enabled enabled := m.config.Enabled
m.configMutex.RUnlock() m.configMutex.RUnlock()
if !enabled { if enabled && m.controlsInitialized {
return m.post(func() {
if err := m.addOutputControl(output); err != nil {
log.Warnf("Failed to add output control: %v", err)
}
})
} }
m.post(func() {
if err := m.addOutputControl(output); err != nil {
log.Warnf("gamma: failed to add output control: %v", err)
return
}
if m.controlsInitialized {
return
}
// All outputs had been torn down (monitor sleep/disconnect),
// clearing controlsInitialized. Mark it ready again so the
// gamma_size event that follows control creation drives the
// reapply, instead of waiting for a manual toggle. No explicit
// apply here: applyGamma's dedup would suppress a no-change
// write anyway, and the new control isn't ready until gamma_size.
log.Info("gamma: output returned, re-establishing controls")
m.controlsInitialized = true
})
} }
}) })
+4 -10
View File
@@ -108,7 +108,10 @@ func (sc *SharedContext) eventDispatcher() {
if r := recover(); r != nil { if r := recover(); r != nil {
err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r) err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r)
log.Error(err) log.Error(err)
sc.signalFatal(err) select {
case sc.fatalError <- err:
default:
}
} }
}() }()
@@ -138,7 +141,6 @@ func (sc *SharedContext) eventDispatcher() {
continue continue
case err != nil: case err != nil:
log.Errorf("Poll error: %v", err) log.Errorf("Poll error: %v", err)
sc.signalFatal(fmt.Errorf("wayland poll error: %w", err))
return return
} }
@@ -159,7 +161,6 @@ func (sc *SharedContext) eventDispatcher() {
if consecutiveErrors >= maxConsecutiveErrors { if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors) log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
sc.signalFatal(fmt.Errorf("wayland connection unrecoverable after %d dispatch failures: %w", maxConsecutiveErrors, err))
return return
} }
@@ -171,13 +172,6 @@ func (sc *SharedContext) eventDispatcher() {
} }
} }
func (sc *SharedContext) signalFatal(err error) {
select {
case sc.fatalError <- err:
default:
}
}
func (sc *SharedContext) drainCmdQueue() { func (sc *SharedContext) drainCmdQueue() {
for { for {
select { select {
+1 -63
View File
@@ -9,7 +9,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -130,7 +129,7 @@ func (m Model) deployConfigurations() tea.Cmd {
deployer := config.NewConfigDeployer(m.logChan) deployer := config.NewConfigDeployer(m.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig()) results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
return configDeploymentResult{ return configDeploymentResult{
results: results, results: results,
@@ -139,29 +138,6 @@ func (m Model) deployConfigurations() tea.Cmd {
} }
} }
func (m Model) optionalDepSelected(name string) bool {
if m.disabledItems[name] {
return false
}
for _, dep := range m.dependencies {
if dep.Name == name {
return true
}
}
return false
}
func (m Model) useSystemdConfig() bool {
if m.osInfo == nil {
return true
}
distroConfig, exists := distros.Registry[m.osInfo.Distribution.ID]
if !exists {
return true
}
return distroConfig.Family != distros.FamilyVoid
}
func (m Model) viewConfigConfirmation() string { func (m Model) viewConfigConfirmation() string {
var b strings.Builder var b strings.Builder
@@ -219,50 +195,12 @@ func (m Model) viewConfigConfirmation() string {
b.WriteString(backup) b.WriteString(backup)
b.WriteString("\n\n") b.WriteString("\n\n")
if note := m.configReplacementNote(); note != "" {
b.WriteString(m.styles.Subtle.Render(note))
b.WriteString("\n\n")
}
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue") help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
b.WriteString(help) b.WriteString(help)
return b.String() return b.String()
} }
func (m Model) configReplacementNote() string {
if m.selectedConfig < 0 || m.selectedConfig >= len(m.existingConfigs) {
return ""
}
configInfo := m.existingConfigs[m.selectedConfig]
if !configInfo.Exists {
return ""
}
switch configInfo.ConfigType {
case "Niri":
if m.useSystemdConfig() {
return "Replacing Niri writes the DMS Niri template and uses the user systemd dms service for shell autostart."
}
return `Replacing Niri writes the DMS Niri template and starts DMS from Niri with spawn-at-startup "dms" "run".`
case "Hyprland":
if m.useSystemdConfig() {
return "Replacing Hyprland writes the DMS Lua template and uses the user systemd dms service for shell autostart."
}
return `Replacing Hyprland writes the DMS Lua template and starts DMS from Hyprland with hl.exec_cmd("dms run").`
case "Mango":
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec-once=dms run."
case "Ghostty":
return "Replacing Ghostty writes the DMS terminal defaults and theme include."
case "Kitty":
return "Replacing Kitty writes the DMS terminal defaults, theme include, and tab styling."
case "Alacritty":
return "Replacing Alacritty writes the DMS terminal defaults and theme import."
default:
return ""
}
}
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
if result, ok := msg.(configCheckResult); ok { if result, ok := msg.(configCheckResult); ok {
if result.error != nil { if result.error != nil {
+15 -58
View File
@@ -28,21 +28,6 @@ func (m Model) viewDetectingDeps() string {
return b.String() return b.String()
} }
func partitionOptionalLast(dependencies []deps.Dependency) []deps.Dependency {
ordered := make([]deps.Dependency, 0, len(dependencies))
for _, dep := range dependencies {
if dep.Required {
ordered = append(ordered, dep)
}
}
for _, dep := range dependencies {
if !dep.Required {
ordered = append(ordered, dep)
}
}
return ordered
}
func (m Model) viewDependencyReview() string { func (m Model) viewDependencyReview() string {
var b strings.Builder var b strings.Builder
@@ -54,15 +39,7 @@ func (m Model) viewDependencyReview() string {
b.WriteString("\n\n") b.WriteString("\n\n")
if len(m.dependencies) > 0 { if len(m.dependencies) > 0 {
optionalHeaderShown := false
for i, dep := range m.dependencies { for i, dep := range m.dependencies {
if !dep.Required && !optionalHeaderShown {
b.WriteString("\n")
b.WriteString(m.styles.Subtle.Render("Optional (space to enable)"))
b.WriteString("\n")
optionalHeaderShown = true
}
var status string var status string
var reinstallMarker string var reinstallMarker string
var variantMarker string var variantMarker string
@@ -105,13 +82,8 @@ func (m Model) viewDependencyReview() string {
} }
note := "" note := ""
switch dep.Name { if dep.Name == "dms-greeter" {
case "dms-greeter":
note = m.styles.Subtle.Render(" (selection replaces your current display manager)") note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
case "danksearch":
note = m.styles.Subtle.Render(" (file search; enables dsearch.service)")
case "dankcalendar":
note = m.styles.Subtle.Render(" (autostart managed in dankcalendar settings)")
} }
var line string var line string
@@ -148,13 +120,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = depsMsg.err m.err = depsMsg.err
m.state = StateError m.state = StateError
} else { } else {
m.dependencies = partitionOptionalLast(depsMsg.deps) m.dependencies = depsMsg.deps
// Optional components are opt-in, skipped by default // dms-greeter is opt-in skipped by default
for _, dep := range m.dependencies { for _, dep := range depsMsg.deps {
if dep.Required { if dep.Name == "dms-greeter" {
continue m.disabledItems["dms-greeter"] = true
break
} }
m.disabledItems[dep.Name] = true
} }
m.state = StateDependencyReview m.state = StateDependencyReview
} }
@@ -259,7 +231,14 @@ func (m Model) installPackages() tea.Cmd {
for msg := range installerProgressChan { for msg := range installerProgressChan {
// Run optional greeter setup // Run optional greeter setup
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil { if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
if m.optionalDepSelected("dms-greeter") { greeterSelected := false
for _, dep := range m.dependencies {
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
greeterSelected = true
break
}
}
if greeterSelected {
compositorName := "niri" compositorName := "niri"
switch m.selectedWindowManager() { switch m.selectedWindowManager() {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
@@ -286,28 +265,6 @@ func (m Model) installPackages() tea.Cmd {
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err), logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
} }
} }
if m.useSystemdConfig() && m.optionalDepSelected("danksearch") {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.97,
step: "Enabling danksearch service...",
logOutput: "Setting up dsearch.service...",
}
dsearchLogFunc := func(line string) {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.97,
step: "Enabling danksearch service...",
logOutput: line,
}
}
if err := distros.SetupDsearchService(context.Background(), dsearchLogFunc); err != nil {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.98,
step: "danksearch service warning",
logOutput: fmt.Sprintf("danksearch service setup warning (non-fatal): %v", err),
}
}
}
} }
} }
tuiMsg := packageInstallProgressMsg{ tuiMsg := packageInstallProgressMsg{
+10 -33
View File
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
return "dms-shell-git" return "dms-shell-git"
} }
return "dms-shell" return "dms-shell"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid: case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit { if isGit {
return "dms-git" return "dms-git"
} }
@@ -168,8 +168,6 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
return "sudo apt remove " + pkg return "sudo apt remove " + pkg
case distros.FamilySUSE: case distros.FamilySUSE:
return "sudo zypper remove " + pkg return "sudo zypper remove " + pkg
case distros.FamilyVoid:
return "sudo xbps-remove -R " + pkg
default: default:
return "" return ""
} }
@@ -203,26 +201,15 @@ func (m Model) viewInstallComplete() string {
wm := m.selectedWindowManager() wm := m.selectedWindowManager()
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\"" loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
if !m.useSystemdConfig() { switch wm {
switch wm { case deps.WindowManagerNiri:
case deps.WindowManagerNiri: loginHint = "If you do not have a greeter, login with \"niri-session\""
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri" case deps.WindowManagerHyprland:
case deps.WindowManagerHyprland: loginHint = "If you do not have a greeter, login with \"Hyprland\""
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland" case deps.WindowManagerMango:
case deps.WindowManagerMango: loginHint = "If you do not have a greeter, login with \"mango\""
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session mango"
}
} else {
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, login with \"niri-session\""
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, login with \"Hyprland\""
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, login with \"mango\""
}
} }
b.WriteString("\n") b.WriteString("\n")
@@ -235,17 +222,7 @@ func (m Model) viewInstallComplete() string {
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle)) labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n") b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
if !m.useSystemdConfig() { if wm == deps.WindowManagerMango {
switch wm {
case deps.WindowManagerNiri:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`) + "\n")
case deps.WindowManagerHyprland:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`) + "\n")
case deps.WindowManagerMango:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
}
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("quickshell --path ~/.config/quickshell/dms log") + "\n")
} else if wm == deps.WindowManagerMango {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n") b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n") b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
} else { } else {
+1 -72
View File
@@ -1,10 +1,7 @@
package utils package utils
import ( import (
"os"
"os/exec" "os/exec"
"path/filepath"
"strings"
) )
type AppChecker interface { type AppChecker interface {
@@ -34,20 +31,7 @@ func (DefaultAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
func CommandExists(cmd string) bool { func CommandExists(cmd string) bool {
_, err := exec.LookPath(cmd) _, err := exec.LookPath(cmd)
if err == nil { return err == nil
return true
}
if strings.ContainsRune(cmd, os.PathSeparator) {
return false
}
for _, dir := range userBinDirs() {
path := filepath.Join(dir, cmd)
info, statErr := os.Stat(path)
if statErr == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
return true
}
}
return false
} }
func AnyCommandExists(cmds ...string) bool { func AnyCommandExists(cmds ...string) bool {
@@ -58,58 +42,3 @@ func AnyCommandExists(cmds ...string) bool {
} }
return false return false
} }
func EnvWithUserBinPath(env []string) []string {
if env == nil {
env = os.Environ()
}
out := append([]string(nil), env...)
pathIndex := -1
pathValue := ""
for i, entry := range out {
if strings.HasPrefix(entry, "PATH=") {
pathIndex = i
pathValue = strings.TrimPrefix(entry, "PATH=")
break
}
}
parts := filepath.SplitList(pathValue)
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
if part != "" {
seen[part] = struct{}{}
}
}
prepend := make([]string, 0, 2)
for _, dir := range userBinDirs() {
if dir == "" {
continue
}
if _, ok := seen[dir]; ok {
continue
}
prepend = append(prepend, dir)
seen[dir] = struct{}{}
}
parts = append(prepend, parts...)
newPath := "PATH=" + strings.Join(parts, string(os.PathListSeparator))
if pathIndex >= 0 {
out[pathIndex] = newPath
} else {
out = append(out, newPath)
}
return out
}
func userBinDirs() []string {
dirs := []string{}
if home, err := os.UserHomeDir(); err == nil && home != "" {
dirs = append(dirs, filepath.Join(home, ".local", "bin"))
}
dirs = append(dirs, "/usr/local/bin")
return dirs
}
-55
View File
@@ -1,55 +0,0 @@
package utils
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommandExistsFallsBackToLocalBin(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".local", "bin")
require.NoError(t, os.MkdirAll(binDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(binDir, "pywalfox"), []byte("#!/bin/sh\n"), 0o755))
t.Setenv("HOME", home)
t.Setenv("PATH", t.TempDir())
assert.True(t, CommandExists("pywalfox"))
}
func TestCommandExistsIgnoresNonExecutableLocalBinFile(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".local", "bin")
require.NoError(t, os.MkdirAll(binDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(binDir, "pywalfox"), []byte("not executable"), 0o644))
t.Setenv("HOME", home)
t.Setenv("PATH", t.TempDir())
assert.False(t, CommandExists("pywalfox"))
}
func TestEnvWithUserBinPathPrependsLocalBin(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
env := EnvWithUserBinPath([]string{"PATH=/usr/bin", "OTHER=value"})
var pathValue string
for _, entry := range env {
if strings.HasPrefix(entry, "PATH=") {
pathValue = strings.TrimPrefix(entry, "PATH=")
break
}
}
parts := filepath.SplitList(pathValue)
require.NotEmpty(t, parts)
assert.Equal(t, filepath.Join(home, ".local", "bin"), parts[0])
assert.Contains(t, parts, "/usr/local/bin")
assert.Contains(t, env, "OTHER=value")
}
-1
View File
@@ -74,7 +74,6 @@ override_dh_auto_install:
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \ install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \ install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \ install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \
install -Dm644 assets/danklogo.svg debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \ install -Dm644 assets/danklogo.svg debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
else \ else \
echo "ERROR: quickshell directory not found!" && \ echo "ERROR: quickshell directory not found!" && \
-1
View File
@@ -67,7 +67,6 @@ override_dh_auto_install:
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/danklogo.svg debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/danklogo.svg debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
else \ else \
echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \ echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \
-2
View File
@@ -122,7 +122,6 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
# Install shell files to shared data location # Install shell files to shared data location
@@ -147,7 +146,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop %{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli %files -n dms-cli
-2
View File
@@ -90,7 +90,6 @@ install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_us
install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
install -Dm644 %{_builddir}/dms-qml/assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg install -Dm644 %{_builddir}/dms-qml/assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms install -dm755 %{buildroot}%{_datadir}/quickshell/dms
@@ -114,7 +113,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop %{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli %files -n dms-cli
+2 -1
View File
@@ -18,7 +18,8 @@ in
] ]
++ lib.optional cfg.enableDynamicTheming pkgs.matugen ++ lib.optional cfg.enableDynamicTheming pkgs.matugen
++ lib.optional cfg.enableAudioWavelength pkgs.cava ++ lib.optional cfg.enableAudioWavelength pkgs.cava
++ lib.optional cfg.enableCalendarEvents pkgs.khal; ++ lib.optional cfg.enableCalendarEvents pkgs.khal
++ lib.optional cfg.enableClipboardPaste pkgs.wtype;
plugins = lib.mapAttrs (name: plugin: { plugins = lib.mapAttrs (name: plugin: {
source = plugin.src; source = plugin.src;
+1 -49
View File
@@ -10,8 +10,6 @@ let
inherit (lib) types; inherit (lib) types;
cfg = config.programs.dank-material-shell.greeter; cfg = config.programs.dank-material-shell.greeter;
cfgDms = config.programs.dank-material-shell; cfgDms = config.programs.dank-material-shell;
cfgAutoLogin = config.services.displayManager.autoLogin;
sessionData = config.services.displayManager.sessionData;
inherit (config.services.greetd.settings.default_session) user; inherit (config.services.greetd.settings.default_session) user;
@@ -28,7 +26,6 @@ let
cfg.quickshell.package cfg.quickshell.package
compositorPackage compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
pkgs.jq # reads the user's cursor theme from settings.json in dms-greeter
] ]
} }
${ ${
@@ -51,38 +48,6 @@ let
} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"} } ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
''; '';
autoLoginCommand =
pkgs.runCommand "dms-greeter-autologin-command"
{
nativeBuildInputs = [
pkgs.gnugrep
pkgs.coreutils
];
}
''
set -euo pipefail
session="${sessionData.autologinSession}"
desktops="${sessionData.desktops}"
for sessionFile in \
"$desktops/share/wayland-sessions/$session.desktop" \
"$desktops/share/xsessions/$session.desktop"
do
if [ -f "$sessionFile" ]; then
command="$(grep -m1 '^Exec=' "$sessionFile" | cut -d= -f2- || true)"
if [ -n "$command" ]; then
printf '%s\n' "$command" > "$out"
exit 0
fi
fi
done
echo "dms-greeter autologin: could not resolve Exec for session '$session'" >&2
exit 1
'';
jq = lib.getExe pkgs.jq; jq = lib.getExe pkgs.jq;
in in
{ {
@@ -190,13 +155,6 @@ in
dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it. dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it.
''; '';
} }
{
assertion = cfgAutoLogin.enable -> sessionData.autologinSession != null;
message = ''
dms-greeter auto-login requires services.displayManager.defaultSession to be set,
or at least one session in services.displayManager.sessionPackages.
'';
}
]; ];
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS. # DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
# Declare security.pam.services.dankshell only if you want to override that runtime fallback. # Declare security.pam.services.dankshell only if you want to override that runtime fallback.
@@ -207,13 +165,7 @@ in
# }; # };
services.greetd = { services.greetd = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
settings = { settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
default_session.command = lib.mkDefault (lib.getExe greeterScript);
initial_session = lib.mkIf (cfgAutoLogin.enable && (cfgAutoLogin.user != null)) {
inherit (cfgAutoLogin) user;
command = ''${lib.getExe pkgs.bash} -lc "${pkgs.systemd}/bin/systemd-cat $(<${autoLoginCommand})"'';
};
};
}; };
fonts.packages = with pkgs; [ fonts.packages = with pkgs; [
fira-code fira-code

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