mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-03 12:08:31 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e791337ae9 | |||
| a8c15fcde9 | |||
| 601d4104a3 | |||
| 7979fb2b0e | |||
| 06fa21118e |
@@ -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 }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
if foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
buf, err := io.ReadAll(data)
|
buf, err := io.ReadAll(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read source: %w", err)
|
return fmt.Errorf("read source: %w", err)
|
||||||
}
|
}
|
||||||
return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
|
||||||
if foreground {
|
|
||||||
return serveOffers(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
return copyMultiFork(offers, pasteOnce)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
@@ -173,11 +132,12 @@ 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)
|
||||||
}
|
|
||||||
|
|
||||||
|
default:
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
@@ -205,38 +165,6 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, offer := range offers {
|
|
||||||
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
|
||||||
if _, err := stdin.Write(offer.Data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write offer data: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
@@ -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()
|
||||||
|
|
||||||
if data, ok := offerData[e.MimeType]; ok {
|
|
||||||
_, _ = file.Write(data)
|
_, _ = file.Write(data)
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -329,7 +298,7 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("set selection: %w", err)
|
return fmt.Errorf("set selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
signalReady()
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -341,26 +310,70 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err := s.ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CopyText(text string) error {
|
||||||
|
return Copy([]byte(text), "text/plain;charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
func Paste() ([]byte, string, error) {
|
func Paste() ([]byte, string, error) {
|
||||||
s, err := connectSession()
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer display.Destroy()
|
||||||
|
|
||||||
dataControlMgr, err := s.requireDataControl()
|
ctx := display.Context()
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
defer registry.Destroy()
|
||||||
|
|
||||||
|
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||||
|
var seat *wlclient.Seat
|
||||||
|
var bindErr error
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case "ext_data_control_manager_v1":
|
||||||
|
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
case "wl_seat":
|
||||||
|
if seat != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seat = wlclient.NewSeat(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(s.seat)
|
if dataControlMgr == nil {
|
||||||
|
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
}
|
||||||
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
|
if seat == nil {
|
||||||
|
return nil, "", fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := dataControlMgr.GetDataDevice(seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
return nil, "", fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -386,14 +399,15 @@ func Paste() ([]byte, string, error) {
|
|||||||
gotSelection = true
|
gotSelection = true
|
||||||
})
|
})
|
||||||
|
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
if !gotSelection || selectionOffer == nil {
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
return nil, "", fmt.Errorf("no clipboard data")
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
|
mimeTypes := offerMimeTypes[selectionOffer]
|
||||||
|
selectedMime := selectPreferredMimeType(mimeTypes)
|
||||||
if selectedMime == "" {
|
if selectedMime == "" {
|
||||||
return nil, "", fmt.Errorf("no supported mime type")
|
return nil, "", fmt.Errorf("no supported mime type")
|
||||||
}
|
}
|
||||||
@@ -410,7 +424,7 @@ func Paste() ([]byte, string, error) {
|
|||||||
}
|
}
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
||||||
s.display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
data, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -456,3 +470,161 @@ func selectPreferredMimeType(mimes []string) string {
|
|||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Offer struct {
|
||||||
|
MimeType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||||
|
if !foreground {
|
||||||
|
return copyMultiFork(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyMultiServe(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||||
|
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||||
|
if pasteOnce {
|
||||||
|
args = append(args, "--paste-once")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offer := range offers {
|
||||||
|
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||||
|
if _, err := stdin.Write(offer.Data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write offer data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer display.Destroy()
|
||||||
|
|
||||||
|
ctx := display.Context()
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
defer registry.Destroy()
|
||||||
|
|
||||||
|
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||||
|
var seat *wlclient.Seat
|
||||||
|
var bindErr error
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case "ext_data_control_manager_v1":
|
||||||
|
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
case "wl_seat":
|
||||||
|
if seat != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seat = wlclient.NewSeat(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
|
}
|
||||||
|
if dataControlMgr == nil {
|
||||||
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
}
|
||||||
|
defer dataControlMgr.Destroy()
|
||||||
|
if seat == nil {
|
||||||
|
return fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := dataControlMgr.GetDataDevice(seat)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get data device: %w", err)
|
||||||
|
}
|
||||||
|
defer device.Destroy()
|
||||||
|
|
||||||
|
source, err := dataControlMgr.CreateDataSource()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create data source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offerMap := make(map[string][]byte)
|
||||||
|
for _, offer := range offers {
|
||||||
|
if err := source.Offer(offer.MimeType); err != nil {
|
||||||
|
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||||
|
}
|
||||||
|
offerMap[offer.MimeType] = offer.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled := make(chan struct{})
|
||||||
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
|
_, _ = file.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pasted <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||||
|
close(cancelled)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := device.SetSelection(source); err != nil {
|
||||||
|
return fmt.Errorf("set selection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil
|
||||||
|
case <-pasted:
|
||||||
|
if pasteOnce {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
xkbKeymapFormatV1 = 1
|
|
||||||
|
|
||||||
keyStateReleased = 0
|
|
||||||
keyStatePressed = 1
|
|
||||||
|
|
||||||
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
|
|
||||||
shiftModMask = 1 << 0
|
|
||||||
ctrlModMask = 1 << 2
|
|
||||||
|
|
||||||
// evdev fallbacks for a standard pc105 map
|
|
||||||
fallbackCtrlKey = 29 // KEY_LEFTCTRL
|
|
||||||
fallbackShiftKey = 42 // KEY_LEFTSHIFT
|
|
||||||
fallbackVKey = 47 // KEY_V
|
|
||||||
)
|
|
||||||
|
|
||||||
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
|
|
||||||
// using the seat's own keymap, so keycodes stay valid for XWayland clients
|
|
||||||
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
|
|
||||||
// selects ctrl+shift+v for terminal targets.
|
|
||||||
func SendPasteKeystroke(withShift bool) error {
|
|
||||||
s, err := connectSession()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
if s.virtualKeyboardMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support zwp_virtual_keyboard_manager_v1")
|
|
||||||
}
|
|
||||||
if s.seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboard, err := s.seat.GetKeyboard()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get keyboard: %w", err)
|
|
||||||
}
|
|
||||||
defer keyboard.Release()
|
|
||||||
|
|
||||||
var keymap *wlclient.KeyboardKeymapEvent
|
|
||||||
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
|
||||||
if keymap == nil {
|
|
||||||
keymap = &e
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
s.display.Roundtrip()
|
|
||||||
|
|
||||||
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
|
|
||||||
return fmt.Errorf("no xkb keymap from seat")
|
|
||||||
}
|
|
||||||
defer unix.Close(keymap.Fd)
|
|
||||||
|
|
||||||
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read keymap: %w", err)
|
|
||||||
}
|
|
||||||
keys := resolveKeycodes(keymapText)
|
|
||||||
|
|
||||||
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create virtual keyboard: %w", err)
|
|
||||||
}
|
|
||||||
defer vk.Destroy()
|
|
||||||
|
|
||||||
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
|
|
||||||
return fmt.Errorf("set keymap: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := uint32(ctrlModMask)
|
|
||||||
held := []uint32{keys.ctrl}
|
|
||||||
if withShift {
|
|
||||||
mods |= shiftModMask
|
|
||||||
held = append(held, keys.shift)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := uint32(0)
|
|
||||||
press := func(key, state uint32) error {
|
|
||||||
t++
|
|
||||||
return vk.Key(t, key, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range held {
|
|
||||||
if err := press(key, keyStatePressed); err != nil {
|
|
||||||
return fmt.Errorf("key press: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
|
|
||||||
return fmt.Errorf("set modifiers: %w", err)
|
|
||||||
}
|
|
||||||
if err := press(keys.v, keyStatePressed); err != nil {
|
|
||||||
return fmt.Errorf("key press: %w", err)
|
|
||||||
}
|
|
||||||
if err := press(keys.v, keyStateReleased); err != nil {
|
|
||||||
return fmt.Errorf("key release: %w", err)
|
|
||||||
}
|
|
||||||
for i := len(held) - 1; i >= 0; i-- {
|
|
||||||
if err := press(held[i], keyStateReleased); err != nil {
|
|
||||||
return fmt.Errorf("key release: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
|
|
||||||
return fmt.Errorf("clear modifiers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.display.Roundtrip()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readKeymap(fd int, size uint32) (string, error) {
|
|
||||||
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
text := strings.TrimRight(string(data), "\x00")
|
|
||||||
return text, unix.Munmap(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type pasteKeycodes struct {
|
|
||||||
ctrl uint32
|
|
||||||
shift uint32
|
|
||||||
v uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
|
|
||||||
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
|
|
||||||
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
|
|
||||||
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// xkbcommon may serialize keysyms as hex escapes instead of names
|
|
||||||
// (e.g. "0x76" for v, "0xffe3" for Control_L).
|
|
||||||
var keysymNames = map[uint32]string{
|
|
||||||
0x76: "v",
|
|
||||||
0xffe3: "Control_L",
|
|
||||||
0xffe1: "Shift_L",
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalKeysym(sym string) string {
|
|
||||||
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
|
|
||||||
return sym
|
|
||||||
}
|
|
||||||
value, err := strconv.ParseUint(sym[2:], 16, 32)
|
|
||||||
if err != nil {
|
|
||||||
return sym
|
|
||||||
}
|
|
||||||
if name, ok := keysymNames[uint32(value)]; ok {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return sym
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
|
|
||||||
// the seat keymap's first group, falling back to pc105 positions.
|
|
||||||
func resolveKeycodes(keymap string) pasteKeycodes {
|
|
||||||
codes := map[string]uint32{}
|
|
||||||
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
|
|
||||||
if code, err := strconv.Atoi(m[2]); err == nil {
|
|
||||||
codes[m[1]] = uint32(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
|
|
||||||
want := map[string]*uint32{
|
|
||||||
"Control_L": &keys.ctrl,
|
|
||||||
"Shift_L": &keys.shift,
|
|
||||||
"v": &keys.v,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
|
|
||||||
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
|
|
||||||
if group == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
xkbCode, ok := codes[m[1]]
|
|
||||||
if !ok || xkbCode < 8 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
|
|
||||||
target, wanted := want[level1]
|
|
||||||
if !wanted {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
*target = xkbCode - 8
|
|
||||||
delete(want, level1)
|
|
||||||
if len(want) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLiveSeatKeymapResolution(t *testing.T) {
|
|
||||||
if os.Getenv("DMS_LIVE_TEST") == "" {
|
|
||||||
t.Skip("set DMS_LIVE_TEST=1 to run against the live compositor")
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := connectSession()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("connect: %v", err)
|
|
||||||
}
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
if s.virtualKeyboardMgr == nil {
|
|
||||||
t.Fatal("compositor does not advertise zwp_virtual_keyboard_manager_v1")
|
|
||||||
}
|
|
||||||
if s.seat == nil {
|
|
||||||
t.Fatal("no seat")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboard, err := s.seat.GetKeyboard()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("get keyboard: %v", err)
|
|
||||||
}
|
|
||||||
defer keyboard.Release()
|
|
||||||
|
|
||||||
var keymap *wlclient.KeyboardKeymapEvent
|
|
||||||
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
|
||||||
if keymap == nil {
|
|
||||||
keymap = &e
|
|
||||||
}
|
|
||||||
})
|
|
||||||
s.display.Roundtrip()
|
|
||||||
|
|
||||||
if keymap == nil {
|
|
||||||
t.Fatal("no keymap event")
|
|
||||||
}
|
|
||||||
defer unix.Close(keymap.Fd)
|
|
||||||
|
|
||||||
text, err := readKeymap(keymap.Fd, keymap.Size)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read keymap: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dump := os.Getenv("DMS_LIVE_DUMP"); dump != "" {
|
|
||||||
if err := os.WriteFile(dump, []byte(text), 0o644); err != nil {
|
|
||||||
t.Fatalf("dump keymap: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := resolveKeycodes(text)
|
|
||||||
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
|
|
||||||
|
|
||||||
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
|
|
||||||
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
const qwertyKeymap = `xkb_keymap {
|
|
||||||
xkb_keycodes "(unnamed)" {
|
|
||||||
minimum = 8;
|
|
||||||
maximum = 708;
|
|
||||||
<ESC> = 9;
|
|
||||||
<AB04> = 55;
|
|
||||||
<LCTL> = 37;
|
|
||||||
<LFSH> = 50;
|
|
||||||
alias <AL01> = <AC01>;
|
|
||||||
indicator 1 = "Caps Lock";
|
|
||||||
};
|
|
||||||
xkb_types "(unnamed)" {
|
|
||||||
type "ALPHABETIC" {
|
|
||||||
modifiers = Shift+Lock;
|
|
||||||
map[Shift] = Level2;
|
|
||||||
level_name[Level1] = "Base";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
xkb_symbols "(unnamed)" {
|
|
||||||
key <ESC> { [ Escape ] };
|
|
||||||
key <AB04> { type= "ALPHABETIC", [ v, V ] };
|
|
||||||
key <LCTL> { [ Control_L ] };
|
|
||||||
key <LFSH> { [ Shift_L ] };
|
|
||||||
};
|
|
||||||
};`
|
|
||||||
|
|
||||||
const hexKeymap = `xkb_keymap {
|
|
||||||
xkb_keycodes "(unnamed)" {
|
|
||||||
<AB04> = 56;
|
|
||||||
<LCTL> = 38;
|
|
||||||
<LFSH> = 51;
|
|
||||||
};
|
|
||||||
xkb_symbols "(unnamed)" {
|
|
||||||
key <LCTL> { [ 0xffe3 ] };
|
|
||||||
key <LFSH> {
|
|
||||||
type= "PC_ALT_LEVEL2",
|
|
||||||
symbols[1]= [ 0xffe1, 0xfe08 ]
|
|
||||||
};
|
|
||||||
key <AB04> { [ 0x76, 0x56 ] };
|
|
||||||
};
|
|
||||||
};`
|
|
||||||
|
|
||||||
const dvorakKeymap = `xkb_keymap {
|
|
||||||
xkb_keycodes "(unnamed)" {
|
|
||||||
<AB09> = 60;
|
|
||||||
<LCTL> = 37;
|
|
||||||
<LFSH> = 50;
|
|
||||||
};
|
|
||||||
xkb_symbols "(unnamed)" {
|
|
||||||
key <AB09> { [ v, V ] };
|
|
||||||
key <LCTL> { [ Control_L ] };
|
|
||||||
key <LFSH> { [ Shift_L ] };
|
|
||||||
};
|
|
||||||
};`
|
|
||||||
|
|
||||||
func TestResolveKeycodes(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
keymap string
|
|
||||||
want pasteKeycodes
|
|
||||||
}{
|
|
||||||
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
|
|
||||||
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
|
|
||||||
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
|
|
||||||
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := resolveKeycodes(tt.keymap)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExpandOffers(t *testing.T) {
|
|
||||||
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
|
|
||||||
if len(text) != 5 {
|
|
||||||
t.Fatalf("expected 5 text offers, got %d", len(text))
|
|
||||||
}
|
|
||||||
seen := map[string]bool{}
|
|
||||||
for _, o := range text {
|
|
||||||
if string(o.Data) != "hi" {
|
|
||||||
t.Errorf("offer %s has wrong data", o.MimeType)
|
|
||||||
}
|
|
||||||
if seen[o.MimeType] {
|
|
||||||
t.Errorf("duplicate offer %s", o.MimeType)
|
|
||||||
}
|
|
||||||
seen[o.MimeType] = true
|
|
||||||
}
|
|
||||||
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
|
|
||||||
t.Errorf("missing X11 alias offers: %v", seen)
|
|
||||||
}
|
|
||||||
|
|
||||||
img := ExpandOffers([]byte{1}, "image/png")
|
|
||||||
if len(img) != 1 || img[0].MimeType != "image/png" {
|
|
||||||
t.Errorf("non-text mime should not expand, got %+v", img)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
type session struct {
|
|
||||||
display *wlclient.Display
|
|
||||||
ctx *wlclient.Context
|
|
||||||
registry *wlclient.Registry
|
|
||||||
seat *wlclient.Seat
|
|
||||||
dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
virtualKeyboardMgr *virtual_keyboard.ZwpVirtualKeyboardManagerV1
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectSession opens a short-lived Wayland connection and binds the seat
|
|
||||||
// plus whichever clipboard-related globals the compositor advertises.
|
|
||||||
func connectSession() (*session, error) {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &session{display: display, ctx: display.Context()}
|
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
display.Destroy()
|
|
||||||
return nil, fmt.Errorf("get registry: %w", err)
|
|
||||||
}
|
|
||||||
s.registry = registry
|
|
||||||
|
|
||||||
var bindErr error
|
|
||||||
bind := func(name uint32, iface string, version uint32, proxy wlclient.Proxy) {
|
|
||||||
if err := registry.Bind(name, iface, version, proxy); err != nil {
|
|
||||||
bindErr = fmt.Errorf("bind %s: %w", iface, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case ext_data_control.ExtDataControlManagerV1InterfaceName:
|
|
||||||
mgr := ext_data_control.NewExtDataControlManagerV1(s.ctx)
|
|
||||||
bind(e.Name, e.Interface, e.Version, mgr)
|
|
||||||
s.dataControlMgr = mgr
|
|
||||||
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
|
|
||||||
mgr := virtual_keyboard.NewZwpVirtualKeyboardManagerV1(s.ctx)
|
|
||||||
bind(e.Name, e.Interface, e.Version, mgr)
|
|
||||||
s.virtualKeyboardMgr = mgr
|
|
||||||
case "wl_seat":
|
|
||||||
if s.seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat := wlclient.NewSeat(s.ctx)
|
|
||||||
bind(e.Name, e.Interface, e.Version, seat)
|
|
||||||
s.seat = seat
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
s.Close()
|
|
||||||
return nil, bindErr
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) requireDataControl() (*ext_data_control.ExtDataControlManagerV1, error) {
|
|
||||||
switch {
|
|
||||||
case s.dataControlMgr == nil:
|
|
||||||
return nil, fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
case s.seat == nil:
|
|
||||||
return nil, fmt.Errorf("no seat available")
|
|
||||||
default:
|
|
||||||
return s.dataControlMgr, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) Close() {
|
|
||||||
if s.dataControlMgr != nil {
|
|
||||||
s.dataControlMgr.Destroy()
|
|
||||||
}
|
|
||||||
if s.registry != nil {
|
|
||||||
s.registry.Destroy()
|
|
||||||
}
|
|
||||||
s.display.Destroy()
|
|
||||||
}
|
|
||||||
@@ -61,10 +61,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
|
||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
|
|||||||
copy-on-select = false
|
copy-on-select = false
|
||||||
confirm-close-surface = false
|
confirm-close-surface = false
|
||||||
|
|
||||||
# Disable in-app Ghostty toast notifications
|
# Disable annoying copied to clipboard
|
||||||
app-notifications = false
|
app-notifications = no-clipboard-copy,no-config-reload
|
||||||
|
|
||||||
# Key bindings for common actions
|
# Key bindings for common actions
|
||||||
#keybind = ctrl+c=copy_to_clipboard
|
#keybind = ctrl+c=copy_to_clipboard
|
||||||
|
|||||||
@@ -9,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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
|
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
dependencies = append(dependencies, o.detectDanksearch())
|
|
||||||
dependencies = append(dependencies, o.detectDankCalendar())
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -131,8 +129,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
|
||||||
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -80,8 +80,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
dependencies = append(dependencies, u.detectDanksearch())
|
|
||||||
dependencies = append(dependencies, u.detectDankCalendar())
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -126,8 +124,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
|
||||||
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -1,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
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -407,41 +402,14 @@ func TestBuildDisabledItems(t *testing.T) {
|
|||||||
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 {
|
||||||
@@ -449,8 +417,6 @@ func TestBuildDisabledItems(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 {
|
||||||
|
|||||||
@@ -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 := §ion.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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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, ";")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
// Plugin is a standalone repo
|
||||||
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(pluginPath)
|
return m.gitClient.HasUpdates(pluginPath)
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +1068,10 @@ 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 {
|
|
||||||
log.Errorf("Failed to offer %s: %v", offer.MimeType, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
offerData[offer.MimeType] = offer.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
fd := e.Fd
|
fd := e.Fd
|
||||||
@@ -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})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.takeSelection(offers)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
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{}
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,15 @@ type Manager struct {
|
|||||||
offerMutex sync.RWMutex
|
offerMutex sync.RWMutex
|
||||||
offerRegistry map[uint32]any
|
offerRegistry map[uint32]any
|
||||||
|
|
||||||
|
sourceMimeTypes []string
|
||||||
|
sourceMutex sync.RWMutex
|
||||||
|
|
||||||
|
persistData map[string][]byte
|
||||||
|
persistMimeTypes []string
|
||||||
|
persistMutex sync.RWMutex
|
||||||
|
|
||||||
isOwner bool
|
isOwner bool
|
||||||
ownerLock sync.Mutex
|
ownerLock sync.Mutex
|
||||||
pasteSupported bool
|
|
||||||
|
|
||||||
initialized bool
|
initialized bool
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,9 +90,6 @@ 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");
|
|
||||||
// falls back to Password under the "password" key when empty.
|
|
||||||
Secrets map[string]string
|
|
||||||
SavePassword bool
|
SavePassword bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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,27 +184,14 @@ 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() {
|
m.post(func() {
|
||||||
if err := m.addOutputControl(output); err != nil {
|
if err := m.addOutputControl(output); err != nil {
|
||||||
log.Warnf("gamma: failed to add output control: %v", err)
|
log.Warnf("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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
|
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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,18 +201,8 @@ func (m Model) viewInstallComplete() string {
|
|||||||
|
|
||||||
wm := m.selectedWindowManager()
|
wm := m.selectedWindowManager()
|
||||||
|
|
||||||
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
|
||||||
if !m.useSystemdConfig() {
|
|
||||||
switch wm {
|
|
||||||
case deps.WindowManagerNiri:
|
|
||||||
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri"
|
|
||||||
case deps.WindowManagerHyprland:
|
|
||||||
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland"
|
|
||||||
case deps.WindowManagerMango:
|
|
||||||
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)
|
// 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\""
|
||||||
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, login with \"niri-session\""
|
||||||
@@ -223,7 +211,6 @@ func (m Model) viewInstallComplete() string {
|
|||||||
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, login with \"mango\""
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + loginHint)
|
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + loginHint)
|
||||||
@@ -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,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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -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!" && \
|
||||||
|
|||||||
@@ -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!" && \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
Reference in New Issue
Block a user