mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-31 08:52:49 -05:00
Compare commits
10 Commits
ce4aca9a72
...
hotfix-1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
119b5df6df | ||
|
|
8ede810d32 | ||
|
|
830dd93af5 | ||
|
|
75f28c5ea7 | ||
|
|
6c9b8c590e | ||
|
|
24d9b77307 | ||
|
|
d4be68912c | ||
|
|
a443721000 | ||
|
|
786b097187 | ||
|
|
8ca60c7d2a |
@@ -1,8 +0,0 @@
|
|||||||
[*.sh]
|
|
||||||
# like -i=4
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
[*.nix]
|
|
||||||
# like -i=4
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
69
.githooks/pre-commit
Executable file
69
.githooks/pre-commit
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Go CI checks (when core/ files are staged)
|
||||||
|
# =============================================================================
|
||||||
|
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
||||||
|
|
||||||
|
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||||
|
echo "Go files staged in core/, running CI checks..."
|
||||||
|
cd "$REPO_ROOT/core"
|
||||||
|
|
||||||
|
# Format check
|
||||||
|
echo " Checking gofmt..."
|
||||||
|
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
||||||
|
if [[ -n "$UNFORMATTED" ]]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
echo "$UNFORMATTED"
|
||||||
|
echo ""
|
||||||
|
echo "Run: cd core && gofmt -s -w ."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# golangci-lint
|
||||||
|
if command -v golangci-lint &>/dev/null; then
|
||||||
|
echo " Running golangci-lint..."
|
||||||
|
golangci-lint run ./...
|
||||||
|
else
|
||||||
|
echo " Warning: golangci-lint not installed, skipping lint"
|
||||||
|
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
echo " Running tests..."
|
||||||
|
if ! go test ./... >/dev/null 2>&1; then
|
||||||
|
echo "Tests failed! Run 'go test ./...' for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build checks
|
||||||
|
echo " Building..."
|
||||||
|
mkdir -p bin
|
||||||
|
go build -buildvcs=false -o bin/dms ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
||||||
|
|
||||||
|
echo "All Go CI checks passed!"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# i18n sync check (DISABLED for now)
|
||||||
|
# =============================================================================
|
||||||
|
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||||
|
# if command -v python3 &>/dev/null; then
|
||||||
|
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||||
|
# echo "Translations out of sync"
|
||||||
|
# echo "Run: python3 scripts/i18nsync.py sync"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
14
.github/workflows/go-ci.yml
vendored
14
.github/workflows/go-ci.yml
vendored
@@ -33,6 +33,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.6
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
|
|||||||
15
.github/workflows/prek.yml
vendored
15
.github/workflows/prek.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
name: Pre-commit Checks
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
branches: [master, main]
|
|
||||||
jobs:
|
|
||||||
pre-commit-check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: run pre-commit hooks
|
|
||||||
uses: j178/prek-action@v1
|
|
||||||
485
.github/workflows/release.yml
vendored
485
.github/workflows/release.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: "Tag to release (e.g., v1.0.1)"
|
description: 'Tag to release (e.g., v1.0.1)'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -281,9 +281,6 @@ jobs:
|
|||||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
cp LICENSE CONTRIBUTING.md quickshell/
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
|
|
||||||
# Copy root assets directory to quickshell for systemd service and desktop file
|
|
||||||
cp -r assets quickshell/
|
|
||||||
|
|
||||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
(cd quickshell && tar --exclude='.git' \
|
(cd quickshell && tar --exclude='.git' \
|
||||||
--exclude='.github' \
|
--exclude='.github' \
|
||||||
@@ -399,296 +396,296 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# trigger-obs-update:
|
trigger-obs-update:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# needs: release
|
needs: release
|
||||||
# env:
|
env:
|
||||||
# TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
# steps:
|
steps:
|
||||||
# - name: Checkout
|
- name: Checkout
|
||||||
# uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
# with:
|
with:
|
||||||
# ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
# - name: Install OSC
|
- name: Install OSC
|
||||||
# run: |
|
run: |
|
||||||
# sudo apt-get update
|
sudo apt-get update
|
||||||
# sudo apt-get install -y osc
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
# mkdir -p ~/.config/osc
|
mkdir -p ~/.config/osc
|
||||||
# cat > ~/.config/osc/oscrc << EOF
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
# [general]
|
[general]
|
||||||
# apiurl = https://api.opensuse.org
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
# [https://api.opensuse.org]
|
[https://api.opensuse.org]
|
||||||
# user = ${{ secrets.OBS_USERNAME }}
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
# pass = ${{ secrets.OBS_PASSWORD }}
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
# EOF
|
EOF
|
||||||
# chmod 600 ~/.config/osc/oscrc
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
# - name: Update OBS packages
|
- name: Update OBS packages
|
||||||
# run: |
|
run: |
|
||||||
# cd distro
|
cd distro
|
||||||
# bash scripts/obs-upload.sh dms "Update to ${TAG}"
|
bash scripts/obs-upload.sh dms "Update to ${TAG}"
|
||||||
|
|
||||||
# trigger-ppa-update:
|
trigger-ppa-update:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# needs: release
|
needs: release
|
||||||
# env:
|
env:
|
||||||
# TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
# steps:
|
steps:
|
||||||
# - name: Checkout
|
- name: Checkout
|
||||||
# uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
# with:
|
with:
|
||||||
# ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
# - name: Install build dependencies
|
- name: Install build dependencies
|
||||||
# run: |
|
run: |
|
||||||
# sudo apt-get update
|
sudo apt-get update
|
||||||
# sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
# debhelper \
|
debhelper \
|
||||||
# devscripts \
|
devscripts \
|
||||||
# dput \
|
dput \
|
||||||
# lftp \
|
lftp \
|
||||||
# build-essential \
|
build-essential \
|
||||||
# fakeroot \
|
fakeroot \
|
||||||
# dpkg-dev
|
dpkg-dev
|
||||||
|
|
||||||
# - name: Configure GPG
|
- name: Configure GPG
|
||||||
# env:
|
env:
|
||||||
# GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
# run: |
|
run: |
|
||||||
# echo "$GPG_KEY" | gpg --import
|
echo "$GPG_KEY" | gpg --import
|
||||||
# GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
# echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
# - name: Upload to PPA
|
- name: Upload to PPA
|
||||||
# run: |
|
run: |
|
||||||
# cd distro/ubuntu/ppa
|
cd distro/ubuntu/ppa
|
||||||
# bash create-and-upload.sh ../dms dms questing
|
bash create-and-upload.sh ../dms dms questing
|
||||||
|
|
||||||
# copr-build:
|
copr-build:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# needs: release
|
needs: release
|
||||||
# env:
|
env:
|
||||||
# TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
|
|
||||||
# steps:
|
steps:
|
||||||
# - name: Checkout repository
|
- name: Checkout repository
|
||||||
# uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
# with:
|
with:
|
||||||
# ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
# - name: Determine version
|
- name: Determine version
|
||||||
# id: version
|
id: version
|
||||||
# run: |
|
run: |
|
||||||
# VERSION="${TAG#v}"
|
VERSION="${TAG#v}"
|
||||||
# echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
# echo "Building DMS stable version: $VERSION"
|
echo "Building DMS stable version: $VERSION"
|
||||||
|
|
||||||
# - name: Setup build environment
|
- name: Setup build environment
|
||||||
# run: |
|
run: |
|
||||||
# sudo apt-get update
|
sudo apt-get update
|
||||||
# sudo apt-get install -y rpm wget curl jq gzip
|
sudo apt-get install -y rpm wget curl jq gzip
|
||||||
# mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
# - name: Download release assets
|
- name: Download release assets
|
||||||
# run: |
|
run: |
|
||||||
# VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
# cd ~/rpmbuild/SOURCES
|
cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
# wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||||
# echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||||
# exit 1
|
exit 1
|
||||||
# }
|
}
|
||||||
|
|
||||||
# - name: Generate stable spec file
|
- name: Generate stable spec file
|
||||||
# run: |
|
run: |
|
||||||
# VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
# CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
|
||||||
# cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
# # Spec for DMS stable releases - Generated by GitHub Actions
|
# Spec for DMS stable releases - Generated by GitHub Actions
|
||||||
|
|
||||||
# %global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
# %global version VERSION_PLACEHOLDER
|
%global version VERSION_PLACEHOLDER
|
||||||
# %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
|
|
||||||
# Name: dms
|
Name: dms
|
||||||
# Version: %{version}
|
Version: %{version}
|
||||||
# Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
# Summary: %{pkg_summary}
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
# License: MIT
|
License: MIT
|
||||||
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
# Source0: dms-qml.tar.gz
|
Source0: dms-qml.tar.gz
|
||||||
|
|
||||||
# BuildRequires: gzip
|
BuildRequires: gzip
|
||||||
# BuildRequires: wget
|
BuildRequires: wget
|
||||||
# BuildRequires: systemd-rpm-macros
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
# Requires: (quickshell or quickshell-git)
|
Requires: (quickshell or quickshell-git)
|
||||||
# Requires: accountsservice
|
Requires: accountsservice
|
||||||
# Requires: dms-cli = %{version}-%{release}
|
Requires: dms-cli = %{version}-%{release}
|
||||||
# Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
# Recommends: cava
|
Recommends: cava
|
||||||
# Recommends: cliphist
|
Recommends: cliphist
|
||||||
# Recommends: danksearch
|
Recommends: danksearch
|
||||||
# Recommends: matugen
|
Recommends: matugen
|
||||||
# Recommends: wl-clipboard
|
Recommends: wl-clipboard
|
||||||
# Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
# Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
# Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
# %description
|
%description
|
||||||
# DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||||
# and optimized for the niri and hyprland compositors. Features notifications,
|
and optimized for the niri and hyprland compositors. Features notifications,
|
||||||
# app launcher, wallpaper customization, and fully customizable with plugins.
|
app launcher, wallpaper customization, and fully customizable with plugins.
|
||||||
|
|
||||||
# Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||||
# process monitoring, notification center, clipboard history, dock, control center,
|
process monitoring, notification center, clipboard history, dock, control center,
|
||||||
# lock screen, and comprehensive plugin system.
|
lock screen, and comprehensive plugin system.
|
||||||
|
|
||||||
# %package -n dms-cli
|
%package -n dms-cli
|
||||||
# Summary: DankMaterialShell CLI tool
|
Summary: DankMaterialShell CLI tool
|
||||||
# License: MIT
|
License: MIT
|
||||||
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
# %description -n dms-cli
|
%description -n dms-cli
|
||||||
# Command-line interface for DankMaterialShell configuration and management.
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
# Provides native DBus bindings, NetworkManager integration, and system utilities.
|
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
# %prep
|
%prep
|
||||||
# %setup -q -c -n dms-qml
|
%setup -q -c -n dms-qml
|
||||||
|
|
||||||
# # Download architecture-specific binaries during build
|
# Download architecture-specific binaries during build
|
||||||
# case "%{_arch}" in
|
case "%{_arch}" in
|
||||||
# x86_64)
|
x86_64)
|
||||||
# ARCH_SUFFIX="amd64"
|
ARCH_SUFFIX="amd64"
|
||||||
# ;;
|
;;
|
||||||
# aarch64)
|
aarch64)
|
||||||
# ARCH_SUFFIX="arm64"
|
ARCH_SUFFIX="arm64"
|
||||||
# ;;
|
;;
|
||||||
# *)
|
*)
|
||||||
# echo "Unsupported architecture: %{_arch}"
|
echo "Unsupported architecture: %{_arch}"
|
||||||
# exit 1
|
exit 1
|
||||||
# ;;
|
;;
|
||||||
# esac
|
esac
|
||||||
|
|
||||||
# wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
# echo "Failed to download dms-cli for architecture %{_arch}"
|
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
# exit 1
|
exit 1
|
||||||
# }
|
}
|
||||||
# gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
# chmod +x %{_builddir}/dms-cli
|
chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
# %build
|
%build
|
||||||
|
|
||||||
# %install
|
%install
|
||||||
# install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
|
||||||
# install -d %{buildroot}%{_datadir}/bash-completion/completions
|
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
# install -d %{buildroot}%{_datadir}/zsh/site-functions
|
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
# install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
# %{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||||
# %{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
# %{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
# install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
# 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/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
# install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
# cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
|
|
||||||
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
# rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
# echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
||||||
|
|
||||||
# %posttrans
|
%posttrans
|
||||||
# if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
# rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
# rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
# rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
# fi
|
fi
|
||||||
# # Signal running DMS instances to reload
|
# Signal running DMS instances to reload
|
||||||
# pkill -USR1 -x dms >/dev/null 2>&1 || :
|
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||||
|
|
||||||
# %files
|
%files
|
||||||
# %license LICENSE
|
%license LICENSE
|
||||||
# %doc README.md CONTRIBUTING.md
|
%doc README.md CONTRIBUTING.md
|
||||||
# %{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
# %{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
# %{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
# %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
# %files -n dms-cli
|
%files -n dms-cli
|
||||||
# %{_bindir}/dms
|
%{_bindir}/dms
|
||||||
# %{_datadir}/bash-completion/completions/dms
|
%{_datadir}/bash-completion/completions/dms
|
||||||
# %{_datadir}/zsh/site-functions/_dms
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
# %{_datadir}/fish/vendor_completions.d/dms.fish
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
# %changelog
|
%changelog
|
||||||
# * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
||||||
# - Stable release VERSION_PLACEHOLDER
|
- Stable release VERSION_PLACEHOLDER
|
||||||
# - Built from GitHub release
|
- Built from GitHub release
|
||||||
# SPECEOF
|
SPECEOF
|
||||||
|
|
||||||
# sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
# sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
# - name: Build SRPM
|
- name: Build SRPM
|
||||||
# id: build
|
id: build
|
||||||
# run: |
|
run: |
|
||||||
# cd ~/rpmbuild/SPECS
|
cd ~/rpmbuild/SPECS
|
||||||
# rpmbuild -bs dms.spec
|
rpmbuild -bs dms.spec
|
||||||
|
|
||||||
# SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||||
# SRPM_NAME=$(basename "$SRPM")
|
SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
# echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
# echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||||
# echo "SRPM built: $SRPM_NAME"
|
echo "SRPM built: $SRPM_NAME"
|
||||||
|
|
||||||
# - name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
# uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
# with:
|
with:
|
||||||
# name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
# path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
# retention-days: 90
|
retention-days: 90
|
||||||
|
|
||||||
# - name: Install Copr CLI
|
- name: Install Copr CLI
|
||||||
# run: |
|
run: |
|
||||||
# sudo apt-get install -y python3-pip
|
sudo apt-get install -y python3-pip
|
||||||
# pip3 install copr-cli
|
pip3 install copr-cli
|
||||||
|
|
||||||
# mkdir -p ~/.config
|
mkdir -p ~/.config
|
||||||
# cat > ~/.config/copr << EOF
|
cat > ~/.config/copr << EOF
|
||||||
# [copr-cli]
|
[copr-cli]
|
||||||
# login = ${{ secrets.COPR_LOGIN }}
|
login = ${{ secrets.COPR_LOGIN }}
|
||||||
# username = avengemedia
|
username = avengemedia
|
||||||
# token = ${{ secrets.COPR_TOKEN }}
|
token = ${{ secrets.COPR_TOKEN }}
|
||||||
# copr_url = https://copr.fedorainfracloud.org
|
copr_url = https://copr.fedorainfracloud.org
|
||||||
# EOF
|
EOF
|
||||||
# chmod 600 ~/.config/copr
|
chmod 600 ~/.config/copr
|
||||||
|
|
||||||
# - name: Upload to Copr
|
- name: Upload to Copr
|
||||||
# run: |
|
run: |
|
||||||
# SRPM="${{ steps.build.outputs.srpm_path }}"
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
# VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
# echo "Uploading SRPM to avengemedia/dms..."
|
echo "Uploading SRPM to avengemedia/dms..."
|
||||||
# BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
# echo "$BUILD_OUTPUT"
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
# BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
# if [ "$BUILD_ID" != "unknown" ]; then
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
# echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
# fi
|
fi
|
||||||
|
|||||||
78
.github/workflows/run-obs.yml
vendored
78
.github/workflows/run-obs.yml
vendored
@@ -4,18 +4,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to update (dms, dms-git, or all)"
|
description: 'Package to update (dms, dms-git, or all)'
|
||||||
required: false
|
required: false
|
||||||
default: "all"
|
default: 'all'
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ''
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
@@ -113,9 +113,6 @@ jobs:
|
|||||||
name: Upload to OBS
|
name: Upload to OBS
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
@@ -125,7 +122,6 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
@@ -219,48 +215,17 @@ jobs:
|
|||||||
# Update openSUSE dms spec (stable only)
|
# Update openSUSE dms spec (stable only)
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
|
||||||
# Update openSUSE spec changelog
|
# Update Debian _service files
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
|
||||||
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
|
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
# Update Debian _service files (both tar_scm and download_url formats)
|
|
||||||
for service in distro/debian/*/_service; do
|
for service in distro/debian/*/_service; do
|
||||||
if [[ -f "$service" ]]; then
|
if [[ -f "$service" ]]; then
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
|
||||||
# Update download_url paths (for dms stable)
|
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Update Debian changelog for dms stable
|
|
||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$TEMP_CHANGELOG" << EOF
|
|
||||||
dms ($VERSION_NO_V) stable; urgency=medium
|
|
||||||
|
|
||||||
* Update to $VERSION stable release
|
|
||||||
* Bug fixes and improvements
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
|
|
||||||
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
|
|
||||||
|
|
||||||
echo "✓ Updated Debian changelog to $VERSION_NO_V"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: '1.24'
|
||||||
|
|
||||||
- name: Install OSC
|
- name: Install OSC
|
||||||
run: |
|
run: |
|
||||||
@@ -297,33 +262,6 @@ jobs:
|
|||||||
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Get changed packages
|
|
||||||
id: changed-packages
|
|
||||||
run: |
|
|
||||||
# Check if there are any changes to commit
|
|
||||||
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog or spec changes to commit"
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
# Get list of changed packages for commit message
|
|
||||||
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
|
|
||||||
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
|
|
||||||
|
|
||||||
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "packages=$PKGS" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Changed packages: $PKGS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit packaging changes
|
|
||||||
if: steps.changed-packages.outputs.has_changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
|
|
||||||
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
77
.github/workflows/run-ppa.yml
vendored
77
.github/workflows/run-ppa.yml
vendored
@@ -4,15 +4,15 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
||||||
required: false
|
required: false
|
||||||
default: "dms-git"
|
default: 'dms-git'
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ''
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
@@ -75,9 +75,6 @@ jobs:
|
|||||||
name: Upload to PPA
|
name: Upload to PPA
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
@@ -87,12 +84,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: '1.24'
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
@@ -129,16 +125,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload to PPA
|
- name: Upload to PPA
|
||||||
|
env:
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
run: |
|
run: |
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
|
|
||||||
# Build command arguments
|
|
||||||
BUILD_ARGS=()
|
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
|
||||||
BUILD_ARGS+=("$REBUILD_RELEASE")
|
|
||||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@@ -147,68 +137,27 @@ jobs:
|
|||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "Uploading dms-git to PPA..."
|
echo "Uploading dms-git to PPA..."
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "Uploading dms-greeter to PPA..."
|
echo "Uploading dms-greeter to PPA..."
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
||||||
else
|
else
|
||||||
# Map package to PPA name
|
PPA_NAME="$PACKAGES"
|
||||||
case "$PACKAGES" in
|
|
||||||
dms)
|
|
||||||
PPA_NAME="dms"
|
|
||||||
;;
|
|
||||||
dms-git)
|
|
||||||
PPA_NAME="dms-git"
|
|
||||||
;;
|
|
||||||
dms-greeter)
|
|
||||||
PPA_NAME="danklinux"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
PPA_NAME="$PACKAGES"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "Uploading $PACKAGES to PPA..."
|
echo "Uploading $PACKAGES to PPA..."
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Get changed packages
|
|
||||||
id: changed-packages
|
|
||||||
run: |
|
|
||||||
# Check if there are any changelog changes to commit
|
|
||||||
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog changes to commit"
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
# Get list of changed packages for commit message (deduplicate)
|
|
||||||
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Changed packages: $CHANGED"
|
|
||||||
echo "📋 Debug - Changed files:"
|
|
||||||
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit changelog changes
|
|
||||||
if: steps.changed-packages.outputs.has_changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add distro/ubuntu/*/debian/changelog
|
|
||||||
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
19
.github/workflows/stable.yml
vendored
19
.github/workflows/stable.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
name: Update stable branch
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-stable:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Push to stable branch
|
|
||||||
run: git push origin HEAD:refs/heads/stable --force
|
|
||||||
4
.github/workflows/update-vendor-hash.yml
vendored
4
.github/workflows/update-vendor-hash.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo "Attempting nix build to get new vendorHash..."
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
if output=$(nix build .#dms-shell 2>&1); then
|
if output=$(nix build .#dmsCli 2>&1); then
|
||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
echo "Verifying build with new vendorHash..."
|
echo "Verifying build with new vendorHash..."
|
||||||
nix build .#dms-shell
|
nix build .#dmsCli
|
||||||
echo "vendorHash updated successfully!"
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
- name: Commit and push vendorHash update
|
- name: Commit and push vendorHash update
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -104,6 +104,12 @@ go.work.sum
|
|||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
|
# Extracted source trees in Ubuntu package directories
|
||||||
|
distro/ubuntu/*/dms-git-repo/
|
||||||
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- id: check-yaml
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
||||||
rev: v0.10.0.1
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# 1.2.0
|
|
||||||
|
|
||||||
- Added clipboard and clipboard history integration
|
|
||||||
- Added swipe to dismiss notification popups and from center
|
|
||||||
- Added paste from clipboard history view - requires wtype
|
|
||||||
- Optimize surface damage of OSD & Toast
|
|
||||||
@@ -6,10 +6,10 @@ To contribute fork this repository, make your changes, and open a pull request.
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
Enable pre-commit hooks to catch CI failures before pushing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
prek install
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nix Development Shell
|
### Nix Development Shell
|
||||||
@@ -21,7 +21,6 @@ nix develop
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will provide:
|
This will provide:
|
||||||
|
|
||||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
- Quickshell and required QML packages
|
- Quickshell and required QML packages
|
||||||
- Properly configured QML2_IMPORT_PATH
|
- Properly configured QML2_IMPORT_PATH
|
||||||
@@ -55,20 +54,6 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
### I18n/Localization
|
|
||||||
|
|
||||||
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
|
|
||||||
|
|
||||||
```qml
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
|
||||||
|
|
||||||
### GO (`core` directory)
|
### GO (`core` directory)
|
||||||
|
|
||||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
|
|||||||
@@ -102,11 +102,7 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- ineffassign
|
- ineffassign
|
||||||
path: internal/proto/
|
path: internal/proto/
|
||||||
# binary.Write/Read to bytes.Buffer can't fail
|
# binary.Write to bytes.Buffer can't fail
|
||||||
- linters:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
text: "Error return value of `binary\\.(Write|Read)` is not checked"
|
text: "Error return value of `binary\\.Write` is not checked"
|
||||||
# bytes.Reader.Read can't fail (reads from memory)
|
|
||||||
- linters:
|
|
||||||
- errcheck
|
|
||||||
text: "Error return value of `buf\\.Read` is not checked"
|
|
||||||
|
|||||||
@@ -56,15 +56,3 @@ packages:
|
|||||||
outpkg: mocks_version
|
outpkg: mocks_version
|
||||||
interfaces:
|
interfaces:
|
||||||
VersionFetcher:
|
VersionFetcher:
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/wlcontext"
|
|
||||||
outpkg: mocks_wlcontext
|
|
||||||
interfaces:
|
|
||||||
WaylandContext:
|
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
|
||||||
config:
|
|
||||||
dir: "internal/mocks/wlclient"
|
|
||||||
outpkg: mocks_wlclient
|
|
||||||
interfaces:
|
|
||||||
WaylandDisplay:
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
|
||||||
rev: v2.6.2
|
|
||||||
hooks:
|
|
||||||
- id: golangci-lint-full
|
|
||||||
- id: golangci-lint-fmt
|
|
||||||
- id: golangci-lint-config-verify
|
|
||||||
@@ -1,628 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var clipboardCmd = &cobra.Command{
|
|
||||||
Use: "clipboard",
|
|
||||||
Aliases: []string{"cl"},
|
|
||||||
Short: "Manage clipboard",
|
|
||||||
Long: "Interact with the clipboard manager",
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipCopyCmd = &cobra.Command{
|
|
||||||
Use: "copy [text]",
|
|
||||||
Short: "Copy text to clipboard",
|
|
||||||
Long: "Copy text to clipboard. If no text provided, reads from stdin. Works without server.",
|
|
||||||
Run: runClipCopy,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
clipCopyForeground bool
|
|
||||||
clipCopyPasteOnce bool
|
|
||||||
clipCopyType string
|
|
||||||
clipJSONOutput bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var clipPasteCmd = &cobra.Command{
|
|
||||||
Use: "paste",
|
|
||||||
Short: "Paste text from clipboard",
|
|
||||||
Long: "Paste text from clipboard to stdout. Works without server.",
|
|
||||||
Run: runClipPaste,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipWatchCmd = &cobra.Command{
|
|
||||||
Use: "watch [command]",
|
|
||||||
Short: "Watch clipboard for changes",
|
|
||||||
Long: `Watch clipboard for changes and optionally execute a command.
|
|
||||||
Works like wl-paste --watch. Does not require server.
|
|
||||||
|
|
||||||
If a command is provided, it will be executed each time the clipboard changes,
|
|
||||||
with the clipboard content piped to its stdin.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms cl watch # Print clipboard changes to stdout
|
|
||||||
dms cl watch cat # Same as above
|
|
||||||
dms cl watch notify-send # Send notification on clipboard change`,
|
|
||||||
Run: runClipWatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipHistoryCmd = &cobra.Command{
|
|
||||||
Use: "history",
|
|
||||||
Short: "Show clipboard history",
|
|
||||||
Long: "Show clipboard history with previews (requires server)",
|
|
||||||
Run: runClipHistory,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipGetCmd = &cobra.Command{
|
|
||||||
Use: "get <id>",
|
|
||||||
Short: "Get clipboard entry by ID",
|
|
||||||
Long: "Get full clipboard entry data by ID (requires server). Use --copy to copy it to clipboard.",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runClipGet,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipGetCopy bool
|
|
||||||
|
|
||||||
var clipDeleteCmd = &cobra.Command{
|
|
||||||
Use: "delete <id>",
|
|
||||||
Short: "Delete clipboard entry",
|
|
||||||
Long: "Delete a clipboard history entry by ID (requires server)",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runClipDelete,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipClearCmd = &cobra.Command{
|
|
||||||
Use: "clear",
|
|
||||||
Short: "Clear clipboard history",
|
|
||||||
Long: "Clear all clipboard history (requires server)",
|
|
||||||
Run: runClipClear,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipWatchStore bool
|
|
||||||
|
|
||||||
var clipSearchCmd = &cobra.Command{
|
|
||||||
Use: "search [query]",
|
|
||||||
Short: "Search clipboard history",
|
|
||||||
Long: "Search clipboard history with filters (requires server)",
|
|
||||||
Run: runClipSearch,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
clipSearchLimit int
|
|
||||||
clipSearchOffset int
|
|
||||||
clipSearchMimeType string
|
|
||||||
clipSearchImages bool
|
|
||||||
clipSearchText bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var clipConfigCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Short: "Manage clipboard config",
|
|
||||||
Long: "Get or set clipboard configuration (requires server)",
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipConfigGetCmd = &cobra.Command{
|
|
||||||
Use: "get",
|
|
||||||
Short: "Get clipboard config",
|
|
||||||
Run: runClipConfigGet,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clipConfigSetCmd = &cobra.Command{
|
|
||||||
Use: "set",
|
|
||||||
Short: "Set clipboard config",
|
|
||||||
Long: `Set clipboard configuration options.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms cl config set --max-history 200
|
|
||||||
dms cl config set --auto-clear-days 7
|
|
||||||
dms cl config set --clear-at-startup`,
|
|
||||||
Run: runClipConfigSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
clipConfigMaxHistory int
|
|
||||||
clipConfigAutoClearDays int
|
|
||||||
clipConfigClearAtStartup bool
|
|
||||||
clipConfigNoClearStartup bool
|
|
||||||
clipConfigDisabled bool
|
|
||||||
clipConfigEnabled bool
|
|
||||||
clipConfigDisableHistory bool
|
|
||||||
clipConfigEnableHistory bool
|
|
||||||
clipConfigDisablePersist bool
|
|
||||||
clipConfigEnablePersist bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
|
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
|
||||||
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
|
||||||
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
|
||||||
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
|
||||||
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "c", false, "Copy entry to clipboard")
|
|
||||||
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
|
||||||
clipSearchCmd.Flags().StringVarP(&clipSearchMimeType, "mime", "m", "", "Filter by MIME type")
|
|
||||||
clipSearchCmd.Flags().BoolVar(&clipSearchImages, "images", false, "Only images")
|
|
||||||
clipSearchCmd.Flags().BoolVar(&clipSearchText, "text", false, "Only text")
|
|
||||||
clipSearchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
|
||||||
|
|
||||||
clipConfigSetCmd.Flags().IntVar(&clipConfigMaxHistory, "max-history", 0, "Max history entries")
|
|
||||||
clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard manager entirely")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard manager")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisableHistory, "disable-history", false, "Disable clipboard history persistence")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnableHistory, "enable-history", false, "Enable clipboard history persistence")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisablePersist, "disable-persist", false, "Disable clipboard ownership persistence")
|
|
||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnablePersist, "enable-persist", false, "Enable clipboard ownership persistence")
|
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
|
||||||
|
|
||||||
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
|
||||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
data = []byte(args[0])
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
data, err = io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("read stdin: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("copy: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipPaste(cmd *cobra.Command, args []string) {
|
|
||||||
data, _, err := clipboard.Paste()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("paste: %v", err)
|
|
||||||
}
|
|
||||||
os.Stdout.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipWatch(cmd *cobra.Command, args []string) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
<-sigCh
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(args) > 0:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
runCommand(args, data)
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
case clipWatchStore:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
if err := clipboard.Store(data, mimeType); err != nil {
|
|
||||||
log.Errorf("store: %v", err)
|
|
||||||
}
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
case clipJSONOutput:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
out := map[string]any{
|
|
||||||
"data": string(data),
|
|
||||||
"mimeType": mimeType,
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"size": len(data),
|
|
||||||
}
|
|
||||||
j, _ := json.Marshal(out)
|
|
||||||
fmt.Println(string(j))
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
os.Stdout.Write(data)
|
|
||||||
os.Stdout.WriteString("\n")
|
|
||||||
}); err != nil && err != context.Canceled {
|
|
||||||
log.Fatalf("Watch error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCommand(args []string, stdin []byte) {
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if len(stdin) == 0 {
|
|
||||||
cmd.Run()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
cmd.Run()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdin = r
|
|
||||||
go func() {
|
|
||||||
w.Write(stdin)
|
|
||||||
w.Close()
|
|
||||||
}()
|
|
||||||
cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipHistory(cmd *cobra.Command, args []string) {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.getHistory",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get clipboard history: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
if clipJSONOutput {
|
|
||||||
fmt.Println("[]")
|
|
||||||
} else {
|
|
||||||
fmt.Println("No clipboard history")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
historyList, ok := (*resp.Result).([]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipJSONOutput {
|
|
||||||
out, _ := json.MarshalIndent(historyList, "", " ")
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(historyList) == 0 {
|
|
||||||
fmt.Println("No clipboard history")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Clipboard History:")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
for _, item := range historyList {
|
|
||||||
entry, ok := item.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uint64(entry["id"].(float64))
|
|
||||||
preview := entry["preview"].(string)
|
|
||||||
timestamp := entry["timestamp"].(string)
|
|
||||||
isImage := entry["isImage"].(bool)
|
|
||||||
|
|
||||||
typeStr := "text"
|
|
||||||
if isImage {
|
|
||||||
typeStr = "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp)
|
|
||||||
fmt.Printf(" %s\n", preview)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipGet(cmd *cobra.Command, args []string) {
|
|
||||||
id, err := strconv.ParseUint(args[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Invalid ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipGetCopy {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.copyEntry",
|
|
||||||
Params: map[string]any{
|
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Copied entry %d to clipboard\n", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.getEntry",
|
|
||||||
Params: map[string]any{
|
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("Entry not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, ok := (*resp.Result).(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case clipJSONOutput:
|
|
||||||
output, _ := json.MarshalIndent(entry, "", " ")
|
|
||||||
fmt.Println(string(output))
|
|
||||||
default:
|
|
||||||
if data, ok := entry["data"].(string); ok {
|
|
||||||
fmt.Print(data)
|
|
||||||
} else {
|
|
||||||
output, _ := json.MarshalIndent(entry, "", " ")
|
|
||||||
fmt.Println(string(output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipDelete(cmd *cobra.Command, args []string) {
|
|
||||||
id, err := strconv.ParseUint(args[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Invalid ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.deleteEntry",
|
|
||||||
Params: map[string]any{
|
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to delete clipboard entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Deleted entry %d\n", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipClear(cmd *cobra.Command, args []string) {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.clearHistory",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to clear clipboard history: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Clipboard history cleared")
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipSearch(cmd *cobra.Command, args []string) {
|
|
||||||
params := map[string]any{
|
|
||||||
"limit": clipSearchLimit,
|
|
||||||
"offset": clipSearchOffset,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
params["query"] = args[0]
|
|
||||||
}
|
|
||||||
if clipSearchMimeType != "" {
|
|
||||||
params["mimeType"] = clipSearchMimeType
|
|
||||||
}
|
|
||||||
if clipSearchImages {
|
|
||||||
params["isImage"] = true
|
|
||||||
} else if clipSearchText {
|
|
||||||
params["isImage"] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.search",
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to search clipboard: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("No results")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, ok := (*resp.Result).(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if clipJSONOutput {
|
|
||||||
out, _ := json.MarshalIndent(result, "", " ")
|
|
||||||
fmt.Println(string(out))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, _ := result["entries"].([]any)
|
|
||||||
total := int(result["total"].(float64))
|
|
||||||
hasMore := result["hasMore"].(bool)
|
|
||||||
|
|
||||||
if len(entries) == 0 {
|
|
||||||
fmt.Println("No results found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Results: %d of %d\n\n", len(entries), total)
|
|
||||||
|
|
||||||
for _, item := range entries {
|
|
||||||
entry, ok := item.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uint64(entry["id"].(float64))
|
|
||||||
preview := entry["preview"].(string)
|
|
||||||
timestamp := entry["timestamp"].(string)
|
|
||||||
isImage := entry["isImage"].(bool)
|
|
||||||
|
|
||||||
typeStr := "text"
|
|
||||||
if isImage {
|
|
||||||
typeStr = "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp)
|
|
||||||
fmt.Printf(" %s\n\n", preview)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasMore {
|
|
||||||
fmt.Printf("Use --offset %d to see more results\n", clipSearchOffset+clipSearchLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipConfigGet(cmd *cobra.Command, args []string) {
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.getConfig",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Result == nil {
|
|
||||||
log.Fatal("No config returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, ok := (*resp.Result).(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("Invalid response format")
|
|
||||||
}
|
|
||||||
|
|
||||||
output, _ := json.MarshalIndent(cfg, "", " ")
|
|
||||||
fmt.Println(string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClipConfigSet(cmd *cobra.Command, args []string) {
|
|
||||||
params := map[string]any{}
|
|
||||||
|
|
||||||
if cmd.Flags().Changed("max-history") {
|
|
||||||
params["maxHistory"] = clipConfigMaxHistory
|
|
||||||
}
|
|
||||||
if cmd.Flags().Changed("auto-clear-days") {
|
|
||||||
params["autoClearDays"] = clipConfigAutoClearDays
|
|
||||||
}
|
|
||||||
if clipConfigClearAtStartup {
|
|
||||||
params["clearAtStartup"] = true
|
|
||||||
}
|
|
||||||
if clipConfigNoClearStartup {
|
|
||||||
params["clearAtStartup"] = false
|
|
||||||
}
|
|
||||||
if clipConfigDisabled {
|
|
||||||
params["disabled"] = true
|
|
||||||
}
|
|
||||||
if clipConfigEnabled {
|
|
||||||
params["disabled"] = false
|
|
||||||
}
|
|
||||||
if clipConfigDisableHistory {
|
|
||||||
params["disableHistory"] = true
|
|
||||||
}
|
|
||||||
if clipConfigEnableHistory {
|
|
||||||
params["disableHistory"] = false
|
|
||||||
}
|
|
||||||
if clipConfigDisablePersist {
|
|
||||||
params["disablePersist"] = true
|
|
||||||
}
|
|
||||||
if clipConfigEnablePersist {
|
|
||||||
params["disablePersist"] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(params) == 0 {
|
|
||||||
fmt.Println("No config options specified")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := models.Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "clipboard.setConfig",
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to set config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Error != "" {
|
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Config updated")
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -121,7 +121,13 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyToClipboard(text string) {
|
func copyToClipboard(text string) {
|
||||||
if err := clipboard.CopyText(text); err != nil {
|
var cmd *exec.Cmd
|
||||||
fmt.Fprintln(os.Stderr, "clipboard copy failed:", err)
|
if _, err := exec.LookPath("wl-copy"); err == nil {
|
||||||
|
cmd = exec.Command("wl-copy", text)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = cmd.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,6 +513,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/matugen"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +49,6 @@ func init() {
|
|||||||
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
@@ -66,7 +68,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
||||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -81,7 +82,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
TerminalsAlwaysDark: terminalsAlwaysDark,
|
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||||
SkipTemplates: skipTemplates,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +97,33 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
wait, _ := cmd.Flags().GetBool("wait")
|
wait, _ := cmd.Flags().GetBool("wait")
|
||||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||||
|
|
||||||
request := models.Request{
|
socketPath := os.Getenv("DMS_SOCKET")
|
||||||
ID: 1,
|
if socketPath == "" {
|
||||||
Method: "matugen.queue",
|
var err error
|
||||||
Params: map[string]any{
|
socketPath, err = server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Info("No socket available, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Socket connection failed, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
request := map[string]any{
|
||||||
|
"id": 1,
|
||||||
|
"method": "matugen.queue",
|
||||||
|
"params": map[string]any{
|
||||||
"stateDir": opts.StateDir,
|
"stateDir": opts.StateDir,
|
||||||
"shellDir": opts.ShellDir,
|
"shellDir": opts.ShellDir,
|
||||||
"configDir": opts.ConfigDir,
|
"configDir": opts.ConfigDir,
|
||||||
@@ -113,19 +136,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"stockColors": opts.StockColors,
|
"stockColors": opts.StockColors,
|
||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !wait {
|
if !wait {
|
||||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
|
||||||
log.Info("Server unavailable, running synchronously")
|
|
||||||
if err := matugen.Run(opts); err != nil {
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("Theme generation queued")
|
fmt.Println("Theme generation queued")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -135,18 +154,17 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
resultCh := make(chan error, 1)
|
resultCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
resp, ok := tryServerRequest(request)
|
var response struct {
|
||||||
if !ok {
|
ID int `json:"id"`
|
||||||
log.Info("Server unavailable, running synchronously")
|
Result any `json:"result"`
|
||||||
if err := matugen.Run(opts); err != nil {
|
Error string `json:"error"`
|
||||||
resultCh <- err
|
}
|
||||||
return
|
if err := json.NewDecoder(conn).Decode(&response); err != nil {
|
||||||
}
|
resultCh <- fmt.Errorf("failed to read response: %w", err)
|
||||||
resultCh <- nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if response.Error != "" {
|
||||||
resultCh <- fmt.Errorf("server error: %s", resp.Error)
|
resultCh <- fmt.Errorf("server error: %s", response.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultCh <- nil
|
resultCh <- nil
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -90,6 +93,32 @@ func mimeTypeToCategories(mimeType string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runOpen(target string) {
|
func runOpen(target string) {
|
||||||
|
socketPath, err := server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket not found: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket connection failed: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
_, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if buf[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse file:// URIs to extract the actual file path
|
// Parse file:// URIs to extract the actual file path
|
||||||
actualTarget := target
|
actualTarget := target
|
||||||
detectedMimeType := openMimeType
|
detectedMimeType := openMimeType
|
||||||
@@ -190,9 +219,8 @@ func runOpen(target string) {
|
|||||||
|
|
||||||
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
||||||
|
|
||||||
if err := sendServerRequestFireAndForget(req); err != nil {
|
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||||
fmt.Println("DMS is not running. Please start DMS first.")
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Request sent successfully")
|
log.Infof("Request sent successfully")
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -257,7 +257,9 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return clipboard.Copy(data.Bytes(), mimeType)
|
cmd := exec.Command("wl-copy", "--type", mimeType)
|
||||||
|
cmd.Stdin = &data
|
||||||
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
|
||||||
socketPath := getServerSocketPath()
|
|
||||||
|
|
||||||
conn, err := net.Dial("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conn)
|
|
||||||
scanner.Scan() // discard initial capabilities message
|
|
||||||
|
|
||||||
reqData, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write(reqData); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write newline: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !scanner.Scan() {
|
|
||||||
return nil, fmt.Errorf("failed to read response")
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp models.Response[any]
|
|
||||||
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendServerRequestFireAndForget sends a request without waiting for a response.
|
|
||||||
// Useful for commands that trigger UI or async operations.
|
|
||||||
func sendServerRequestFireAndForget(req models.Request) error {
|
|
||||||
socketPath := getServerSocketPath()
|
|
||||||
|
|
||||||
conn, err := net.Dial("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conn)
|
|
||||||
scanner.Scan() // discard initial capabilities message
|
|
||||||
|
|
||||||
reqData, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write(reqData); err != nil {
|
|
||||||
return fmt.Errorf("failed to write request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
|
||||||
return fmt.Errorf("failed to write newline: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryServerRequest attempts to send a request but returns false if server unavailable.
|
|
||||||
// Does not log errors - caller can decide what to do on failure.
|
|
||||||
func tryServerRequest(req models.Request) (*models.Response[any], bool) {
|
|
||||||
resp, err := sendServerRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return resp, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServerSocketPath() string {
|
|
||||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
|
||||||
if runtimeDir == "" {
|
|
||||||
runtimeDir = os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(runtimeDir)
|
|
||||||
if err != nil {
|
|
||||||
return filepath.Join(runtimeDir, "danklinux.sock")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
name := entry.Name()
|
|
||||||
if name == "danklinux.sock" {
|
|
||||||
return filepath.Join(runtimeDir, name)
|
|
||||||
}
|
|
||||||
if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" {
|
|
||||||
return filepath.Join(runtimeDir, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return server.GetSocketPath()
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,7 @@ require (
|
|||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.etcd.io/bbolt v1.4.3
|
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||||
golang.org/x/image v0.34.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -67,6 +65,6 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
10
core/go.sum
10
core/go.sum
@@ -131,26 +131,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
|
||||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ NC='\033[0m' # No Color
|
|||||||
|
|
||||||
# Check for root privileges
|
# Check for root privileges
|
||||||
if [ "$(id -u)" == "0" ]; then
|
if [ "$(id -u)" == "0" ]; then
|
||||||
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if running on Linux
|
# Check if running on Linux
|
||||||
@@ -22,17 +22,17 @@ fi
|
|||||||
# Detect architecture
|
# Detect architecture
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
x86_64)
|
x86_64)
|
||||||
ARCH="amd64"
|
ARCH="amd64"
|
||||||
;;
|
;;
|
||||||
aarch64)
|
aarch64)
|
||||||
ARCH="arm64"
|
ARCH="arm64"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
||||||
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Get the latest release version
|
# Get the latest release version
|
||||||
@@ -55,7 +55,7 @@ curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LAT
|
|||||||
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
||||||
|
|
||||||
# Get the expected checksum
|
# Get the expected checksum
|
||||||
EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256)
|
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
|
||||||
|
|
||||||
# Calculate actual checksum
|
# Calculate actual checksum
|
||||||
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
||||||
@@ -67,7 +67,7 @@ if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
|
|||||||
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
||||||
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
||||||
printf "The downloaded file may be corrupted or tampered with\n"
|
printf "The downloaded file may be corrupted or tampered with\n"
|
||||||
cd - >/dev/null
|
cd - > /dev/null
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -82,5 +82,5 @@ printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
|||||||
./installer
|
./installer
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
cd - >/dev/null
|
cd - > /dev/null
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
|
||||||
return CopyOpts(data, mimeType, false, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
return copyServe(data, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := stdin.Write(data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyServe(data []byte, mimeType string, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := source.Offer(mimeType); err != nil {
|
|
||||||
return fmt.Errorf("offer mime type: %w", 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
|
||||||
pasted := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
defer syscall.Close(e.Fd)
|
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
|
||||||
defer file.Close()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyText(text string) error {
|
|
||||||
return Copy([]byte(text), "text/plain;charset=utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paste() ([]byte, string, error) {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
ctx := display.Context()
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
|
||||||
|
|
||||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerMimeTypes[e.Id] = nil
|
|
||||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
|
||||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var selectionOffer *ext_data_control.ExtDataControlOfferV1
|
|
||||||
gotSelection := false
|
|
||||||
|
|
||||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
|
||||||
selectionOffer = e.Id
|
|
||||||
gotSelection = true
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeTypes := offerMimeTypes[selectionOffer]
|
|
||||||
selectedMime := selectPreferredMimeType(mimeTypes)
|
|
||||||
if selectedMime == "" {
|
|
||||||
return nil, "", fmt.Errorf("no supported mime type")
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("create pipe: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
|
|
||||||
w.Close()
|
|
||||||
return nil, "", fmt.Errorf("receive: %w", err)
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("read: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, selectedMime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PasteText() (string, error) {
|
|
||||||
data, _, err := Paste()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectPreferredMimeType(mimes []string) string {
|
|
||||||
preferred := []string{
|
|
||||||
"text/plain;charset=utf-8",
|
|
||||||
"text/plain",
|
|
||||||
"UTF8_STRING",
|
|
||||||
"STRING",
|
|
||||||
"TEXT",
|
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pref := range preferred {
|
|
||||||
for _, mime := range mimes {
|
|
||||||
if mime == pref {
|
|
||||||
return mime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mimes) > 0 {
|
|
||||||
return mimes[0]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsImageMimeType(mime string) bool {
|
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "golang.org/x/image/bmp"
|
|
||||||
_ "golang.org/x/image/tiff"
|
|
||||||
"hash/fnv"
|
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StoreConfig struct {
|
|
||||||
MaxHistory int
|
|
||||||
MaxEntrySize int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultStoreConfig() StoreConfig {
|
|
||||||
return StoreConfig{
|
|
||||||
MaxHistory: 100,
|
|
||||||
MaxEntrySize: 5 * 1024 * 1024,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry struct {
|
|
||||||
ID uint64
|
|
||||||
Data []byte
|
|
||||||
MimeType string
|
|
||||||
Preview string
|
|
||||||
Size int
|
|
||||||
Timestamp time.Time
|
|
||||||
IsImage bool
|
|
||||||
Hash uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func Store(data []byte, mimeType string) error {
|
|
||||||
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if int64(len(data)) > cfg.MaxEntrySize {
|
|
||||||
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath, err := getDBPath()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get db path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open db: %w", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
entry := Entry{
|
|
||||||
Data: data,
|
|
||||||
MimeType: mimeType,
|
|
||||||
Size: len(data),
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
IsImage: IsImageMimeType(mimeType),
|
|
||||||
Hash: computeHash(data),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case entry.IsImage:
|
|
||||||
entry.Preview = imagePreview(data, mimeType)
|
|
||||||
default:
|
|
||||||
entry.Preview = textPreview(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Update(func(tx *bolt.Tx) error {
|
|
||||||
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := b.NextSequence()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
entry.ID = id
|
|
||||||
|
|
||||||
encoded, err := encodeEntry(entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Put(itob(id), encoded); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimLengthInTx(b, cfg.MaxHistory)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDBPath() (string, error) {
|
|
||||||
cacheDir, err := os.UserCacheDir()
|
|
||||||
if err != nil {
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
cacheDir = filepath.Join(homeDir, ".cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
dbDir := filepath.Join(cacheDir, "dms-clipboard")
|
|
||||||
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(dbDir, "db"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|
||||||
c := b.Cursor()
|
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
|
||||||
if extractHash(v) != hash {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := b.Delete(k); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
|
|
||||||
c := b.Cursor()
|
|
||||||
var count int
|
|
||||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
|
||||||
if count < maxHistory {
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := b.Delete(k); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeEntry(e Entry) ([]byte, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
binary.Write(buf, binary.BigEndian, e.ID)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
|
|
||||||
buf.Write(e.Data)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
|
|
||||||
buf.WriteString(e.MimeType)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
|
|
||||||
buf.WriteString(e.Preview)
|
|
||||||
binary.Write(buf, binary.BigEndian, int32(e.Size))
|
|
||||||
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
|
|
||||||
if e.IsImage {
|
|
||||||
buf.WriteByte(1)
|
|
||||||
} else {
|
|
||||||
buf.WriteByte(0)
|
|
||||||
}
|
|
||||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func itob(v uint64) []byte {
|
|
||||||
b := make([]byte, 8)
|
|
||||||
binary.BigEndian.PutUint64(b, v)
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeHash(data []byte) uint64 {
|
|
||||||
h := fnv.New64a()
|
|
||||||
h.Write(data)
|
|
||||||
return h.Sum64()
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractHash(data []byte) uint64 {
|
|
||||||
if len(data) < 8 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func textPreview(data []byte) string {
|
|
||||||
text := string(data)
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
text = strings.Join(strings.Fields(text), " ")
|
|
||||||
|
|
||||||
if len(text) > 100 {
|
|
||||||
return text[:100] + "…"
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
func imagePreview(data []byte, format string) string {
|
|
||||||
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sizeStr(size int) string {
|
|
||||||
units := []string{"B", "KiB", "MiB"}
|
|
||||||
var i int
|
|
||||||
fsize := float64(size)
|
|
||||||
for fsize >= 1024 && i < len(units)-1 {
|
|
||||||
fsize /= 1024
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.0f %s", fsize, units[i])
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClipboardChange struct {
|
|
||||||
Data []byte
|
|
||||||
MimeType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
wlCtx := 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(wlCtx)
|
|
||||||
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(wlCtx)
|
|
||||||
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()
|
|
||||||
|
|
||||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
|
||||||
|
|
||||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerMimeTypes[e.Id] = nil
|
|
||||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
|
||||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mimes := offerMimeTypes[e.Id]
|
|
||||||
selectedMime := selectPreferredMimeType(mimes)
|
|
||||||
if selectedMime == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
|
||||||
w.Close()
|
|
||||||
r.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer r.Close()
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback(data, selectedMime)
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
|
||||||
}
|
|
||||||
if err := wlCtx.Dispatch(); err != nil && err != os.ErrDeadlineExceeded {
|
|
||||||
return fmt.Errorf("dispatch: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
|
||||||
ch := make(chan ClipboardChange, 16)
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(ch)
|
|
||||||
err := Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
select {
|
|
||||||
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil && err != context.Canceled {
|
|
||||||
errCh <- err
|
|
||||||
}
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
return ch, errCh
|
|
||||||
}
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
package colorpicker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s.SetScale(int32(id%3 + 1))
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
scale := s.Scale()
|
|
||||||
assert.GreaterOrEqual(t, scale, int32(1))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
w, h := s.LogicalSize()
|
|
||||||
_ = w
|
|
||||||
_ = h
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/3; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s.OnPointerButton(0x110, 1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/3; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s.OnKey(1, 1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/3; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
picked, cancelled := s.IsDone()
|
|
||||||
_ = picked
|
|
||||||
_ = cancelled
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
_ = s.IsReady()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s.SwapBuffers()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ZeroScale(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
s.SetScale(0)
|
|
||||||
assert.Equal(t, int32(1), s.Scale())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_NegativeScale(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
s.SetScale(-5)
|
|
||||||
assert.Equal(t, int32(1), s.Scale())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ZeroDimensionConfigure(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
|
|
||||||
err := s.OnLayerConfigure(0, 100)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = s.OnLayerConfigure(100, 0)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = s.OnLayerConfigure(-1, 100)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
w, h := s.LogicalSize()
|
|
||||||
assert.Equal(t, 0, w)
|
|
||||||
assert.Equal(t, 0, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
color, ok := s.PickColor()
|
|
||||||
assert.False(t, ok)
|
|
||||||
assert.Equal(t, Color{}, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
buf := s.Redraw()
|
|
||||||
assert.Nil(t, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
buf := s.RedrawScreenOnly()
|
|
||||||
assert.Nil(t, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_FrontRenderBufferNil(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
buf := s.FrontRenderBuffer()
|
|
||||||
assert.Nil(t, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_ScreenBufferNil(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
buf := s.ScreenBuffer()
|
|
||||||
assert.Nil(t, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSurfaceState_DestroyMultipleTimes(t *testing.T) {
|
|
||||||
s := NewSurfaceState(FormatHex, false)
|
|
||||||
s.Destroy()
|
|
||||||
s.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClamp(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
v, lo, hi, expected int
|
|
||||||
}{
|
|
||||||
{5, 0, 10, 5},
|
|
||||||
{-5, 0, 10, 0},
|
|
||||||
{15, 0, 10, 10},
|
|
||||||
{0, 0, 10, 0},
|
|
||||||
{10, 0, 10, 10},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := clamp(tt.v, tt.lo, tt.hi)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClampF(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
v, lo, hi, expected float64
|
|
||||||
}{
|
|
||||||
{5.0, 0.0, 10.0, 5.0},
|
|
||||||
{-5.0, 0.0, 10.0, 0.0},
|
|
||||||
{15.0, 0.0, 10.0, 10.0},
|
|
||||||
{0.0, 0.0, 10.0, 0.0},
|
|
||||||
{10.0, 0.0, 10.0, 10.0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := clampF(tt.v, tt.lo, tt.hi)
|
|
||||||
assert.InDelta(t, tt.expected, result, 0.001)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAbs(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
v, expected int
|
|
||||||
}{
|
|
||||||
{5, 5},
|
|
||||||
{-5, 5},
|
|
||||||
{0, 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := abs(tt.v)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlendColors(t *testing.T) {
|
|
||||||
bg := Color{R: 0, G: 0, B: 0, A: 255}
|
|
||||||
fg := Color{R: 255, G: 255, B: 255, A: 255}
|
|
||||||
|
|
||||||
result := blendColors(bg, fg, 0.0)
|
|
||||||
assert.Equal(t, bg.R, result.R)
|
|
||||||
assert.Equal(t, bg.G, result.G)
|
|
||||||
assert.Equal(t, bg.B, result.B)
|
|
||||||
|
|
||||||
result = blendColors(bg, fg, 1.0)
|
|
||||||
assert.Equal(t, fg.R, result.R)
|
|
||||||
assert.Equal(t, fg.G, result.G)
|
|
||||||
assert.Equal(t, fg.B, result.B)
|
|
||||||
|
|
||||||
result = blendColors(bg, fg, 0.5)
|
|
||||||
assert.InDelta(t, 127, int(result.R), 1)
|
|
||||||
assert.InDelta(t, 127, int(result.G), 1)
|
|
||||||
assert.InDelta(t, 127, int(result.B), 1)
|
|
||||||
|
|
||||||
result = blendColors(bg, fg, -1.0)
|
|
||||||
assert.Equal(t, bg.R, result.R)
|
|
||||||
|
|
||||||
result = blendColors(bg, fg, 2.0)
|
|
||||||
assert.Equal(t, fg.R, result.R)
|
|
||||||
}
|
|
||||||
@@ -615,11 +615,10 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
|
|||||||
|
|
||||||
spawnDms := `spawn-at-startup "dms" "run"`
|
spawnDms := `spawn-at-startup "dms" "run"`
|
||||||
if !strings.Contains(config, spawnDms) {
|
if !strings.Contains(config, spawnDms) {
|
||||||
// Insert spawn-at-startup for dms after the environment block
|
config = strings.Replace(config,
|
||||||
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
|
||||||
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
|
||||||
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
|
1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LocateDMSConfig() (string, error) {
|
func LocateDMSConfig() (string, error) {
|
||||||
var primaryPaths []string
|
var primaryPaths []string
|
||||||
|
|
||||||
configHome, err := os.UserConfigDir()
|
configHome := utils.XDGConfigHome()
|
||||||
if err == nil && configHome != "" {
|
if configHome != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ monitor = , preferred,auto,auto
|
|||||||
# ==================
|
# ==================
|
||||||
exec-once = dbus-update-activation-environment --systemd --all
|
exec-once = dbus-update-activation-environment --systemd --all
|
||||||
exec-once = systemctl --user start hyprland-session.target
|
exec-once = systemctl --user start hyprland-session.target
|
||||||
|
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# INPUT CONFIG
|
# INPUT CONFIG
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
highlight {
|
highlight {
|
||||||
corner-radius 12
|
corner-radius 12
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
// === System & Overview ===
|
// === System & Overview ===
|
||||||
Mod+D repeat=false { toggle-overview; }
|
Mod+D repeat=false { toggle-overview; }
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
background-color "transparent"
|
background-color "transparent"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
gaps 4
|
gaps 4
|
||||||
|
|
||||||
|
|||||||
@@ -18,64 +18,15 @@ gestures {
|
|||||||
input {
|
input {
|
||||||
keyboard {
|
keyboard {
|
||||||
xkb {
|
xkb {
|
||||||
// You can set rules, model, layout, variant and options.
|
|
||||||
// For more information, see xkeyboard-config(7).
|
|
||||||
|
|
||||||
// For example:
|
|
||||||
// layout "us,ru"
|
|
||||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
|
||||||
|
|
||||||
// If this section is empty, niri will fetch xkb settings
|
|
||||||
// from org.freedesktop.locale1. You can control these using
|
|
||||||
// localectl set-x11-keymap.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable numlock on startup, omitting this setting disables it.
|
|
||||||
numlock
|
numlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next sections include libinput settings.
|
|
||||||
// Omitting settings disables them, or leaves them at their default values.
|
|
||||||
// All commented-out settings here are examples, not defaults.
|
|
||||||
touchpad {
|
touchpad {
|
||||||
// off
|
|
||||||
tap
|
|
||||||
// dwt
|
|
||||||
// dwtp
|
|
||||||
// drag false
|
|
||||||
// drag-lock
|
|
||||||
natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "two-finger"
|
|
||||||
// disabled-on-external-mouse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse {
|
mouse {
|
||||||
// off
|
|
||||||
// natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "no-scroll"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackpoint {
|
trackpoint {
|
||||||
// off
|
|
||||||
// natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "on-button-down"
|
|
||||||
// scroll-button 273
|
|
||||||
// scroll-button-lock
|
|
||||||
// middle-emulation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
|
||||||
// warp-mouse-to-focus
|
|
||||||
|
|
||||||
// Focus windows and outputs automatically when moving the mouse into them.
|
|
||||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
|
||||||
// focus-follows-mouse max-scroll-amount="0%"
|
|
||||||
}
|
}
|
||||||
// You can configure outputs by their name, which you can find
|
// You can configure outputs by their name, which you can find
|
||||||
// by running `niri msg outputs` while inside a niri instance.
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
@@ -158,6 +109,7 @@ overview {
|
|||||||
// which may be more convenient to use.
|
// which may be more convenient to use.
|
||||||
// See the binds section below for more spawn examples.
|
// See the binds section below for more spawn examples.
|
||||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
|
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||||
environment {
|
environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,8 +103,10 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
|
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -137,6 +139,8 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", 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},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,37 @@ func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
cliphist := deps.StatusMissing
|
||||||
|
if b.commandExists("cliphist") {
|
||||||
|
cliphist = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
wlClipboard := deps.StatusMissing
|
||||||
|
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||||
|
wlClipboard = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies,
|
||||||
|
deps.Dependency{
|
||||||
|
Name: "cliphist",
|
||||||
|
Status: cliphist,
|
||||||
|
Description: "Wayland clipboard manager",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
deps.Dependency{
|
||||||
|
Name: "wl-clipboard",
|
||||||
|
Status: wlClipboard,
|
||||||
|
Description: "Wayland clipboard utilities",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
var dependencies []deps.Dependency
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ 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.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -101,6 +102,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", 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},
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
"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"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +549,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,10 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
|
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -115,12 +117,14 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", 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},
|
||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"cliphist": {Name: "cliphist", 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"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ 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.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -139,6 +140,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
|
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
@@ -149,6 +151,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
|
|
||||||
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
|
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install hyprland: %w", err)
|
return fmt.Errorf("failed to install hyprland: %w", err)
|
||||||
}
|
}
|
||||||
|
case "hyprpicker":
|
||||||
|
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
|
}
|
||||||
case "ghostty":
|
case "ghostty":
|
||||||
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
@@ -82,6 +86,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install matugen: %w", err)
|
return fmt.Errorf("failed to install matugen: %w", err)
|
||||||
}
|
}
|
||||||
|
case "cliphist":
|
||||||
|
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||||
|
}
|
||||||
case "xwayland-satellite":
|
case "xwayland-satellite":
|
||||||
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||||
@@ -397,6 +405,184 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing hyprpicker from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install hyprutils first
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Building hyprutils dependency...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
|
||||||
|
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create hyprutils directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(hyprutilsDir)
|
||||||
|
|
||||||
|
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
|
||||||
|
if err := cloneUtilsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"--no-warn-unused-cli",
|
||||||
|
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
||||||
|
"-DBUILD_TESTING=off",
|
||||||
|
"-S", ".",
|
||||||
|
"-B", "./build")
|
||||||
|
configureUtilsCmd.Dir = hyprutilsDir
|
||||||
|
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
|
||||||
|
buildUtilsCmd.Dir = hyprutilsDir
|
||||||
|
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
|
installUtilsCmd.Dir = hyprutilsDir
|
||||||
|
if err := installUtilsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install hyprwayland-scanner
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.2,
|
||||||
|
Step: "Building hyprwayland-scanner dependency...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
|
||||||
|
if err := os.MkdirAll(scannerDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create scanner directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(scannerDir)
|
||||||
|
|
||||||
|
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
|
||||||
|
if err := cloneScannerCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureScannerCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX=/usr",
|
||||||
|
"-B", "build")
|
||||||
|
configureScannerCmd.Dir = scannerDir
|
||||||
|
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
|
||||||
|
buildScannerCmd.Dir = scannerDir
|
||||||
|
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
|
installScannerCmd.Dir = scannerDir
|
||||||
|
if err := installScannerCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now build hyprpicker
|
||||||
|
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: "Cloning hyprpicker repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.45,
|
||||||
|
Step: "Configuring hyprpicker build...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
|
||||||
|
}
|
||||||
|
|
||||||
|
configureCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"--no-warn-unused-cli",
|
||||||
|
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
||||||
|
"-S", ".",
|
||||||
|
"-B", "./build")
|
||||||
|
configureCmd.Dir = tmpDir
|
||||||
|
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
|
||||||
|
output, err := configureCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||||
|
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.55,
|
||||||
|
Step: "Building hyprpicker...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake --build build --target hyprpicker",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing hyprpicker...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cmake --install build",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("hyprpicker installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing Ghostty from source...")
|
m.log("Installing Ghostty from source...")
|
||||||
|
|
||||||
@@ -617,6 +803,52 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing cliphist from source...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Installing cliphist via go install...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
||||||
|
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
||||||
|
targetPath := "/usr/local/bin/cliphist"
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.7,
|
||||||
|
Step: "Installing cliphist binary to system...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("cliphist installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing xwayland-satellite from source...")
|
m.log("Installing xwayland-satellite from source...")
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,10 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
|
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -105,8 +107,10 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", 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},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
|
|||||||
@@ -76,8 +76,10 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
|
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
@@ -110,6 +112,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", 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},
|
||||||
|
|
||||||
@@ -118,6 +121,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
"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"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +539,8 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
buildDeps["libpam0g-dev"] = true
|
buildDeps["libpam0g-dev"] = true
|
||||||
case "matugen":
|
case "matugen":
|
||||||
buildDeps["curl"] = true
|
buildDeps["curl"] = true
|
||||||
|
case "cliphist":
|
||||||
|
// Go will be installed separately with PPA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +550,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
|||||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||||
case "niri":
|
case "niri":
|
||||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||||
case "kitty", "alacritty", "ghostty":
|
case "kitty", "alacritty", "ghostty", "hyprpicker":
|
||||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||||
default:
|
default:
|
||||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ type DiscoveryConfig struct {
|
|||||||
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
||||||
var searchPaths []string
|
var searchPaths []string
|
||||||
|
|
||||||
configDir, err := os.UserConfigDir()
|
configHome := utils.XDGConfigHome()
|
||||||
if err == nil && configDir != "" {
|
if configHome != "" {
|
||||||
searchPaths = append(searchPaths, filepath.Join(configDir, "DankMaterialShell", "cheatsheets"))
|
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
|
||||||
}
|
}
|
||||||
|
|
||||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
@@ -30,11 +31,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultNiriConfigDir() string {
|
func defaultNiriConfigDir() string {
|
||||||
configDir, err := os.UserConfigDir()
|
return filepath.Join(utils.XDGConfigHome(), "niri")
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return filepath.Join(configDir, "niri")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) Name() string {
|
func (n *NiriProvider) Name() string {
|
||||||
@@ -461,16 +458,9 @@ func (n *NiriProvider) getBindSortPriority(action string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dmsWarningHeader = `// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
||||||
if len(binds) == 0 {
|
if len(binds) == 0 {
|
||||||
return dmsWarningHeader + "binds {}\n"
|
return "binds {}\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
var regularBinds, recentWindowsBinds []*overrideBind
|
var regularBinds, recentWindowsBinds []*overrideBind
|
||||||
@@ -497,7 +487,6 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
sb.WriteString(dmsWarningHeader)
|
|
||||||
sb.WriteString("binds {\n")
|
sb.WriteString("binds {\n")
|
||||||
for _, bind := range regularBinds {
|
for _, bind := range regularBinds {
|
||||||
n.writeBindNode(&sb, bind, " ")
|
n.writeBindNode(&sb, bind, " ")
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testHeader = `// ! DO NOT EDIT !
|
|
||||||
// ! AUTO-GENERATED BY DMS !
|
|
||||||
// ! CHANGES WILL BE OVERWRITTEN !
|
|
||||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestNiriProviderName(t *testing.T) {
|
func TestNiriProviderName(t *testing.T) {
|
||||||
provider := NewNiriProvider("")
|
provider := NewNiriProvider("")
|
||||||
if provider.Name() != "niri" {
|
if provider.Name() != "niri" {
|
||||||
@@ -204,7 +197,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "empty binds",
|
name: "empty binds",
|
||||||
binds: map[string]*overrideBind{},
|
binds: map[string]*overrideBind{},
|
||||||
expected: testHeader + "binds {}\n",
|
expected: "binds {}\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple spawn bind",
|
name: "simple spawn bind",
|
||||||
@@ -215,7 +208,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Description: "Open Terminal",
|
Description: "Open Terminal",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -229,7 +222,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Description: "Application Launcher",
|
Description: "Application Launcher",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `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"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -243,7 +236,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Options: map[string]any{"allow-when-locked": true},
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -257,7 +250,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Description: "Close Window",
|
Description: "Close Window",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -270,7 +263,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
Action: "next-window",
|
Action: "next-window",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
}
|
}
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
@@ -422,7 +415,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Focus Workspace 1",
|
Description: "Focus Workspace 1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -436,7 +429,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Focus Workspace 10",
|
Description: "Focus Workspace 10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -450,7 +443,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Adjust Column Width -10%",
|
Description: "Adjust Column Width -10%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -464,7 +457,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
|||||||
Description: "Adjust Column Width +10%",
|
Description: "Adjust Column Width +10%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: testHeader + `binds {
|
expected: `binds {
|
||||||
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -493,7 +486,7 @@ func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := provider.generateBindsContent(binds)
|
content := provider.generateBindsContent(binds)
|
||||||
expected := testHeader + `binds {
|
expected := `binds {
|
||||||
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -514,7 +507,7 @@ func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := provider.generateBindsContent(binds)
|
content := provider.generateBindsContent(binds)
|
||||||
expected := testHeader + `binds {
|
expected := `binds {
|
||||||
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -535,7 +528,7 @@ func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := provider.generateBindsContent(binds)
|
content := provider.generateBindsContent(binds)
|
||||||
expected := testHeader + `binds {
|
expected := `binds {
|
||||||
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type Options struct {
|
|||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
SkipTemplates string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColorsOutput struct {
|
type ColorsOutput struct {
|
||||||
@@ -48,18 +47,6 @@ func (o *Options) ColorsOutput() string {
|
|||||||
return filepath.Join(o.StateDir, "dms-colors.json")
|
return filepath.Join(o.StateDir, "dms-colors.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) ShouldSkipTemplate(name string) bool {
|
|
||||||
if o.SkipTemplates == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, skip := range strings.Split(o.SkipTemplates, ",") {
|
|
||||||
if strings.TrimSpace(skip) == name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(opts Options) error {
|
func Run(opts Options) error {
|
||||||
if opts.StateDir == "" {
|
if opts.StateDir == "" {
|
||||||
return fmt.Errorf("state-dir is required")
|
return fmt.Errorf("state-dir is required")
|
||||||
@@ -231,66 +218,34 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("gtk") {
|
switch opts.Mode {
|
||||||
switch opts.Mode {
|
case "light":
|
||||||
case "light":
|
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
||||||
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
default:
|
||||||
default:
|
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
|
||||||
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("niri") {
|
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
||||||
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
||||||
}
|
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
||||||
if !opts.ShouldSkipTemplate("qt5ct") {
|
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
||||||
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
||||||
}
|
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
||||||
if !opts.ShouldSkipTemplate("qt6ct") {
|
|
||||||
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
|
||||||
}
|
|
||||||
if !opts.ShouldSkipTemplate("firefox") {
|
|
||||||
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
|
||||||
}
|
|
||||||
if !opts.ShouldSkipTemplate("pywalfox") {
|
|
||||||
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
|
||||||
}
|
|
||||||
if !opts.ShouldSkipTemplate("vesktop") {
|
|
||||||
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("ghostty") {
|
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
||||||
}
|
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
||||||
if !opts.ShouldSkipTemplate("kitty") {
|
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
||||||
}
|
|
||||||
if !opts.ShouldSkipTemplate("foot") {
|
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
|
||||||
}
|
|
||||||
if !opts.ShouldSkipTemplate("alacritty") {
|
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
|
||||||
}
|
|
||||||
if !opts.ShouldSkipTemplate("wezterm") {
|
|
||||||
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("dgop") {
|
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
||||||
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.ShouldSkipTemplate("kcolorscheme") {
|
homeDir, _ := os.UserHomeDir()
|
||||||
appendConfig(opts, cfgFile, "skip", "kcolorscheme.toml")
|
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
}
|
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
if !opts.ShouldSkipTemplate("vscode") {
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
homeDir, _ := os.UserHomeDir()
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
|
||||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
|
||||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
|
||||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
|
||||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.RunUserTemplates {
|
if opts.RunUserTemplates {
|
||||||
if data, err := os.ReadFile(userConfigPath); err == nil {
|
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
|
||||||
|
|
||||||
package mocks_wlclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockWaylandDisplay is an autogenerated mock type for the WaylandDisplay type
|
|
||||||
type MockWaylandDisplay struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockWaylandDisplay_Expecter struct {
|
|
||||||
mock *mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_m *MockWaylandDisplay) EXPECT() *MockWaylandDisplay_Expecter {
|
|
||||||
return &MockWaylandDisplay_Expecter{mock: &_m.Mock}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) Context() *client.Context {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Context")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 *client.Context
|
|
||||||
if rf, ok := ret.Get(0).(func() *client.Context); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*client.Context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context'
|
|
||||||
type MockWaylandDisplay_Context_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) Context() *MockWaylandDisplay_Context_Call {
|
|
||||||
return &MockWaylandDisplay_Context_Call{Call: _e.mock.On("Context")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Context_Call) Run(run func()) *MockWaylandDisplay_Context_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Context_Call) Return(_a0 *client.Context) *MockWaylandDisplay_Context_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Context_Call) RunAndReturn(run func() *client.Context) *MockWaylandDisplay_Context_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) Destroy() error {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Destroy")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func() error); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_Destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Destroy'
|
|
||||||
type MockWaylandDisplay_Destroy_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) Destroy() *MockWaylandDisplay_Destroy_Call {
|
|
||||||
return &MockWaylandDisplay_Destroy_Call{Call: _e.mock.On("Destroy")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Destroy_Call) Run(run func()) *MockWaylandDisplay_Destroy_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Destroy_Call) Return(_a0 error) *MockWaylandDisplay_Destroy_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Destroy_Call) RunAndReturn(run func() error) *MockWaylandDisplay_Destroy_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistry provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) GetRegistry() (*client.Registry, error) {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for GetRegistry")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 *client.Registry
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(0).(func() (*client.Registry, error)); ok {
|
|
||||||
return rf()
|
|
||||||
}
|
|
||||||
if rf, ok := ret.Get(0).(func() *client.Registry); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*client.Registry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
|
||||||
r1 = rf()
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_GetRegistry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRegistry'
|
|
||||||
type MockWaylandDisplay_GetRegistry_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistry is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) GetRegistry() *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
return &MockWaylandDisplay_GetRegistry_Call{Call: _e.mock.On("GetRegistry")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_GetRegistry_Call) Run(run func()) *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_GetRegistry_Call) Return(_a0 *client.Registry, _a1 error) *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
_c.Call.Return(_a0, _a1)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_GetRegistry_Call) RunAndReturn(run func() (*client.Registry, error)) *MockWaylandDisplay_GetRegistry_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roundtrip provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandDisplay) Roundtrip() error {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Roundtrip")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func() error); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandDisplay_Roundtrip_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Roundtrip'
|
|
||||||
type MockWaylandDisplay_Roundtrip_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roundtrip is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandDisplay_Expecter) Roundtrip() *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
return &MockWaylandDisplay_Roundtrip_Call{Call: _e.mock.On("Roundtrip")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Roundtrip_Call) Run(run func()) *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Roundtrip_Call) Return(_a0 error) *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandDisplay_Roundtrip_Call) RunAndReturn(run func() error) *MockWaylandDisplay_Roundtrip_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockWaylandDisplay creates a new instance of MockWaylandDisplay. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
|
||||||
// The first argument is typically a *testing.T value.
|
|
||||||
func NewMockWaylandDisplay(t interface {
|
|
||||||
mock.TestingT
|
|
||||||
Cleanup(func())
|
|
||||||
}) *MockWaylandDisplay {
|
|
||||||
mock := &MockWaylandDisplay{}
|
|
||||||
mock.Mock.Test(t)
|
|
||||||
|
|
||||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
|
||||||
|
|
||||||
package mocks_wlcontext
|
|
||||||
|
|
||||||
import (
|
|
||||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockWaylandContext is an autogenerated mock type for the WaylandContext type
|
|
||||||
type MockWaylandContext struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockWaylandContext_Expecter struct {
|
|
||||||
mock *mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_m *MockWaylandContext) EXPECT() *MockWaylandContext_Expecter {
|
|
||||||
return &MockWaylandContext_Expecter{mock: &_m.Mock}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandContext) Close() {
|
|
||||||
_m.Called()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandContext_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
|
||||||
type MockWaylandContext_Close_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandContext_Expecter) Close() *MockWaylandContext_Close_Call {
|
|
||||||
return &MockWaylandContext_Close_Call{Call: _e.mock.On("Close")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Close_Call) Run(run func()) *MockWaylandContext_Close_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Close_Call) Return() *MockWaylandContext_Close_Call {
|
|
||||||
_c.Call.Return()
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Close_Call) RunAndReturn(run func()) *MockWaylandContext_Close_Call {
|
|
||||||
_c.Run(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandContext) Display() *client.Display {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for Display")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 *client.Display
|
|
||||||
if rf, ok := ret.Get(0).(func() *client.Display); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*client.Display)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandContext_Display_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Display'
|
|
||||||
type MockWaylandContext_Display_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandContext_Expecter) Display() *MockWaylandContext_Display_Call {
|
|
||||||
return &MockWaylandContext_Display_Call{Call: _e.mock.On("Display")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Display_Call) Run(run func()) *MockWaylandContext_Display_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Display_Call) Return(_a0 *client.Display) *MockWaylandContext_Display_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Display_Call) RunAndReturn(run func() *client.Display) *MockWaylandContext_Display_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// FatalError provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandContext) FatalError() <-chan error {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for FatalError")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 <-chan error
|
|
||||||
if rf, ok := ret.Get(0).(func() <-chan error); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(<-chan error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandContext_FatalError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FatalError'
|
|
||||||
type MockWaylandContext_FatalError_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// FatalError is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandContext_Expecter) FatalError() *MockWaylandContext_FatalError_Call {
|
|
||||||
return &MockWaylandContext_FatalError_Call{Call: _e.mock.On("FatalError")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_FatalError_Call) Run(run func()) *MockWaylandContext_FatalError_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_FatalError_Call) Return(_a0 <-chan error) *MockWaylandContext_FatalError_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_FatalError_Call) RunAndReturn(run func() <-chan error) *MockWaylandContext_FatalError_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post provides a mock function with given fields: fn
|
|
||||||
func (_m *MockWaylandContext) Post(fn func()) {
|
|
||||||
_m.Called(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandContext_Post_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Post'
|
|
||||||
type MockWaylandContext_Post_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post is a helper method to define mock.On call
|
|
||||||
// - fn func()
|
|
||||||
func (_e *MockWaylandContext_Expecter) Post(fn interface{}) *MockWaylandContext_Post_Call {
|
|
||||||
return &MockWaylandContext_Post_Call{Call: _e.mock.On("Post", fn)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Post_Call) Run(run func(fn func())) *MockWaylandContext_Post_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(func()))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Post_Call) Return() *MockWaylandContext_Post_Call {
|
|
||||||
_c.Call.Return()
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Post_Call) RunAndReturn(run func(func())) *MockWaylandContext_Post_Call {
|
|
||||||
_c.Run(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start provides a mock function with no fields
|
|
||||||
func (_m *MockWaylandContext) Start() {
|
|
||||||
_m.Called()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockWaylandContext_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start'
|
|
||||||
type MockWaylandContext_Start_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start is a helper method to define mock.On call
|
|
||||||
func (_e *MockWaylandContext_Expecter) Start() *MockWaylandContext_Start_Call {
|
|
||||||
return &MockWaylandContext_Start_Call{Call: _e.mock.On("Start")}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Start_Call) Run(run func()) *MockWaylandContext_Start_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run()
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Start_Call) Return() *MockWaylandContext_Start_Call {
|
|
||||||
_c.Call.Return()
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockWaylandContext_Start_Call) RunAndReturn(run func()) *MockWaylandContext_Start_Call {
|
|
||||||
_c.Run(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockWaylandContext creates a new instance of MockWaylandContext. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
|
||||||
// The first argument is typically a *testing.T value.
|
|
||||||
func NewMockWaylandContext(t interface {
|
|
||||||
mock.TestingT
|
|
||||||
Cleanup(func())
|
|
||||||
}) *MockWaylandContext {
|
|
||||||
mock := &MockWaylandContext{}
|
|
||||||
mock.Mock.Test(t)
|
|
||||||
|
|
||||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,12 +33,7 @@ func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getPluginsDir() string {
|
func getPluginsDir() string {
|
||||||
configDir, err := os.UserConfigDir()
|
return filepath.Join(utils.XDGConfigHome(), "DankMaterialShell", "plugins")
|
||||||
if err != nil {
|
|
||||||
log.Error("failed to get user config dir", "err", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return filepath.Join(configDir, "DankMaterialShell", "plugins")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
|
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
|
||||||
|
|||||||
@@ -1,695 +0,0 @@
|
|||||||
// Generated by go-wayland-scanner
|
|
||||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
|
||||||
// XML file : internal/proto/xml/ext-data-control-v1.xml
|
|
||||||
//
|
|
||||||
// ext_data_control_v1 Protocol Copyright:
|
|
||||||
//
|
|
||||||
// Copyright © 2018 Simon Ser
|
|
||||||
// Copyright © 2019 Ivan Molodetskikh
|
|
||||||
// Copyright © 2024 Neal Gompa
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, distribute, and sell this
|
|
||||||
// software and its documentation for any purpose is hereby granted
|
|
||||||
// without fee, provided that the above copyright notice appear in
|
|
||||||
// all copies and that both that copyright notice and this permission
|
|
||||||
// notice appear in supporting documentation, and that the name of
|
|
||||||
// the copyright holders not be used in advertising or publicity
|
|
||||||
// pertaining to distribution of the software without specific,
|
|
||||||
// written prior permission. The copyright holders make no
|
|
||||||
// representations about the suitability of this software for any
|
|
||||||
// purpose. It is provided "as is" without express or implied
|
|
||||||
// warranty.
|
|
||||||
//
|
|
||||||
// THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
|
||||||
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
||||||
// FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
||||||
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
||||||
// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
||||||
// THIS SOFTWARE.
|
|
||||||
|
|
||||||
package ext_data_control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExtDataControlManagerV1InterfaceName 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 ExtDataControlManagerV1InterfaceName = "ext_data_control_manager_v1"
|
|
||||||
|
|
||||||
// ExtDataControlManagerV1 : manager to control data devices
|
|
||||||
//
|
|
||||||
// This interface is a manager that allows creating per-seat data device
|
|
||||||
// controls.
|
|
||||||
type ExtDataControlManagerV1 struct {
|
|
||||||
client.BaseProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExtDataControlManagerV1 : manager to control data devices
|
|
||||||
//
|
|
||||||
// This interface is a manager that allows creating per-seat data device
|
|
||||||
// controls.
|
|
||||||
func NewExtDataControlManagerV1(ctx *client.Context) *ExtDataControlManagerV1 {
|
|
||||||
extDataControlManagerV1 := &ExtDataControlManagerV1{}
|
|
||||||
ctx.Register(extDataControlManagerV1)
|
|
||||||
return extDataControlManagerV1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDataSource : create a new data source
|
|
||||||
//
|
|
||||||
// Create a new data source.
|
|
||||||
func (i *ExtDataControlManagerV1) CreateDataSource() (*ExtDataControlSourceV1, error) {
|
|
||||||
id := NewExtDataControlSourceV1(i.Context())
|
|
||||||
const opcode = 0
|
|
||||||
const _reqBufLen = 8 + 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], id.ID())
|
|
||||||
l += 4
|
|
||||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDataDevice : get a data device for a seat
|
|
||||||
//
|
|
||||||
// Create a data device that can be used to manage a seat's selection.
|
|
||||||
func (i *ExtDataControlManagerV1) GetDataDevice(seat *client.Seat) (*ExtDataControlDeviceV1, error) {
|
|
||||||
id := NewExtDataControlDeviceV1(i.Context())
|
|
||||||
const opcode = 1
|
|
||||||
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], id.ID())
|
|
||||||
l += 4
|
|
||||||
client.PutUint32(_reqBuf[l:l+4], seat.ID())
|
|
||||||
l += 4
|
|
||||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDataDeviceWithProxy : get a data device for a seat using a pre-created proxy
|
|
||||||
//
|
|
||||||
// Like GetDataDevice, but uses a pre-created ExtDataControlDeviceV1 proxy.
|
|
||||||
// This allows setting up event handlers before the request is sent.
|
|
||||||
func (i *ExtDataControlManagerV1) GetDataDeviceWithProxy(device *ExtDataControlDeviceV1, seat *client.Seat) error {
|
|
||||||
const opcode = 1
|
|
||||||
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], device.ID())
|
|
||||||
l += 4
|
|
||||||
client.PutUint32(_reqBuf[l:l+4], seat.ID())
|
|
||||||
l += 4
|
|
||||||
return i.Context().WriteMsg(_reqBuf[:], nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy : destroy the manager
|
|
||||||
//
|
|
||||||
// All objects created by the manager will still remain valid, until their
|
|
||||||
// appropriate destroy request has been called.
|
|
||||||
func (i *ExtDataControlManagerV1) Destroy() error {
|
|
||||||
defer i.MarkZombie()
|
|
||||||
const opcode = 2
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1InterfaceName 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 ExtDataControlDeviceV1InterfaceName = "ext_data_control_device_v1"
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1 : manage a data device for a seat
|
|
||||||
//
|
|
||||||
// This interface allows a client to manage a seat's selection.
|
|
||||||
//
|
|
||||||
// When the seat is destroyed, this object becomes inert.
|
|
||||||
type ExtDataControlDeviceV1 struct {
|
|
||||||
client.BaseProxy
|
|
||||||
dataOfferHandler ExtDataControlDeviceV1DataOfferHandlerFunc
|
|
||||||
selectionHandler ExtDataControlDeviceV1SelectionHandlerFunc
|
|
||||||
finishedHandler ExtDataControlDeviceV1FinishedHandlerFunc
|
|
||||||
primarySelectionHandler ExtDataControlDeviceV1PrimarySelectionHandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExtDataControlDeviceV1 : manage a data device for a seat
|
|
||||||
//
|
|
||||||
// This interface allows a client to manage a seat's selection.
|
|
||||||
//
|
|
||||||
// When the seat is destroyed, this object becomes inert.
|
|
||||||
func NewExtDataControlDeviceV1(ctx *client.Context) *ExtDataControlDeviceV1 {
|
|
||||||
extDataControlDeviceV1 := &ExtDataControlDeviceV1{}
|
|
||||||
ctx.Register(extDataControlDeviceV1)
|
|
||||||
return extDataControlDeviceV1
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSelection : copy data to the selection
|
|
||||||
//
|
|
||||||
// This request asks the compositor to set the selection to the data from
|
|
||||||
// the source on behalf of the client.
|
|
||||||
//
|
|
||||||
// The given source may not be used in any further set_selection or
|
|
||||||
// set_primary_selection requests. Attempting to use a previously used
|
|
||||||
// source triggers the used_source protocol error.
|
|
||||||
//
|
|
||||||
// To unset the selection, set the source to NULL.
|
|
||||||
func (i *ExtDataControlDeviceV1) SetSelection(source *ExtDataControlSourceV1) error {
|
|
||||||
const opcode = 0
|
|
||||||
const _reqBufLen = 8 + 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
|
|
||||||
if source == nil {
|
|
||||||
client.PutUint32(_reqBuf[l:l+4], 0)
|
|
||||||
l += 4
|
|
||||||
} else {
|
|
||||||
client.PutUint32(_reqBuf[l:l+4], source.ID())
|
|
||||||
l += 4
|
|
||||||
}
|
|
||||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy : destroy this data device
|
|
||||||
//
|
|
||||||
// Destroys the data device object.
|
|
||||||
func (i *ExtDataControlDeviceV1) Destroy() error {
|
|
||||||
defer i.MarkZombie()
|
|
||||||
const opcode = 1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPrimarySelection : copy data to the primary selection
|
|
||||||
//
|
|
||||||
// This request asks the compositor to set the primary selection to the
|
|
||||||
// data from the source on behalf of the client.
|
|
||||||
//
|
|
||||||
// The given source may not be used in any further set_selection or
|
|
||||||
// set_primary_selection requests. Attempting to use a previously used
|
|
||||||
// source triggers the used_source protocol error.
|
|
||||||
//
|
|
||||||
// To unset the primary selection, set the source to NULL.
|
|
||||||
//
|
|
||||||
// The compositor will ignore this request if it does not support primary
|
|
||||||
// selection.
|
|
||||||
func (i *ExtDataControlDeviceV1) SetPrimarySelection(source *ExtDataControlSourceV1) error {
|
|
||||||
const opcode = 2
|
|
||||||
const _reqBufLen = 8 + 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
|
|
||||||
if source == nil {
|
|
||||||
client.PutUint32(_reqBuf[l:l+4], 0)
|
|
||||||
l += 4
|
|
||||||
} else {
|
|
||||||
client.PutUint32(_reqBuf[l:l+4], source.ID())
|
|
||||||
l += 4
|
|
||||||
}
|
|
||||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtDataControlDeviceV1Error uint32
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1Error :
|
|
||||||
const (
|
|
||||||
// ExtDataControlDeviceV1ErrorUsedSource : source given to set_selection or set_primary_selection was already used before
|
|
||||||
ExtDataControlDeviceV1ErrorUsedSource ExtDataControlDeviceV1Error = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e ExtDataControlDeviceV1Error) Name() string {
|
|
||||||
switch e {
|
|
||||||
case ExtDataControlDeviceV1ErrorUsedSource:
|
|
||||||
return "used_source"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExtDataControlDeviceV1Error) Value() string {
|
|
||||||
switch e {
|
|
||||||
case ExtDataControlDeviceV1ErrorUsedSource:
|
|
||||||
return "1"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExtDataControlDeviceV1Error) String() string {
|
|
||||||
return e.Name() + "=" + e.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1DataOfferEvent : introduce a new ext_data_control_offer
|
|
||||||
//
|
|
||||||
// The data_offer event introduces a new ext_data_control_offer object,
|
|
||||||
// which will subsequently be used in either the
|
|
||||||
// ext_data_control_device.selection event (for the regular clipboard
|
|
||||||
// selections) or the ext_data_control_device.primary_selection event (for
|
|
||||||
// the primary clipboard selections). Immediately following the
|
|
||||||
// ext_data_control_device.data_offer event, the new data_offer object
|
|
||||||
// will send out ext_data_control_offer.offer events to describe the MIME
|
|
||||||
// types it offers.
|
|
||||||
type ExtDataControlDeviceV1DataOfferEvent struct {
|
|
||||||
Id *ExtDataControlOfferV1
|
|
||||||
}
|
|
||||||
type ExtDataControlDeviceV1DataOfferHandlerFunc func(ExtDataControlDeviceV1DataOfferEvent)
|
|
||||||
|
|
||||||
// SetDataOfferHandler : sets handler for ExtDataControlDeviceV1DataOfferEvent
|
|
||||||
func (i *ExtDataControlDeviceV1) SetDataOfferHandler(f ExtDataControlDeviceV1DataOfferHandlerFunc) {
|
|
||||||
i.dataOfferHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1SelectionEvent : advertise new selection
|
|
||||||
//
|
|
||||||
// The selection event is sent out to notify the client of a new
|
|
||||||
// ext_data_control_offer for the selection for this device. The
|
|
||||||
// ext_data_control_device.data_offer and the ext_data_control_offer.offer
|
|
||||||
// events are sent out immediately before this event to introduce the data
|
|
||||||
// offer object. The selection event is sent to a client when a new
|
|
||||||
// selection is set. The ext_data_control_offer is valid until a new
|
|
||||||
// ext_data_control_offer or NULL is received. The client must destroy the
|
|
||||||
// previous selection ext_data_control_offer, if any, upon receiving this
|
|
||||||
// event. Regardless, the previous selection will be ignored once a new
|
|
||||||
// selection ext_data_control_offer is received.
|
|
||||||
//
|
|
||||||
// The first selection event is sent upon binding the
|
|
||||||
// ext_data_control_device object.
|
|
||||||
type ExtDataControlDeviceV1SelectionEvent struct {
|
|
||||||
Id *ExtDataControlOfferV1
|
|
||||||
OfferId uint32 // Raw object ID for external registry lookups
|
|
||||||
}
|
|
||||||
type ExtDataControlDeviceV1SelectionHandlerFunc func(ExtDataControlDeviceV1SelectionEvent)
|
|
||||||
|
|
||||||
// SetSelectionHandler : sets handler for ExtDataControlDeviceV1SelectionEvent
|
|
||||||
func (i *ExtDataControlDeviceV1) SetSelectionHandler(f ExtDataControlDeviceV1SelectionHandlerFunc) {
|
|
||||||
i.selectionHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1FinishedEvent : this data control is no longer valid
|
|
||||||
//
|
|
||||||
// This data control object is no longer valid and should be destroyed by
|
|
||||||
// the client.
|
|
||||||
type ExtDataControlDeviceV1FinishedEvent struct{}
|
|
||||||
type ExtDataControlDeviceV1FinishedHandlerFunc func(ExtDataControlDeviceV1FinishedEvent)
|
|
||||||
|
|
||||||
// SetFinishedHandler : sets handler for ExtDataControlDeviceV1FinishedEvent
|
|
||||||
func (i *ExtDataControlDeviceV1) SetFinishedHandler(f ExtDataControlDeviceV1FinishedHandlerFunc) {
|
|
||||||
i.finishedHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlDeviceV1PrimarySelectionEvent : advertise new primary selection
|
|
||||||
//
|
|
||||||
// The primary_selection event is sent out to notify the client of a new
|
|
||||||
// ext_data_control_offer for the primary selection for this device. The
|
|
||||||
// ext_data_control_device.data_offer and the ext_data_control_offer.offer
|
|
||||||
// events are sent out immediately before this event to introduce the data
|
|
||||||
// offer object. The primary_selection event is sent to a client when a
|
|
||||||
// new primary selection is set. The ext_data_control_offer is valid until
|
|
||||||
// a new ext_data_control_offer or NULL is received. The client must
|
|
||||||
// destroy the previous primary selection ext_data_control_offer, if any,
|
|
||||||
// upon receiving this event. Regardless, the previous primary selection
|
|
||||||
// will be ignored once a new primary selection ext_data_control_offer is
|
|
||||||
// received.
|
|
||||||
//
|
|
||||||
// If the compositor supports primary selection, the first
|
|
||||||
// primary_selection event is sent upon binding the
|
|
||||||
// ext_data_control_device object.
|
|
||||||
type ExtDataControlDeviceV1PrimarySelectionEvent struct {
|
|
||||||
Id *ExtDataControlOfferV1
|
|
||||||
OfferId uint32 // Raw object ID for external registry lookups
|
|
||||||
}
|
|
||||||
type ExtDataControlDeviceV1PrimarySelectionHandlerFunc func(ExtDataControlDeviceV1PrimarySelectionEvent)
|
|
||||||
|
|
||||||
// SetPrimarySelectionHandler : sets handler for ExtDataControlDeviceV1PrimarySelectionEvent
|
|
||||||
func (i *ExtDataControlDeviceV1) SetPrimarySelectionHandler(f ExtDataControlDeviceV1PrimarySelectionHandlerFunc) {
|
|
||||||
i.primarySelectionHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ExtDataControlDeviceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|
||||||
switch opcode {
|
|
||||||
case 0:
|
|
||||||
// data_offer event: server creates a new object (new_id)
|
|
||||||
if i.dataOfferHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlDeviceV1DataOfferEvent
|
|
||||||
l := 0
|
|
||||||
newID := client.Uint32(data[l : l+4])
|
|
||||||
l += 4
|
|
||||||
|
|
||||||
ctx := i.Context()
|
|
||||||
offer := &ExtDataControlOfferV1{}
|
|
||||||
offer.SetContext(ctx)
|
|
||||||
offer.SetID(newID)
|
|
||||||
ctx.RegisterWithID(offer, newID)
|
|
||||||
e.Id = offer
|
|
||||||
|
|
||||||
i.dataOfferHandler(e)
|
|
||||||
case 1:
|
|
||||||
// selection event: nullable object reference
|
|
||||||
if i.selectionHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlDeviceV1SelectionEvent
|
|
||||||
l := 0
|
|
||||||
objID := client.Uint32(data[l : l+4])
|
|
||||||
l += 4
|
|
||||||
|
|
||||||
e.OfferId = objID
|
|
||||||
if objID != 0 {
|
|
||||||
if p := i.Context().GetProxy(objID); p != nil {
|
|
||||||
e.Id = p.(*ExtDataControlOfferV1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i.selectionHandler(e)
|
|
||||||
case 2:
|
|
||||||
if i.finishedHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlDeviceV1FinishedEvent
|
|
||||||
|
|
||||||
i.finishedHandler(e)
|
|
||||||
case 3:
|
|
||||||
// primary_selection event: nullable object reference
|
|
||||||
if i.primarySelectionHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlDeviceV1PrimarySelectionEvent
|
|
||||||
l := 0
|
|
||||||
objID := client.Uint32(data[l : l+4])
|
|
||||||
l += 4
|
|
||||||
|
|
||||||
e.OfferId = objID
|
|
||||||
if objID != 0 {
|
|
||||||
if p := i.Context().GetProxy(objID); p != nil {
|
|
||||||
e.Id = p.(*ExtDataControlOfferV1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i.primarySelectionHandler(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlSourceV1InterfaceName 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 ExtDataControlSourceV1InterfaceName = "ext_data_control_source_v1"
|
|
||||||
|
|
||||||
// ExtDataControlSourceV1 : offer to transfer data
|
|
||||||
//
|
|
||||||
// The ext_data_control_source object is the source side of a
|
|
||||||
// ext_data_control_offer. It is created by the source client in a data
|
|
||||||
// transfer and provides a way to describe the offered data and a way to
|
|
||||||
// respond to requests to transfer the data.
|
|
||||||
type ExtDataControlSourceV1 struct {
|
|
||||||
client.BaseProxy
|
|
||||||
sendHandler ExtDataControlSourceV1SendHandlerFunc
|
|
||||||
cancelledHandler ExtDataControlSourceV1CancelledHandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExtDataControlSourceV1 : offer to transfer data
|
|
||||||
//
|
|
||||||
// The ext_data_control_source object is the source side of a
|
|
||||||
// ext_data_control_offer. It is created by the source client in a data
|
|
||||||
// transfer and provides a way to describe the offered data and a way to
|
|
||||||
// respond to requests to transfer the data.
|
|
||||||
func NewExtDataControlSourceV1(ctx *client.Context) *ExtDataControlSourceV1 {
|
|
||||||
extDataControlSourceV1 := &ExtDataControlSourceV1{}
|
|
||||||
ctx.Register(extDataControlSourceV1)
|
|
||||||
return extDataControlSourceV1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offer : add an offered MIME type
|
|
||||||
//
|
|
||||||
// This request adds a MIME type to the set of MIME types advertised to
|
|
||||||
// targets. Can be called several times to offer multiple types.
|
|
||||||
//
|
|
||||||
// Calling this after ext_data_control_device.set_selection is a protocol
|
|
||||||
// error.
|
|
||||||
//
|
|
||||||
// mimeType: MIME type offered by the data source
|
|
||||||
func (i *ExtDataControlSourceV1) Offer(mimeType string) error {
|
|
||||||
const opcode = 0
|
|
||||||
mimeTypeLen := client.PaddedLen(len(mimeType) + 1)
|
|
||||||
_reqBufLen := 8 + (4 + mimeTypeLen)
|
|
||||||
_reqBuf := make([]byte, _reqBufLen)
|
|
||||||
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.PutString(_reqBuf[l:l+(4+mimeTypeLen)], mimeType)
|
|
||||||
l += (4 + mimeTypeLen)
|
|
||||||
err := i.Context().WriteMsg(_reqBuf, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy : destroy this source
|
|
||||||
//
|
|
||||||
// Destroys the data source object.
|
|
||||||
func (i *ExtDataControlSourceV1) Destroy() error {
|
|
||||||
defer i.MarkZombie()
|
|
||||||
const opcode = 1
|
|
||||||
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 ExtDataControlSourceV1Error uint32
|
|
||||||
|
|
||||||
// ExtDataControlSourceV1Error :
|
|
||||||
const (
|
|
||||||
// ExtDataControlSourceV1ErrorInvalidOffer : offer sent after ext_data_control_device.set_selection
|
|
||||||
ExtDataControlSourceV1ErrorInvalidOffer ExtDataControlSourceV1Error = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e ExtDataControlSourceV1Error) Name() string {
|
|
||||||
switch e {
|
|
||||||
case ExtDataControlSourceV1ErrorInvalidOffer:
|
|
||||||
return "invalid_offer"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExtDataControlSourceV1Error) Value() string {
|
|
||||||
switch e {
|
|
||||||
case ExtDataControlSourceV1ErrorInvalidOffer:
|
|
||||||
return "1"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExtDataControlSourceV1Error) String() string {
|
|
||||||
return e.Name() + "=" + e.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlSourceV1SendEvent : send the data
|
|
||||||
//
|
|
||||||
// Request for data from the client. Send the data as the specified MIME
|
|
||||||
// type over the passed file descriptor, then close it.
|
|
||||||
type ExtDataControlSourceV1SendEvent struct {
|
|
||||||
MimeType string
|
|
||||||
Fd int
|
|
||||||
}
|
|
||||||
type ExtDataControlSourceV1SendHandlerFunc func(ExtDataControlSourceV1SendEvent)
|
|
||||||
|
|
||||||
// SetSendHandler : sets handler for ExtDataControlSourceV1SendEvent
|
|
||||||
func (i *ExtDataControlSourceV1) SetSendHandler(f ExtDataControlSourceV1SendHandlerFunc) {
|
|
||||||
i.sendHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlSourceV1CancelledEvent : selection was cancelled
|
|
||||||
//
|
|
||||||
// This data source is no longer valid. The data source has been replaced
|
|
||||||
// by another data source.
|
|
||||||
//
|
|
||||||
// The client should clean up and destroy this data source.
|
|
||||||
type ExtDataControlSourceV1CancelledEvent struct{}
|
|
||||||
type ExtDataControlSourceV1CancelledHandlerFunc func(ExtDataControlSourceV1CancelledEvent)
|
|
||||||
|
|
||||||
// SetCancelledHandler : sets handler for ExtDataControlSourceV1CancelledEvent
|
|
||||||
func (i *ExtDataControlSourceV1) SetCancelledHandler(f ExtDataControlSourceV1CancelledHandlerFunc) {
|
|
||||||
i.cancelledHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ExtDataControlSourceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|
||||||
switch opcode {
|
|
||||||
case 0:
|
|
||||||
if i.sendHandler == nil {
|
|
||||||
if fd != -1 {
|
|
||||||
unix.Close(fd)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlSourceV1SendEvent
|
|
||||||
l := 0
|
|
||||||
mimeTypeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
|
||||||
l += 4
|
|
||||||
e.MimeType = client.String(data[l : l+mimeTypeLen])
|
|
||||||
l += mimeTypeLen
|
|
||||||
e.Fd = fd
|
|
||||||
|
|
||||||
i.sendHandler(e)
|
|
||||||
case 1:
|
|
||||||
if i.cancelledHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlSourceV1CancelledEvent
|
|
||||||
|
|
||||||
i.cancelledHandler(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlOfferV1InterfaceName 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 ExtDataControlOfferV1InterfaceName = "ext_data_control_offer_v1"
|
|
||||||
|
|
||||||
// ExtDataControlOfferV1 : offer to transfer data
|
|
||||||
//
|
|
||||||
// A ext_data_control_offer represents a piece of data offered for transfer
|
|
||||||
// by another client (the source client). The offer describes the different
|
|
||||||
// MIME types that the data can be converted to and provides the mechanism
|
|
||||||
// for transferring the data directly from the source client.
|
|
||||||
type ExtDataControlOfferV1 struct {
|
|
||||||
client.BaseProxy
|
|
||||||
offerHandler ExtDataControlOfferV1OfferHandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExtDataControlOfferV1 : offer to transfer data
|
|
||||||
//
|
|
||||||
// A ext_data_control_offer represents a piece of data offered for transfer
|
|
||||||
// by another client (the source client). The offer describes the different
|
|
||||||
// MIME types that the data can be converted to and provides the mechanism
|
|
||||||
// for transferring the data directly from the source client.
|
|
||||||
func NewExtDataControlOfferV1(ctx *client.Context) *ExtDataControlOfferV1 {
|
|
||||||
extDataControlOfferV1 := &ExtDataControlOfferV1{}
|
|
||||||
ctx.Register(extDataControlOfferV1)
|
|
||||||
return extDataControlOfferV1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive : request that the data is transferred
|
|
||||||
//
|
|
||||||
// To transfer the offered data, the client issues this request and
|
|
||||||
// indicates the MIME type it wants to receive. The transfer happens
|
|
||||||
// through the passed file descriptor (typically created with the pipe
|
|
||||||
// system call). The source client writes the data in the MIME type
|
|
||||||
// representation requested and then closes the file descriptor.
|
|
||||||
//
|
|
||||||
// The receiving client reads from the read end of the pipe until EOF and
|
|
||||||
// then closes its end, at which point the transfer is complete.
|
|
||||||
//
|
|
||||||
// This request may happen multiple times for different MIME types.
|
|
||||||
//
|
|
||||||
// mimeType: MIME type desired by receiver
|
|
||||||
// fd: file descriptor for data transfer
|
|
||||||
func (i *ExtDataControlOfferV1) Receive(mimeType string, fd int) error {
|
|
||||||
const opcode = 0
|
|
||||||
mimeTypeLen := client.PaddedLen(len(mimeType) + 1)
|
|
||||||
_reqBufLen := 8 + (4 + mimeTypeLen)
|
|
||||||
_reqBuf := make([]byte, _reqBufLen)
|
|
||||||
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.PutString(_reqBuf[l:l+(4+mimeTypeLen)], mimeType)
|
|
||||||
l += (4 + mimeTypeLen)
|
|
||||||
oob := unix.UnixRights(int(fd))
|
|
||||||
err := i.Context().WriteMsg(_reqBuf, oob)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy : destroy this offer
|
|
||||||
//
|
|
||||||
// Destroys the data offer object.
|
|
||||||
func (i *ExtDataControlOfferV1) Destroy() error {
|
|
||||||
defer i.MarkZombie()
|
|
||||||
const opcode = 1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtDataControlOfferV1OfferEvent : advertise offered MIME type
|
|
||||||
//
|
|
||||||
// Sent immediately after creating the ext_data_control_offer object.
|
|
||||||
// One event per offered MIME type.
|
|
||||||
type ExtDataControlOfferV1OfferEvent struct {
|
|
||||||
MimeType string
|
|
||||||
}
|
|
||||||
type ExtDataControlOfferV1OfferHandlerFunc func(ExtDataControlOfferV1OfferEvent)
|
|
||||||
|
|
||||||
// SetOfferHandler : sets handler for ExtDataControlOfferV1OfferEvent
|
|
||||||
func (i *ExtDataControlOfferV1) SetOfferHandler(f ExtDataControlOfferV1OfferHandlerFunc) {
|
|
||||||
i.offerHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ExtDataControlOfferV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|
||||||
switch opcode {
|
|
||||||
case 0:
|
|
||||||
if i.offerHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e ExtDataControlOfferV1OfferEvent
|
|
||||||
l := 0
|
|
||||||
mimeTypeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
|
||||||
l += 4
|
|
||||||
e.MimeType = client.String(data[l : l+mimeTypeLen])
|
|
||||||
l += mimeTypeLen
|
|
||||||
|
|
||||||
i.offerHandler(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<protocol name="ext_data_control_v1">
|
|
||||||
<copyright>
|
|
||||||
Copyright © 2018 Simon Ser
|
|
||||||
Copyright © 2019 Ivan Molodetskikh
|
|
||||||
Copyright © 2024 Neal Gompa
|
|
||||||
|
|
||||||
Permission to use, copy, modify, distribute, and sell this
|
|
||||||
software and its documentation for any purpose is hereby granted
|
|
||||||
without fee, provided that the above copyright notice appear in
|
|
||||||
all copies and that both that copyright notice and this permission
|
|
||||||
notice appear in supporting documentation, and that the name of
|
|
||||||
the copyright holders not be used in advertising or publicity
|
|
||||||
pertaining to distribution of the software without specific,
|
|
||||||
written prior permission. The copyright holders make no
|
|
||||||
representations about the suitability of this software for any
|
|
||||||
purpose. It is provided "as is" without express or implied
|
|
||||||
warranty.
|
|
||||||
|
|
||||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
|
||||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
||||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
||||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
||||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
||||||
THIS SOFTWARE.
|
|
||||||
</copyright>
|
|
||||||
|
|
||||||
<description summary="control data devices">
|
|
||||||
This protocol allows a privileged client to control data devices. In
|
|
||||||
particular, the client will be able to manage the current selection and take
|
|
||||||
the role of a clipboard manager.
|
|
||||||
|
|
||||||
Warning! The protocol described in this file is currently in the testing
|
|
||||||
phase. Backward compatible changes may be added together with the
|
|
||||||
corresponding interface version bump. Backward incompatible changes can
|
|
||||||
only be done by creating a new major version of the extension.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<interface name="ext_data_control_manager_v1" version="1">
|
|
||||||
<description summary="manager to control data devices">
|
|
||||||
This interface is a manager that allows creating per-seat data device
|
|
||||||
controls.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<request name="create_data_source">
|
|
||||||
<description summary="create a new data source">
|
|
||||||
Create a new data source.
|
|
||||||
</description>
|
|
||||||
<arg name="id" type="new_id" interface="ext_data_control_source_v1"
|
|
||||||
summary="data source to create"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="get_data_device">
|
|
||||||
<description summary="get a data device for a seat">
|
|
||||||
Create a data device that can be used to manage a seat's selection.
|
|
||||||
</description>
|
|
||||||
<arg name="id" type="new_id" interface="ext_data_control_device_v1"/>
|
|
||||||
<arg name="seat" type="object" interface="wl_seat"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy the manager">
|
|
||||||
All objects created by the manager will still remain valid, until their
|
|
||||||
appropriate destroy request has been called.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
</interface>
|
|
||||||
|
|
||||||
<interface name="ext_data_control_device_v1" version="1">
|
|
||||||
<description summary="manage a data device for a seat">
|
|
||||||
This interface allows a client to manage a seat's selection.
|
|
||||||
|
|
||||||
When the seat is destroyed, this object becomes inert.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<request name="set_selection">
|
|
||||||
<description summary="copy data to the selection">
|
|
||||||
This request asks the compositor to set the selection to the data from
|
|
||||||
the source on behalf of the client.
|
|
||||||
|
|
||||||
The given source may not be used in any further set_selection or
|
|
||||||
set_primary_selection requests. Attempting to use a previously used
|
|
||||||
source triggers the used_source protocol error.
|
|
||||||
|
|
||||||
To unset the selection, set the source to NULL.
|
|
||||||
</description>
|
|
||||||
<arg name="source" type="object" interface="ext_data_control_source_v1"
|
|
||||||
allow-null="true"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy this data device">
|
|
||||||
Destroys the data device object.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<event name="data_offer">
|
|
||||||
<description summary="introduce a new ext_data_control_offer">
|
|
||||||
The data_offer event introduces a new ext_data_control_offer object,
|
|
||||||
which will subsequently be used in either the
|
|
||||||
ext_data_control_device.selection event (for the regular clipboard
|
|
||||||
selections) or the ext_data_control_device.primary_selection event (for
|
|
||||||
the primary clipboard selections). Immediately following the
|
|
||||||
ext_data_control_device.data_offer event, the new data_offer object
|
|
||||||
will send out ext_data_control_offer.offer events to describe the MIME
|
|
||||||
types it offers.
|
|
||||||
</description>
|
|
||||||
<arg name="id" type="new_id" interface="ext_data_control_offer_v1"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="selection">
|
|
||||||
<description summary="advertise new selection">
|
|
||||||
The selection event is sent out to notify the client of a new
|
|
||||||
ext_data_control_offer for the selection for this device. The
|
|
||||||
ext_data_control_device.data_offer and the ext_data_control_offer.offer
|
|
||||||
events are sent out immediately before this event to introduce the data
|
|
||||||
offer object. The selection event is sent to a client when a new
|
|
||||||
selection is set. The ext_data_control_offer is valid until a new
|
|
||||||
ext_data_control_offer or NULL is received. The client must destroy the
|
|
||||||
previous selection ext_data_control_offer, if any, upon receiving this
|
|
||||||
event. Regardless, the previous selection will be ignored once a new
|
|
||||||
selection ext_data_control_offer is received.
|
|
||||||
|
|
||||||
The first selection event is sent upon binding the
|
|
||||||
ext_data_control_device object.
|
|
||||||
</description>
|
|
||||||
<arg name="id" type="object" interface="ext_data_control_offer_v1"
|
|
||||||
allow-null="true"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="finished">
|
|
||||||
<description summary="this data control is no longer valid">
|
|
||||||
This data control object is no longer valid and should be destroyed by
|
|
||||||
the client.
|
|
||||||
</description>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="primary_selection">
|
|
||||||
<description summary="advertise new primary selection">
|
|
||||||
The primary_selection event is sent out to notify the client of a new
|
|
||||||
ext_data_control_offer for the primary selection for this device. The
|
|
||||||
ext_data_control_device.data_offer and the ext_data_control_offer.offer
|
|
||||||
events are sent out immediately before this event to introduce the data
|
|
||||||
offer object. The primary_selection event is sent to a client when a
|
|
||||||
new primary selection is set. The ext_data_control_offer is valid until
|
|
||||||
a new ext_data_control_offer or NULL is received. The client must
|
|
||||||
destroy the previous primary selection ext_data_control_offer, if any,
|
|
||||||
upon receiving this event. Regardless, the previous primary selection
|
|
||||||
will be ignored once a new primary selection ext_data_control_offer is
|
|
||||||
received.
|
|
||||||
|
|
||||||
If the compositor supports primary selection, the first
|
|
||||||
primary_selection event is sent upon binding the
|
|
||||||
ext_data_control_device object.
|
|
||||||
</description>
|
|
||||||
<arg name="id" type="object" interface="ext_data_control_offer_v1"
|
|
||||||
allow-null="true"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<request name="set_primary_selection">
|
|
||||||
<description summary="copy data to the primary selection">
|
|
||||||
This request asks the compositor to set the primary selection to the
|
|
||||||
data from the source on behalf of the client.
|
|
||||||
|
|
||||||
The given source may not be used in any further set_selection or
|
|
||||||
set_primary_selection requests. Attempting to use a previously used
|
|
||||||
source triggers the used_source protocol error.
|
|
||||||
|
|
||||||
To unset the primary selection, set the source to NULL.
|
|
||||||
|
|
||||||
The compositor will ignore this request if it does not support primary
|
|
||||||
selection.
|
|
||||||
</description>
|
|
||||||
<arg name="source" type="object" interface="ext_data_control_source_v1"
|
|
||||||
allow-null="true"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<enum name="error">
|
|
||||||
<entry name="used_source" value="1"
|
|
||||||
summary="source given to set_selection or set_primary_selection was already used before"/>
|
|
||||||
</enum>
|
|
||||||
</interface>
|
|
||||||
|
|
||||||
<interface name="ext_data_control_source_v1" version="1">
|
|
||||||
<description summary="offer to transfer data">
|
|
||||||
The ext_data_control_source object is the source side of a
|
|
||||||
ext_data_control_offer. It is created by the source client in a data
|
|
||||||
transfer and provides a way to describe the offered data and a way to
|
|
||||||
respond to requests to transfer the data.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<enum name="error">
|
|
||||||
<entry name="invalid_offer" value="1"
|
|
||||||
summary="offer sent after ext_data_control_device.set_selection"/>
|
|
||||||
</enum>
|
|
||||||
|
|
||||||
<request name="offer">
|
|
||||||
<description summary="add an offered MIME type">
|
|
||||||
This request adds a MIME type to the set of MIME types advertised to
|
|
||||||
targets. Can be called several times to offer multiple types.
|
|
||||||
|
|
||||||
Calling this after ext_data_control_device.set_selection is a protocol
|
|
||||||
error.
|
|
||||||
</description>
|
|
||||||
<arg name="mime_type" type="string"
|
|
||||||
summary="MIME type offered by the data source"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy this source">
|
|
||||||
Destroys the data source object.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<event name="send">
|
|
||||||
<description summary="send the data">
|
|
||||||
Request for data from the client. Send the data as the specified MIME
|
|
||||||
type over the passed file descriptor, then close it.
|
|
||||||
</description>
|
|
||||||
<arg name="mime_type" type="string" summary="MIME type for the data"/>
|
|
||||||
<arg name="fd" type="fd" summary="file descriptor for the data"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="cancelled">
|
|
||||||
<description summary="selection was cancelled">
|
|
||||||
This data source is no longer valid. The data source has been replaced
|
|
||||||
by another data source.
|
|
||||||
|
|
||||||
The client should clean up and destroy this data source.
|
|
||||||
</description>
|
|
||||||
</event>
|
|
||||||
</interface>
|
|
||||||
|
|
||||||
<interface name="ext_data_control_offer_v1" version="1">
|
|
||||||
<description summary="offer to transfer data">
|
|
||||||
A ext_data_control_offer represents a piece of data offered for transfer
|
|
||||||
by another client (the source client). The offer describes the different
|
|
||||||
MIME types that the data can be converted to and provides the mechanism
|
|
||||||
for transferring the data directly from the source client.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<request name="receive">
|
|
||||||
<description summary="request that the data is transferred">
|
|
||||||
To transfer the offered data, the client issues this request and
|
|
||||||
indicates the MIME type it wants to receive. The transfer happens
|
|
||||||
through the passed file descriptor (typically created with the pipe
|
|
||||||
system call). The source client writes the data in the MIME type
|
|
||||||
representation requested and then closes the file descriptor.
|
|
||||||
|
|
||||||
The receiving client reads from the read end of the pipe until EOF and
|
|
||||||
then closes its end, at which point the transfer is complete.
|
|
||||||
|
|
||||||
This request may happen multiple times for different MIME types.
|
|
||||||
</description>
|
|
||||||
<arg name="mime_type" type="string"
|
|
||||||
summary="MIME type desired by receiver"/>
|
|
||||||
<arg name="fd" type="fd" summary="file descriptor for data transfer"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy this offer">
|
|
||||||
Destroys the data offer object.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<event name="offer">
|
|
||||||
<description summary="advertise offered MIME type">
|
|
||||||
Sent immediately after creating the ext_data_control_offer object.
|
|
||||||
One event per offered MIME type.
|
|
||||||
</description>
|
|
||||||
<arg name="mime_type" type="string" summary="offered MIME type"/>
|
|
||||||
</event>
|
|
||||||
</interface>
|
|
||||||
</protocol>
|
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,12 +119,7 @@ func GetOutputDir() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getXDGPicturesDir() string {
|
func getXDGPicturesDir() string {
|
||||||
userConfigDir, err := os.UserConfigDir()
|
userDirsFile := filepath.Join(utils.XDGConfigHome(), "user-dirs.dirs")
|
||||||
if err != nil {
|
|
||||||
log.Error("failed to get user config dir", "err", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
userDirsFile := filepath.Join(userConfigDir, "user-dirs.dirs")
|
|
||||||
data, err := os.ReadFile(userDirsFile)
|
data, err := os.ReadFile(userDirsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ThemeColors struct {
|
type ThemeColors struct {
|
||||||
@@ -74,12 +74,7 @@ func loadColorsFile() *ColorScheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getColorsFilePath() string {
|
func getColorsFilePath() string {
|
||||||
cacheDir, err := os.UserCacheDir()
|
return filepath.Join(utils.XDGCacheHome(), "DankMaterialShell", "dms-colors.json")
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to get user cache dir", "err", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isLightMode() bool {
|
func isLightMode() bool {
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
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, m *Manager) {
|
|
||||||
switch req.Method {
|
|
||||||
case "clipboard.getState":
|
|
||||||
handleGetState(conn, req, m)
|
|
||||||
case "clipboard.getHistory":
|
|
||||||
handleGetHistory(conn, req, m)
|
|
||||||
case "clipboard.getEntry":
|
|
||||||
handleGetEntry(conn, req, m)
|
|
||||||
case "clipboard.deleteEntry":
|
|
||||||
handleDeleteEntry(conn, req, m)
|
|
||||||
case "clipboard.clearHistory":
|
|
||||||
handleClearHistory(conn, req, m)
|
|
||||||
case "clipboard.copy":
|
|
||||||
handleCopy(conn, req, m)
|
|
||||||
case "clipboard.copyEntry":
|
|
||||||
handleCopyEntry(conn, req, m)
|
|
||||||
case "clipboard.paste":
|
|
||||||
handlePaste(conn, req, m)
|
|
||||||
case "clipboard.subscribe":
|
|
||||||
handleSubscribe(conn, req, m)
|
|
||||||
case "clipboard.search":
|
|
||||||
handleSearch(conn, req, m)
|
|
||||||
case "clipboard.getConfig":
|
|
||||||
handleGetConfig(conn, req, m)
|
|
||||||
case "clipboard.setConfig":
|
|
||||||
handleSetConfig(conn, req, m)
|
|
||||||
case "clipboard.store":
|
|
||||||
handleStore(conn, req, m)
|
|
||||||
default:
|
|
||||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
models.Respond(conn, req.ID, m.GetState())
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
history := m.GetHistory()
|
|
||||||
for i := range history {
|
|
||||||
history[i].Data = nil
|
|
||||||
}
|
|
||||||
models.Respond(conn, req.ID, history)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
id, err := params.Int(req.Params, "id")
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err := m.GetEntry(uint64(id))
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
id, err := params.Int(req.Params, "id")
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.DeleteEntry(uint64(id)); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleClearHistory(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
m.ClearHistory()
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCopy(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
text, err := params.String(req.Params, "text")
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.CopyText(text); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
id, err := params.Int(req.Params, "id")
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err := m.GetEntry(uint64(id))
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.SetClipboard(entry.Data, entry.MimeType); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePaste(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
text, err := m.PasteText()
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, map[string]string{"text": text})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
clientID := fmt.Sprintf("clipboard-%d", req.ID)
|
|
||||||
|
|
||||||
ch := m.Subscribe(clientID)
|
|
||||||
defer m.Unsubscribe(clientID)
|
|
||||||
|
|
||||||
initialState := m.GetState()
|
|
||||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: &initialState,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for state := range ch {
|
|
||||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: &state,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSearch(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
p := SearchParams{
|
|
||||||
Query: params.StringOpt(req.Params, "query", ""),
|
|
||||||
MimeType: params.StringOpt(req.Params, "mimeType", ""),
|
|
||||||
Limit: params.IntOpt(req.Params, "limit", 50),
|
|
||||||
Offset: params.IntOpt(req.Params, "offset", 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
if img, ok := req.Params["isImage"].(bool); ok {
|
|
||||||
p.IsImage = &img
|
|
||||||
}
|
|
||||||
if b, ok := req.Params["before"].(float64); ok {
|
|
||||||
v := int64(b)
|
|
||||||
p.Before = &v
|
|
||||||
}
|
|
||||||
if a, ok := req.Params["after"].(float64); ok {
|
|
||||||
v := int64(a)
|
|
||||||
p.After = &v
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, m.Search(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
models.Respond(conn, req.ID, m.GetConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
cfg := m.GetConfig()
|
|
||||||
|
|
||||||
if _, ok := req.Params["maxHistory"]; ok {
|
|
||||||
cfg.MaxHistory = params.IntOpt(req.Params, "maxHistory", cfg.MaxHistory)
|
|
||||||
}
|
|
||||||
if _, ok := req.Params["maxEntrySize"]; ok {
|
|
||||||
cfg.MaxEntrySize = int64(params.IntOpt(req.Params, "maxEntrySize", int(cfg.MaxEntrySize)))
|
|
||||||
}
|
|
||||||
if _, ok := req.Params["autoClearDays"]; ok {
|
|
||||||
cfg.AutoClearDays = params.IntOpt(req.Params, "autoClearDays", cfg.AutoClearDays)
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["clearAtStartup"].(bool); ok {
|
|
||||||
cfg.ClearAtStartup = v
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["disabled"].(bool); ok {
|
|
||||||
cfg.Disabled = v
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
|
||||||
cfg.DisableHistory = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.SetConfig(cfg); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
|
||||||
data, err := params.String(req.Params, "data")
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeType := params.StringOpt(req.Params, "mimeType", "text/plain;charset=utf-8")
|
|
||||||
|
|
||||||
if err := m.StoreData([]byte(data), mimeType); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,455 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
|
|
||||||
mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEncodeDecodeEntry_Roundtrip(t *testing.T) {
|
|
||||||
original := Entry{
|
|
||||||
ID: 12345,
|
|
||||||
Data: []byte("hello world"),
|
|
||||||
MimeType: "text/plain;charset=utf-8",
|
|
||||||
Preview: "hello world",
|
|
||||||
Size: 11,
|
|
||||||
Timestamp: time.Now().Truncate(time.Second),
|
|
||||||
IsImage: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := encodeEntry(original)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
decoded, err := decodeEntry(encoded)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, original.ID, decoded.ID)
|
|
||||||
assert.Equal(t, original.Data, decoded.Data)
|
|
||||||
assert.Equal(t, original.MimeType, decoded.MimeType)
|
|
||||||
assert.Equal(t, original.Preview, decoded.Preview)
|
|
||||||
assert.Equal(t, original.Size, decoded.Size)
|
|
||||||
assert.Equal(t, original.Timestamp.Unix(), decoded.Timestamp.Unix())
|
|
||||||
assert.Equal(t, original.IsImage, decoded.IsImage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodeDecodeEntry_Image(t *testing.T) {
|
|
||||||
original := Entry{
|
|
||||||
ID: 99999,
|
|
||||||
Data: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
|
|
||||||
MimeType: "image/png",
|
|
||||||
Preview: "[[ image 8 B png 100x100 ]]",
|
|
||||||
Size: 8,
|
|
||||||
Timestamp: time.Now().Truncate(time.Second),
|
|
||||||
IsImage: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := encodeEntry(original)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
decoded, err := decodeEntry(encoded)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, original.ID, decoded.ID)
|
|
||||||
assert.Equal(t, original.Data, decoded.Data)
|
|
||||||
assert.True(t, decoded.IsImage)
|
|
||||||
assert.Equal(t, original.Preview, decoded.Preview)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodeDecodeEntry_EmptyData(t *testing.T) {
|
|
||||||
original := Entry{
|
|
||||||
ID: 1,
|
|
||||||
Data: []byte{},
|
|
||||||
MimeType: "text/plain",
|
|
||||||
Preview: "",
|
|
||||||
Size: 0,
|
|
||||||
Timestamp: time.Now().Truncate(time.Second),
|
|
||||||
IsImage: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := encodeEntry(original)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
decoded, err := decodeEntry(encoded)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, original.ID, decoded.ID)
|
|
||||||
assert.Empty(t, decoded.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodeDecodeEntry_LargeData(t *testing.T) {
|
|
||||||
largeData := make([]byte, 100000)
|
|
||||||
for i := range largeData {
|
|
||||||
largeData[i] = byte(i % 256)
|
|
||||||
}
|
|
||||||
|
|
||||||
original := Entry{
|
|
||||||
ID: 777,
|
|
||||||
Data: largeData,
|
|
||||||
MimeType: "application/octet-stream",
|
|
||||||
Preview: "binary data...",
|
|
||||||
Size: len(largeData),
|
|
||||||
Timestamp: time.Now().Truncate(time.Second),
|
|
||||||
IsImage: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := encodeEntry(original)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
decoded, err := decodeEntry(encoded)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, original.Data, decoded.Data)
|
|
||||||
assert.Equal(t, original.Size, decoded.Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateEqual_BothNil(t *testing.T) {
|
|
||||||
assert.False(t, stateEqual(nil, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateEqual_OneNil(t *testing.T) {
|
|
||||||
s := &State{Enabled: true}
|
|
||||||
assert.False(t, stateEqual(s, nil))
|
|
||||||
assert.False(t, stateEqual(nil, s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateEqual_EnabledDiffers(t *testing.T) {
|
|
||||||
a := &State{Enabled: true, History: []Entry{}}
|
|
||||||
b := &State{Enabled: false, History: []Entry{}}
|
|
||||||
assert.False(t, stateEqual(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateEqual_HistoryLengthDiffers(t *testing.T) {
|
|
||||||
a := &State{Enabled: true, History: []Entry{{ID: 1}}}
|
|
||||||
b := &State{Enabled: true, History: []Entry{}}
|
|
||||||
assert.False(t, stateEqual(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateEqual_BothEqual(t *testing.T) {
|
|
||||||
a := &State{Enabled: true, History: []Entry{{ID: 1}, {ID: 2}}}
|
|
||||||
b := &State{Enabled: true, History: []Entry{{ID: 3}, {ID: 4}}}
|
|
||||||
assert.True(t, stateEqual(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
subscribers: make(map[string]chan State),
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
subID := string(rune('a' + id))
|
|
||||||
ch := m.Subscribe(subID)
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
m.Unsubscribe(subID)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentGetState(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
state: &State{
|
|
||||||
Enabled: true,
|
|
||||||
History: []Entry{{ID: 1}, {ID: 2}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s := m.GetState()
|
|
||||||
_ = s.Enabled
|
|
||||||
_ = len(s.History)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &State{
|
|
||||||
Enabled: j%2 == 0,
|
|
||||||
History: []Entry{{ID: uint64(j)}},
|
|
||||||
}
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentConfigAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
config: DefaultConfig(),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
cfg := m.getConfig()
|
|
||||||
_ = cfg.MaxHistory
|
|
||||||
_ = cfg.MaxEntrySize
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.configMutex.Lock()
|
|
||||||
m.config.MaxHistory = 50 + j
|
|
||||||
m.config.MaxEntrySize = int64(1024 * j)
|
|
||||||
m.configMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, m.dirty, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentOfferAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
offerMimeTypes: make(map[any][]string),
|
|
||||||
offerRegistry: make(map[uint32]any),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.offerMutex.Lock()
|
|
||||||
m.offerRegistry[key] = struct{}{}
|
|
||||||
m.offerMimeTypes[key] = []string{"text/plain"}
|
|
||||||
m.offerMutex.Unlock()
|
|
||||||
|
|
||||||
m.offerMutex.RLock()
|
|
||||||
_ = m.offerRegistry[key]
|
|
||||||
_ = m.offerMimeTypes[key]
|
|
||||||
m.offerMutex.RUnlock()
|
|
||||||
|
|
||||||
m.offerMutex.Lock()
|
|
||||||
delete(m.offerRegistry, key)
|
|
||||||
delete(m.offerMimeTypes, key)
|
|
||||||
m.offerMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestItob(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input uint64
|
|
||||||
expected []byte
|
|
||||||
}{
|
|
||||||
{0, []byte{0, 0, 0, 0, 0, 0, 0, 0}},
|
|
||||||
{1, []byte{0, 0, 0, 0, 0, 0, 0, 1}},
|
|
||||||
{256, []byte{0, 0, 0, 0, 0, 0, 1, 0}},
|
|
||||||
{0xFFFFFFFFFFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := itob(tt.input)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSizeStr(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input int
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{0, "0 B"},
|
|
||||||
{100, "100 B"},
|
|
||||||
{1024, "1 KiB"},
|
|
||||||
{2048, "2 KiB"},
|
|
||||||
{1048576, "1 MiB"},
|
|
||||||
{5242880, "5 MiB"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := sizeStr(tt.input)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSelectMimeType(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
mimes []string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
|
|
||||||
{[]string{"text/html", "text/plain"}, "text/plain"},
|
|
||||||
{[]string{"application/json", "image/png"}, "image/png"},
|
|
||||||
{[]string{"application/json", "application/xml"}, ""},
|
|
||||||
{[]string{}, ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := m.selectMimeType(tt.mimes)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsImageMimeType(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
assert.True(t, m.isImageMimeType("image/png"))
|
|
||||||
assert.True(t, m.isImageMimeType("image/jpeg"))
|
|
||||||
assert.True(t, m.isImageMimeType("image/gif"))
|
|
||||||
assert.False(t, m.isImageMimeType("text/plain"))
|
|
||||||
assert.False(t, m.isImageMimeType("application/json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextPreview(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
short := m.textPreview([]byte("hello world"))
|
|
||||||
assert.Equal(t, "hello world", short)
|
|
||||||
|
|
||||||
withWhitespace := m.textPreview([]byte(" hello world "))
|
|
||||||
assert.Equal(t, "hello world", withWhitespace)
|
|
||||||
|
|
||||||
longText := make([]byte, 200)
|
|
||||||
for i := range longText {
|
|
||||||
longText[i] = 'a'
|
|
||||||
}
|
|
||||||
preview := m.textPreview(longText)
|
|
||||||
assert.True(t, len(preview) > 100)
|
|
||||||
assert.Contains(t, preview, "…")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultConfig(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
assert.Equal(t, 100, cfg.MaxHistory)
|
|
||||||
assert.Equal(t, int64(5*1024*1024), cfg.MaxEntrySize)
|
|
||||||
assert.Equal(t, 0, cfg.AutoClearDays)
|
|
||||||
assert.False(t, cfg.ClearAtStartup)
|
|
||||||
assert.False(t, cfg.Disabled)
|
|
||||||
assert.False(t, cfg.DisableHistory)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PostDelegatesToWlContext(t *testing.T) {
|
|
||||||
mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
|
|
||||||
|
|
||||||
var called atomic.Bool
|
|
||||||
mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
|
|
||||||
called.Store(true)
|
|
||||||
fn()
|
|
||||||
}).Once()
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
wlCtx: mockCtx,
|
|
||||||
}
|
|
||||||
|
|
||||||
executed := false
|
|
||||||
m.post(func() {
|
|
||||||
executed = true
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.True(t, called.Load())
|
|
||||||
assert.True(t, executed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PostExecutesFunctionViaContext(t *testing.T) {
|
|
||||||
mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
|
|
||||||
|
|
||||||
var capturedFn func()
|
|
||||||
mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
|
|
||||||
capturedFn = fn
|
|
||||||
}).Times(3)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
wlCtx: mockCtx,
|
|
||||||
}
|
|
||||||
|
|
||||||
counter := 0
|
|
||||||
m.post(func() { counter++ })
|
|
||||||
m.post(func() { counter += 10 })
|
|
||||||
m.post(func() { counter += 100 })
|
|
||||||
|
|
||||||
assert.NotNil(t, capturedFn)
|
|
||||||
capturedFn()
|
|
||||||
assert.Equal(t, 100, counter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentPostWithMock(t *testing.T) {
|
|
||||||
mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
|
|
||||||
|
|
||||||
var postCount atomic.Int32
|
|
||||||
mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
|
|
||||||
postCount.Add(1)
|
|
||||||
}).Times(100)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
wlCtx: mockCtx,
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < 10; j++ {
|
|
||||||
m.post(func() {})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
assert.Equal(t, int32(100), postCount.Load())
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
MaxHistory int `json:"maxHistory"`
|
|
||||||
MaxEntrySize int64 `json:"maxEntrySize"`
|
|
||||||
AutoClearDays int `json:"autoClearDays"`
|
|
||||||
ClearAtStartup bool `json:"clearAtStartup"`
|
|
||||||
|
|
||||||
Disabled bool `json:"disabled"`
|
|
||||||
DisableHistory bool `json:"disableHistory"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
|
||||||
return Config{
|
|
||||||
MaxHistory: 100,
|
|
||||||
MaxEntrySize: 5 * 1024 * 1024,
|
|
||||||
AutoClearDays: 0,
|
|
||||||
ClearAtStartup: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigPath() (string, error) {
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(configDir, "DankMaterialShell", "clsettings.json"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig() Config {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
|
|
||||||
path, err := getConfigPath()
|
|
||||||
if err != nil {
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
||||||
return DefaultConfig()
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveConfig(cfg Config) error {
|
|
||||||
path, err := getConfigPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(cfg, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchParams struct {
|
|
||||||
Query string `json:"query"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
IsImage *bool `json:"isImage"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
Before *int64 `json:"before"`
|
|
||||||
After *int64 `json:"after"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchResult struct {
|
|
||||||
Entries []Entry `json:"entries"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
HasMore bool `json:"hasMore"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry struct {
|
|
||||||
ID uint64 `json:"id"`
|
|
||||||
Data []byte `json:"data,omitempty"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
Preview string `json:"preview"`
|
|
||||||
Size int `json:"size"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
IsImage bool `json:"isImage"`
|
|
||||||
Hash uint64 `json:"hash,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
History []Entry `json:"history"`
|
|
||||||
Current *Entry `json:"current,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
config Config
|
|
||||||
configMutex sync.RWMutex
|
|
||||||
configPath string
|
|
||||||
|
|
||||||
display wlclient.WaylandDisplay
|
|
||||||
wlCtx wlcontext.WaylandContext
|
|
||||||
|
|
||||||
registry *wlclient.Registry
|
|
||||||
dataControlMgr any
|
|
||||||
seat *wlclient.Seat
|
|
||||||
dataDevice any
|
|
||||||
currentOffer any
|
|
||||||
currentSource any
|
|
||||||
seatName uint32
|
|
||||||
mimeTypes []string
|
|
||||||
offerMimeTypes map[any][]string
|
|
||||||
offerMutex sync.RWMutex
|
|
||||||
offerRegistry map[uint32]any
|
|
||||||
|
|
||||||
sourceMimeTypes []string
|
|
||||||
sourceMutex sync.RWMutex
|
|
||||||
|
|
||||||
initialized bool
|
|
||||||
|
|
||||||
alive bool
|
|
||||||
stopChan chan struct{}
|
|
||||||
|
|
||||||
db *bolt.DB
|
|
||||||
dbPath string
|
|
||||||
|
|
||||||
state *State
|
|
||||||
stateMutex sync.RWMutex
|
|
||||||
|
|
||||||
subscribers map[string]chan State
|
|
||||||
subMutex sync.RWMutex
|
|
||||||
dirty chan struct{}
|
|
||||||
notifierWg sync.WaitGroup
|
|
||||||
lastState *State
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) GetState() State {
|
|
||||||
m.stateMutex.RLock()
|
|
||||||
defer m.stateMutex.RUnlock()
|
|
||||||
if m.state == nil {
|
|
||||||
return State{}
|
|
||||||
}
|
|
||||||
return *m.state
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Subscribe(id string) chan State {
|
|
||||||
ch := make(chan State, 64)
|
|
||||||
m.subMutex.Lock()
|
|
||||||
m.subscribers[id] = ch
|
|
||||||
m.subMutex.Unlock()
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Unsubscribe(id string) {
|
|
||||||
m.subMutex.Lock()
|
|
||||||
if ch, ok := m.subscribers[id]; ok {
|
|
||||||
close(ch)
|
|
||||||
delete(m.subscribers, id)
|
|
||||||
}
|
|
||||||
m.subMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) notifySubscribers() {
|
|
||||||
select {
|
|
||||||
case m.dirty <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
func NewManager(display *wlclient.Display) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
package dwl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStateChanged_BothNil(t *testing.T) {
|
|
||||||
assert.True(t, stateChanged(nil, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OneNil(t *testing.T) {
|
|
||||||
s := &State{TagCount: 9}
|
|
||||||
assert.True(t, stateChanged(s, nil))
|
|
||||||
assert.True(t, stateChanged(nil, s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_TagCountDiffers(t *testing.T) {
|
|
||||||
a := &State{TagCount: 9, Outputs: make(map[string]*OutputState), Layouts: []string{}}
|
|
||||||
b := &State{TagCount: 10, Outputs: make(map[string]*OutputState), Layouts: []string{}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_LayoutLengthDiffers(t *testing.T) {
|
|
||||||
a := &State{TagCount: 9, Layouts: []string{"tile"}, Outputs: make(map[string]*OutputState)}
|
|
||||||
b := &State{TagCount: 9, Layouts: []string{"tile", "monocle"}, Outputs: make(map[string]*OutputState)}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_ActiveOutputDiffers(t *testing.T) {
|
|
||||||
a := &State{TagCount: 9, ActiveOutput: "eDP-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
|
|
||||||
b := &State{TagCount: 9, ActiveOutput: "HDMI-A-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputCountDiffers(t *testing.T) {
|
|
||||||
a := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Outputs: map[string]*OutputState{"eDP-1": {}},
|
|
||||||
Layouts: []string{},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Outputs: map[string]*OutputState{},
|
|
||||||
Layouts: []string{},
|
|
||||||
}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputFieldsDiffer(t *testing.T) {
|
|
||||||
a := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Active: 1, Layout: 0, Title: "Firefox"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Active: 0, Layout: 0, Title: "Firefox"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Outputs["eDP-1"].Active = 1
|
|
||||||
b.Outputs["eDP-1"].Layout = 1
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Outputs["eDP-1"].Layout = 0
|
|
||||||
b.Outputs["eDP-1"].Title = "Code"
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_TagsDiffer(t *testing.T) {
|
|
||||||
a := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Tags: []TagState{{Tag: 1, State: 2, Clients: 2, Focused: 1}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Outputs["eDP-1"].Tags[0].State = 1
|
|
||||||
b.Outputs["eDP-1"].Tags[0].Clients = 3
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_Equal(t *testing.T) {
|
|
||||||
a := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
ActiveOutput: "eDP-1",
|
|
||||||
Layouts: []string{"tile", "monocle"},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {
|
|
||||||
Name: "eDP-1",
|
|
||||||
Active: 1,
|
|
||||||
Layout: 0,
|
|
||||||
LayoutSymbol: "[]=",
|
|
||||||
Title: "Firefox",
|
|
||||||
AppID: "firefox",
|
|
||||||
KbLayout: "us",
|
|
||||||
Keymode: "",
|
|
||||||
Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
ActiveOutput: "eDP-1",
|
|
||||||
Layouts: []string{"tile", "monocle"},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {
|
|
||||||
Name: "eDP-1",
|
|
||||||
Active: 1,
|
|
||||||
Layout: 0,
|
|
||||||
LayoutSymbol: "[]=",
|
|
||||||
Title: "Firefox",
|
|
||||||
AppID: "firefox",
|
|
||||||
KbLayout: "us",
|
|
||||||
Keymode: "",
|
|
||||||
Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.False(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentGetState(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
state: &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{"tile"},
|
|
||||||
Outputs: map[string]*OutputState{"eDP-1": {Name: "eDP-1"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s := m.GetState()
|
|
||||||
_ = s.TagCount
|
|
||||||
_ = s.Outputs
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &State{
|
|
||||||
TagCount: uint32(j % 10),
|
|
||||||
Layouts: []string{"tile", "monocle"},
|
|
||||||
Outputs: map[string]*OutputState{"eDP-1": {Active: uint32(j % 2)}},
|
|
||||||
}
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
subID := string(rune('a' + id))
|
|
||||||
ch := m.Subscribe(subID)
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
m.Unsubscribe(subID)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &outputState{
|
|
||||||
id: key,
|
|
||||||
name: "test-output",
|
|
||||||
active: uint32(j % 2),
|
|
||||||
tags: []TagState{{Tag: uint32(j), State: 1}},
|
|
||||||
}
|
|
||||||
m.outputs.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.outputs.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputs.Range(func(k uint32, v *outputState) bool {
|
|
||||||
_ = v.name
|
|
||||||
_ = v.active
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputs.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, m.dirty, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PostQueueFull(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
cmdq: make(chan cmd, 2),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
|
|
||||||
assert.Len(t, m.cmdq, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_GetStateNilState(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
s := m.GetState()
|
|
||||||
assert.NotNil(t, s.Outputs)
|
|
||||||
assert.NotNil(t, s.Layouts)
|
|
||||||
assert.Equal(t, uint32(0), s.TagCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTagState_Fields(t *testing.T) {
|
|
||||||
tag := TagState{
|
|
||||||
Tag: 1,
|
|
||||||
State: 2,
|
|
||||||
Clients: 3,
|
|
||||||
Focused: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, uint32(1), tag.Tag)
|
|
||||||
assert.Equal(t, uint32(2), tag.State)
|
|
||||||
assert.Equal(t, uint32(3), tag.Clients)
|
|
||||||
assert.Equal(t, uint32(1), tag.Focused)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputState_Fields(t *testing.T) {
|
|
||||||
out := OutputState{
|
|
||||||
Name: "eDP-1",
|
|
||||||
Active: 1,
|
|
||||||
Tags: []TagState{{Tag: 1}},
|
|
||||||
Layout: 0,
|
|
||||||
LayoutSymbol: "[]=",
|
|
||||||
Title: "Firefox",
|
|
||||||
AppID: "firefox",
|
|
||||||
KbLayout: "us",
|
|
||||||
Keymode: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "eDP-1", out.Name)
|
|
||||||
assert.Equal(t, uint32(1), out.Active)
|
|
||||||
assert.Len(t, out.Tags, 1)
|
|
||||||
assert.Equal(t, "[]=", out.LayoutSymbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_NewOutputAppears(t *testing.T) {
|
|
||||||
a := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Name: "eDP-1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Name: "eDP-1"},
|
|
||||||
"HDMI-A-1": {Name: "HDMI-A-1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_TagsLengthDiffers(t *testing.T) {
|
|
||||||
a := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Tags: []TagState{{Tag: 1}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
TagCount: 9,
|
|
||||||
Layouts: []string{},
|
|
||||||
Outputs: map[string]*OutputState{
|
|
||||||
"eDP-1": {Tags: []TagState{{Tag: 1}, {Tag: 2}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "failed to get registry")
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@ type cmd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
display wlclient.WaylandDisplay
|
display *wlclient.Display
|
||||||
ctx *wlclient.Context
|
ctx *wlclient.Context
|
||||||
registry *wlclient.Registry
|
registry *wlclient.Registry
|
||||||
manager any
|
manager any
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func CheckCapability() bool {
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
func NewManager(display *wlclient.Display) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
|
|||||||
@@ -1,392 +0,0 @@
|
|||||||
package extworkspace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStateChanged_BothNil(t *testing.T) {
|
|
||||||
assert.True(t, stateChanged(nil, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OneNil(t *testing.T) {
|
|
||||||
s := &State{Groups: []*WorkspaceGroup{}}
|
|
||||||
assert.True(t, stateChanged(s, nil))
|
|
||||||
assert.True(t, stateChanged(nil, s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_GroupCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1"}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_GroupIDDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{ID: "group-2", Outputs: []string{}, Workspaces: []*Workspace{}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputNameDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"HDMI-A-1"}, Workspaces: []*Workspace{}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_WorkspaceCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{ID: "1", Name: "ws1"}},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_WorkspaceFieldsDiffer(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "1", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false,
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "2", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false,
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].ID = "1"
|
|
||||||
b.Groups[0].Workspaces[0].Name = "ws2"
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Name = "ws1"
|
|
||||||
b.Groups[0].Workspaces[0].State = 1
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].State = 0
|
|
||||||
b.Groups[0].Workspaces[0].Active = true
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Active = false
|
|
||||||
b.Groups[0].Workspaces[0].Urgent = true
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Urgent = false
|
|
||||||
b.Groups[0].Workspaces[0].Hidden = true
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_WorkspaceCoordinatesDiffer(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "1", Name: "ws1", Coordinates: []uint32{0, 0},
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "1", Name: "ws1", Coordinates: []uint32{1, 0},
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Coordinates = []uint32{0}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_Equal(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{"eDP-1", "HDMI-A-1"},
|
|
||||||
Workspaces: []*Workspace{
|
|
||||||
{ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true},
|
|
||||||
{ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false},
|
|
||||||
},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{"eDP-1", "HDMI-A-1"},
|
|
||||||
Workspaces: []*Workspace{
|
|
||||||
{ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true},
|
|
||||||
{ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false},
|
|
||||||
},
|
|
||||||
}}}
|
|
||||||
assert.False(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentGetState(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
state: &State{
|
|
||||||
Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s := m.GetState()
|
|
||||||
_ = s.Groups
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &State{
|
|
||||||
Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}},
|
|
||||||
}
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
subID := string(rune('a' + id))
|
|
||||||
ch := m.Subscribe(subID)
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
m.Unsubscribe(subID)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapGroupsConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &workspaceGroupState{
|
|
||||||
id: key,
|
|
||||||
outputIDs: map[uint32]bool{1: true},
|
|
||||||
workspaceIDs: []uint32{uint32(j)},
|
|
||||||
}
|
|
||||||
m.groups.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.groups.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.groups.Range(func(k uint32, v *workspaceGroupState) bool {
|
|
||||||
_ = v.id
|
|
||||||
_ = v.outputIDs
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.groups.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapWorkspacesConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &workspaceState{
|
|
||||||
id: key,
|
|
||||||
workspaceID: "ws-1",
|
|
||||||
name: "workspace",
|
|
||||||
state: uint32(j % 4),
|
|
||||||
coordinates: []uint32{uint32(j), 0},
|
|
||||||
}
|
|
||||||
m.workspaces.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.workspaces.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.workspaces.Range(func(k uint32, v *workspaceState) bool {
|
|
||||||
_ = v.name
|
|
||||||
_ = v.state
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.workspaces.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapOutputNamesConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.outputNames.Store(key, "eDP-1")
|
|
||||||
|
|
||||||
if loaded, ok := m.outputNames.Load(key); ok {
|
|
||||||
assert.NotEmpty(t, loaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputNames.Range(func(k uint32, v string) bool {
|
|
||||||
_ = v
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputNames.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, m.dirty, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PostQueueFull(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
cmdq: make(chan cmd, 2),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
|
|
||||||
assert.Len(t, m.cmdq, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_GetStateNilState(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
s := m.GetState()
|
|
||||||
assert.NotNil(t, s.Groups)
|
|
||||||
assert.Empty(t, s.Groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkspace_Fields(t *testing.T) {
|
|
||||||
ws := Workspace{
|
|
||||||
ID: "ws-1",
|
|
||||||
Name: "workspace 1",
|
|
||||||
Coordinates: []uint32{0, 0},
|
|
||||||
State: 1,
|
|
||||||
Active: true,
|
|
||||||
Urgent: false,
|
|
||||||
Hidden: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "ws-1", ws.ID)
|
|
||||||
assert.Equal(t, "workspace 1", ws.Name)
|
|
||||||
assert.True(t, ws.Active)
|
|
||||||
assert.False(t, ws.Urgent)
|
|
||||||
assert.False(t, ws.Hidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkspaceGroup_Fields(t *testing.T) {
|
|
||||||
group := WorkspaceGroup{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{"eDP-1", "HDMI-A-1"},
|
|
||||||
Workspaces: []*Workspace{
|
|
||||||
{ID: "ws-1", Name: "workspace 1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "group-1", group.ID)
|
|
||||||
assert.Len(t, group.Outputs, 2)
|
|
||||||
assert.Len(t, group.Workspaces, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "failed to get registry")
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ type cmd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
display wlclient.WaylandDisplay
|
display *wlclient.Display
|
||||||
ctx *wlclient.Context
|
ctx *wlclient.Context
|
||||||
registry *wlclient.Registry
|
registry *wlclient.Registry
|
||||||
manager *ext_workspace.ExtWorkspaceManagerV1
|
manager *ext_workspace.ExtWorkspaceManagerV1
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
|
|||||||
StockColors: getString("stockColors"),
|
StockColors: getString("stockColors"),
|
||||||
SyncModeWithPortal: getBool("syncModeWithPortal", false),
|
SyncModeWithPortal: getBool("syncModeWithPortal", false),
|
||||||
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
|
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
|
||||||
SkipTemplates: getString("skipTemplates"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wait := getBool("wait", true)
|
wait := getBool("wait", true)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"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"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
@@ -148,24 +147,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "clipboard.") {
|
|
||||||
switch req.Method {
|
|
||||||
case "clipboard.getConfig":
|
|
||||||
cfg := clipboard.LoadConfig()
|
|
||||||
models.Respond(conn, req.ID, cfg)
|
|
||||||
return
|
|
||||||
case "clipboard.setConfig":
|
|
||||||
handleClipboardSetConfig(conn, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if clipboardManager == nil {
|
|
||||||
models.RespondError(conn, req.ID, "clipboard manager not initialized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clipboard.HandleRequest(conn, req, clipboardManager)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "ping":
|
case "ping":
|
||||||
models.Respond(conn, req.ID, "pong")
|
models.Respond(conn, req.ID, "pong")
|
||||||
@@ -182,33 +163,3 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClipboardSetConfig(conn net.Conn, req models.Request) {
|
|
||||||
cfg := clipboard.LoadConfig()
|
|
||||||
|
|
||||||
if v, ok := req.Params["maxHistory"].(float64); ok {
|
|
||||||
cfg.MaxHistory = int(v)
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["maxEntrySize"].(float64); ok {
|
|
||||||
cfg.MaxEntrySize = int64(v)
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["autoClearDays"].(float64); ok {
|
|
||||||
cfg.AutoClearDays = int(v)
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["clearAtStartup"].(bool); ok {
|
|
||||||
cfg.ClearAtStartup = v
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["disabled"].(bool); ok {
|
|
||||||
cfg.Disabled = v
|
|
||||||
}
|
|
||||||
if v, ok := req.Params["disableHistory"].(bool); ok {
|
|
||||||
cfg.DisableHistory = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.SaveConfig(cfg); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"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"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
@@ -33,7 +32,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APIVersion = 23
|
const APIVersion = 22
|
||||||
|
|
||||||
var CLIVersion = "dev"
|
var CLIVersion = "dev"
|
||||||
|
|
||||||
@@ -64,7 +63,6 @@ var extWorkspaceManager *extworkspace.Manager
|
|||||||
var brightnessManager *brightness.Manager
|
var brightnessManager *brightness.Manager
|
||||||
var wlrOutputManager *wlroutput.Manager
|
var wlrOutputManager *wlroutput.Manager
|
||||||
var evdevManager *evdev.Manager
|
var evdevManager *evdev.Manager
|
||||||
var clipboardManager *clipboard.Manager
|
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
|
|
||||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||||
@@ -338,31 +336,6 @@ func InitializeEvdevManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeClipboardManager() error {
|
|
||||||
log.Info("Attempting to initialize clipboard manager...")
|
|
||||||
|
|
||||||
if wlContext == nil {
|
|
||||||
ctx, err := wlcontext.New()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create shared Wayland context: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wlContext = ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
config := clipboard.LoadConfig()
|
|
||||||
manager, err := clipboard.NewManager(wlContext, config)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to initialize clipboard manager: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
clipboardManager = manager
|
|
||||||
|
|
||||||
log.Info("Clipboard manager initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleConnection(conn net.Conn) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -436,10 +409,6 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "evdev")
|
caps = append(caps, "evdev")
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipboardManager != nil {
|
|
||||||
caps = append(caps, "clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Capabilities{Capabilities: caps}
|
return Capabilities{Capabilities: caps}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,10 +463,6 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "evdev")
|
caps = append(caps, "evdev")
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipboardManager != nil {
|
|
||||||
caps = append(caps, "clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerInfo{
|
return ServerInfo{
|
||||||
APIVersion: APIVersion,
|
APIVersion: APIVersion,
|
||||||
CLIVersion: CLIVersion,
|
CLIVersion: CLIVersion,
|
||||||
@@ -1069,38 +1034,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldSubscribe("clipboard") && clipboardManager != nil {
|
|
||||||
wg.Add(1)
|
|
||||||
clipboardChan := clipboardManager.Subscribe(clientID + "-clipboard")
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer clipboardManager.Unsubscribe(clientID + "-clipboard")
|
|
||||||
|
|
||||||
initialState := clipboardManager.GetState()
|
|
||||||
select {
|
|
||||||
case eventChan <- ServiceEvent{Service: "clipboard", Data: initialState}:
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case state, ok := <-clipboardChan:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case eventChan <- ServiceEvent{Service: "clipboard", Data: state}:
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(eventChan)
|
close(eventChan)
|
||||||
@@ -1163,9 +1096,6 @@ func cleanupManagers() {
|
|||||||
if evdevManager != nil {
|
if evdevManager != nil {
|
||||||
evdevManager.Close()
|
evdevManager.Close()
|
||||||
}
|
}
|
||||||
if clipboardManager != nil {
|
|
||||||
clipboardManager.Close()
|
|
||||||
}
|
|
||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
@@ -1329,18 +1259,6 @@ func Start(printDocs bool) error {
|
|||||||
log.Info("Evdev:")
|
log.Info("Evdev:")
|
||||||
log.Info(" evdev.getState - Get current evdev state (caps lock)")
|
log.Info(" evdev.getState - Get current evdev state (caps lock)")
|
||||||
log.Info(" evdev.subscribe - Subscribe to evdev state changes (streaming)")
|
log.Info(" evdev.subscribe - Subscribe to evdev state changes (streaming)")
|
||||||
log.Info("Clipboard:")
|
|
||||||
log.Info(" clipboard.getState - Get clipboard state (enabled, history, current)")
|
|
||||||
log.Info(" clipboard.getHistory - Get clipboard history with previews")
|
|
||||||
log.Info(" clipboard.getEntry - Get full entry by ID (params: id)")
|
|
||||||
log.Info(" clipboard.deleteEntry - Delete entry by ID (params: id)")
|
|
||||||
log.Info(" clipboard.clearHistory - Clear all clipboard history")
|
|
||||||
log.Info(" clipboard.copy - Copy text to clipboard (params: text)")
|
|
||||||
log.Info(" clipboard.paste - Get current clipboard text")
|
|
||||||
log.Info(" clipboard.search - Search history (params: query?, mimeType?, isImage?, limit?, offset?, before?, after?)")
|
|
||||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
|
||||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
|
||||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
|
||||||
log.Info("")
|
log.Info("")
|
||||||
}
|
}
|
||||||
log.Info("Initializing managers...")
|
log.Info("Initializing managers...")
|
||||||
@@ -1448,15 +1366,10 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
if wlContext != nil {
|
||||||
if err := InitializeClipboardManager(); err != nil {
|
wlContext.Start()
|
||||||
log.Warnf("Clipboard manager unavailable: %v", err)
|
log.Info("Wayland event dispatcher started")
|
||||||
}
|
}
|
||||||
if wlContext != nil {
|
|
||||||
wlContext.Start()
|
|
||||||
log.Info("Wayland event dispatcher started")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Info("")
|
log.Info("")
|
||||||
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
const animKelvinStep = 25
|
const animKelvinStep = 25
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
|
func NewManager(display *wlclient.Display, config Config) (*Manager, error) {
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -268,36 +268,31 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_control.ZwlrGammaControlV1) {
|
func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_control.ZwlrGammaControlV1) {
|
||||||
outputID := state.id
|
|
||||||
|
|
||||||
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
|
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
|
||||||
size := e.Size
|
if out, ok := m.outputs.Load(state.id); ok {
|
||||||
|
out.rampSize = e.Size
|
||||||
|
out.failed = false
|
||||||
|
out.retryCount = 0
|
||||||
|
}
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
if out, ok := m.outputs.Load(outputID); ok {
|
|
||||||
out.rampSize = size
|
|
||||||
out.failed = false
|
|
||||||
out.retryCount = 0
|
|
||||||
}
|
|
||||||
m.applyCurrentTemp()
|
m.applyCurrentTemp()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
control.SetFailedHandler(func(_ wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
|
control.SetFailedHandler(func(_ wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
|
||||||
m.post(func() {
|
out, ok := m.outputs.Load(state.id)
|
||||||
out, ok := m.outputs.Load(outputID)
|
if !ok {
|
||||||
if !ok {
|
return
|
||||||
return
|
}
|
||||||
}
|
out.failed = true
|
||||||
out.failed = true
|
out.rampSize = 0
|
||||||
out.rampSize = 0
|
out.retryCount++
|
||||||
out.retryCount++
|
out.lastFailTime = time.Now()
|
||||||
out.lastFailTime = time.Now()
|
|
||||||
|
|
||||||
backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond
|
backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond
|
||||||
time.AfterFunc(backoff, func() {
|
time.AfterFunc(backoff, func() {
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
m.recreateOutputControl(out)
|
m.recreateOutputControl(out)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -588,7 +583,7 @@ func (m *Manager) schedulerLoop() {
|
|||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
m.post(func() { m.applyCurrentTemp() })
|
m.applyCurrentTemp()
|
||||||
}
|
}
|
||||||
|
|
||||||
var timer *time.Timer
|
var timer *time.Timer
|
||||||
@@ -630,14 +625,14 @@ func (m *Manager) schedulerLoop() {
|
|||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
if enabled {
|
if enabled {
|
||||||
m.post(func() { m.applyCurrentTemp() })
|
m.applyCurrentTemp()
|
||||||
}
|
}
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
if enabled {
|
if enabled {
|
||||||
m.post(func() { m.applyCurrentTemp() })
|
m.applyCurrentTemp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
package wayland
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
cmdq: make(chan cmd, 128),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.wg.Add(1)
|
|
||||||
go m.waylandActor()
|
|
||||||
|
|
||||||
state := &outputState{
|
|
||||||
id: 1,
|
|
||||||
registryName: 100,
|
|
||||||
rampSize: 256,
|
|
||||||
}
|
|
||||||
m.outputs.Store(state.id, state)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.post(func() {
|
|
||||||
if out, ok := m.outputs.Load(state.id); ok {
|
|
||||||
out.rampSize = uint32(j)
|
|
||||||
out.failed = j%2 == 0
|
|
||||||
out.retryCount = j
|
|
||||||
out.lastFailTime = time.Now()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
m.post(func() { close(done) })
|
|
||||||
<-done
|
|
||||||
|
|
||||||
close(m.stopChan)
|
|
||||||
m.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
updateTrigger: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
subID := string(rune('a' + id))
|
|
||||||
ch := m.Subscribe(subID)
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
m.Unsubscribe(subID)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentGetState(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
state: &State{
|
|
||||||
CurrentTemp: 5000,
|
|
||||||
IsDay: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s := m.GetState()
|
|
||||||
assert.GreaterOrEqual(t, s.CurrentTemp, 0)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &State{
|
|
||||||
CurrentTemp: 4000 + i*100,
|
|
||||||
IsDay: j%2 == 0,
|
|
||||||
}
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentConfigAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
config: DefaultConfig(),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.configMutex.RLock()
|
|
||||||
_ = m.config.LowTemp
|
|
||||||
_ = m.config.HighTemp
|
|
||||||
_ = m.config.Enabled
|
|
||||||
m.configMutex.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.configMutex.Lock()
|
|
||||||
m.config.LowTemp = 3000 + j
|
|
||||||
m.config.HighTemp = 7000 - j
|
|
||||||
m.config.Enabled = j%2 == 0
|
|
||||||
m.configMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &outputState{
|
|
||||||
id: key,
|
|
||||||
rampSize: uint32(j),
|
|
||||||
failed: j%2 == 0,
|
|
||||||
}
|
|
||||||
m.outputs.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.outputs.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputs.Range(func(k uint32, v *outputState) bool {
|
|
||||||
_ = v.rampSize
|
|
||||||
_ = v.failed
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputs.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_LocationCacheConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.locationMutex.RLock()
|
|
||||||
_ = m.cachedIPLat
|
|
||||||
_ = m.cachedIPLon
|
|
||||||
m.locationMutex.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
lat := float64(40 + i)
|
|
||||||
lon := float64(-74 + j)
|
|
||||||
m.locationMutex.Lock()
|
|
||||||
m.cachedIPLat = &lat
|
|
||||||
m.cachedIPLon = &lon
|
|
||||||
m.locationMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ScheduleConcurrentAccess(t *testing.T) {
|
|
||||||
now := time.Now()
|
|
||||||
m := &Manager{
|
|
||||||
schedule: sunSchedule{
|
|
||||||
times: SunTimes{
|
|
||||||
Dawn: now,
|
|
||||||
Sunrise: now.Add(time.Hour),
|
|
||||||
Sunset: now.Add(12 * time.Hour),
|
|
||||||
Night: now.Add(13 * time.Hour),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.scheduleMutex.RLock()
|
|
||||||
_ = m.schedule.times.Dawn
|
|
||||||
_ = m.schedule.times.Sunrise
|
|
||||||
_ = m.schedule.times.Sunset
|
|
||||||
_ = m.schedule.condition
|
|
||||||
m.scheduleMutex.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.scheduleMutex.Lock()
|
|
||||||
m.schedule.times.Dawn = time.Now()
|
|
||||||
m.schedule.times.Sunrise = time.Now().Add(time.Hour)
|
|
||||||
m.schedule.condition = SunNormal
|
|
||||||
m.scheduleMutex.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInterpolate_EdgeCases(t *testing.T) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
now time.Time
|
|
||||||
start time.Time
|
|
||||||
stop time.Time
|
|
||||||
expected float64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "same start and stop",
|
|
||||||
now: now,
|
|
||||||
start: now,
|
|
||||||
stop: now,
|
|
||||||
expected: 1.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "now before start",
|
|
||||||
now: now,
|
|
||||||
start: now.Add(time.Hour),
|
|
||||||
stop: now.Add(2 * time.Hour),
|
|
||||||
expected: 0.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "now after stop",
|
|
||||||
now: now.Add(3 * time.Hour),
|
|
||||||
start: now,
|
|
||||||
stop: now.Add(time.Hour),
|
|
||||||
expected: 1.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "now at midpoint",
|
|
||||||
now: now.Add(30 * time.Minute),
|
|
||||||
start: now,
|
|
||||||
stop: now.Add(time.Hour),
|
|
||||||
expected: 0.5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "now equals start",
|
|
||||||
now: now,
|
|
||||||
start: now,
|
|
||||||
stop: now.Add(time.Hour),
|
|
||||||
expected: 0.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "now equals stop",
|
|
||||||
now: now.Add(time.Hour),
|
|
||||||
start: now,
|
|
||||||
stop: now.Add(time.Hour),
|
|
||||||
expected: 1.0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := interpolate(tt.now, tt.start, tt.stop)
|
|
||||||
assert.InDelta(t, tt.expected, result, 0.01)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateGammaRamp_ZeroSize(t *testing.T) {
|
|
||||||
ramp := GenerateGammaRamp(0, 5000, 1.0)
|
|
||||||
assert.Empty(t, ramp.Red)
|
|
||||||
assert.Empty(t, ramp.Green)
|
|
||||||
assert.Empty(t, ramp.Blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateGammaRamp_ValidSizes(t *testing.T) {
|
|
||||||
sizes := []uint32{1, 256, 1024}
|
|
||||||
temps := []int{1000, 4000, 6500, 10000}
|
|
||||||
gammas := []float64{0.5, 1.0, 2.0}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
for _, temp := range temps {
|
|
||||||
for _, gamma := range gammas {
|
|
||||||
ramp := GenerateGammaRamp(size, temp, gamma)
|
|
||||||
assert.Len(t, ramp.Red, int(size))
|
|
||||||
assert.Len(t, ramp.Green, int(size))
|
|
||||||
assert.Len(t, ramp.Blue, int(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifySubscribers_NonBlocking(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, m.dirty, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
|
||||||
|
|
||||||
config := DefaultConfig()
|
|
||||||
_, err := NewManager(mockDisplay, config)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "get registry")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManager_InvalidConfig(t *testing.T) {
|
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
|
||||||
|
|
||||||
config := Config{
|
|
||||||
LowTemp: 500,
|
|
||||||
HighTemp: 6500,
|
|
||||||
Gamma: 1.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay, config)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
@@ -65,7 +65,7 @@ type Manager struct {
|
|||||||
state *State
|
state *State
|
||||||
stateMutex sync.RWMutex
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
display wlclient.WaylandDisplay
|
display *wlclient.Display
|
||||||
ctx *wlclient.Context
|
ctx *wlclient.Context
|
||||||
registry *wlclient.Registry
|
registry *wlclient.Registry
|
||||||
gammaControl any
|
gammaControl any
|
||||||
|
|||||||
@@ -1,32 +1,18 @@
|
|||||||
package wlcontext
|
package wlcontext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WaylandContext interface {
|
|
||||||
Display() *wlclient.Display
|
|
||||||
Post(fn func())
|
|
||||||
FatalError() <-chan error
|
|
||||||
Start()
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ WaylandContext = (*SharedContext)(nil)
|
|
||||||
|
|
||||||
type SharedContext struct {
|
type SharedContext struct {
|
||||||
display *wlclient.Display
|
display *wlclient.Display
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
fatalError chan error
|
fatalError chan error
|
||||||
cmdQueue chan func()
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
started bool
|
started bool
|
||||||
@@ -42,7 +28,6 @@ func New() (*SharedContext, error) {
|
|||||||
display: display,
|
display: display,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
fatalError: make(chan error, 1),
|
fatalError: make(chan error, 1),
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
started: false,
|
started: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +51,6 @@ func (sc *SharedContext) Display() *wlclient.Display {
|
|||||||
return sc.display
|
return sc.display
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *SharedContext) Post(fn func()) {
|
|
||||||
select {
|
|
||||||
case sc.cmdQueue <- fn:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *SharedContext) FatalError() <-chan error {
|
func (sc *SharedContext) FatalError() <-chan error {
|
||||||
return sc.fatalError
|
return sc.fatalError
|
||||||
}
|
}
|
||||||
@@ -96,35 +74,10 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
case <-sc.stopChan:
|
case <-sc.stopChan:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
log.Errorf("Wayland connection error: %v", err)
|
||||||
sc.drainCmdQueue()
|
return
|
||||||
|
}
|
||||||
if err := ctx.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
|
|
||||||
log.Errorf("Failed to set read deadline: %v", err)
|
|
||||||
}
|
|
||||||
err := ctx.Dispatch()
|
|
||||||
if err := ctx.SetReadDeadline(time.Time{}); err != nil {
|
|
||||||
log.Errorf("Failed to clear read deadline: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
case errors.Is(err, os.ErrDeadlineExceeded):
|
|
||||||
default:
|
|
||||||
log.Errorf("Wayland connection error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *SharedContext) drainCmdQueue() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case fn := <-sc.cmdQueue:
|
|
||||||
fn()
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
package wlcontext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSharedContext_ConcurrentPostNonBlocking(t *testing.T) {
|
|
||||||
sc := &SharedContext{
|
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 100
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
sc.Post(func() {
|
|
||||||
_ = id + j
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharedContext_PostQueueFull(t *testing.T) {
|
|
||||||
sc := &SharedContext{
|
|
||||||
cmdQueue: make(chan func(), 2),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.Post(func() {})
|
|
||||||
sc.Post(func() {})
|
|
||||||
sc.Post(func() {})
|
|
||||||
sc.Post(func() {})
|
|
||||||
|
|
||||||
assert.Len(t, sc.cmdQueue, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharedContext_StartMultipleTimes(t *testing.T) {
|
|
||||||
sc := &SharedContext{
|
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
started: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 10
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
sc.Start()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
assert.True(t, sc.started)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharedContext_DrainCmdQueue(t *testing.T) {
|
|
||||||
sc := &SharedContext{
|
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
counter := 0
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
sc.cmdQueue <- func() {
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.drainCmdQueue()
|
|
||||||
|
|
||||||
assert.Equal(t, 10, counter)
|
|
||||||
assert.Len(t, sc.cmdQueue, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharedContext_DrainCmdQueueEmpty(t *testing.T) {
|
|
||||||
sc := &SharedContext{
|
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.drainCmdQueue()
|
|
||||||
|
|
||||||
assert.Len(t, sc.cmdQueue, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSharedContext_ConcurrentDrainAndPost(t *testing.T) {
|
|
||||||
sc := &SharedContext{
|
|
||||||
cmdQueue: make(chan func(), 256),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
sc.Post(func() {})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for i := 0; i < 50; i++ {
|
|
||||||
sc.drainCmdQueue()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
@@ -125,47 +125,21 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
responded := false
|
statusChan := make(chan error, 1)
|
||||||
|
|
||||||
config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
|
config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
|
||||||
if responded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responded = true
|
|
||||||
log.Info("WlrOutput: configuration succeeded")
|
log.Info("WlrOutput: configuration succeeded")
|
||||||
config.Destroy()
|
statusChan <- nil
|
||||||
resultChan <- nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
|
config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
|
||||||
if responded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responded = true
|
|
||||||
log.Warn("WlrOutput: configuration failed")
|
log.Warn("WlrOutput: configuration failed")
|
||||||
config.Destroy()
|
statusChan <- fmt.Errorf("compositor rejected configuration")
|
||||||
resultChan <- fmt.Errorf("compositor rejected configuration")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) {
|
config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) {
|
||||||
if responded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responded = true
|
|
||||||
log.Warn("WlrOutput: configuration cancelled")
|
log.Warn("WlrOutput: configuration cancelled")
|
||||||
config.Destroy()
|
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)")
|
||||||
resultChan <- fmt.Errorf("configuration cancelled (outdated serial)")
|
|
||||||
})
|
|
||||||
|
|
||||||
time.AfterFunc(time.Second, func() {
|
|
||||||
m.post(func() {
|
|
||||||
if responded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responded = true
|
|
||||||
config.Destroy()
|
|
||||||
resultChan <- fmt.Errorf("timeout waiting for configuration response")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
headsByName := make(map[string]*headState)
|
headsByName := make(map[string]*headState)
|
||||||
@@ -267,7 +241,6 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if applyErr != nil {
|
if applyErr != nil {
|
||||||
responded = true
|
|
||||||
config.Destroy()
|
config.Destroy()
|
||||||
action := "apply"
|
action := "apply"
|
||||||
if test {
|
if test {
|
||||||
@@ -276,6 +249,17 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
|
|||||||
resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr)
|
resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case err := <-statusChan:
|
||||||
|
config.Destroy()
|
||||||
|
resultChan <- err
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
config.Destroy()
|
||||||
|
resultChan <- fmt.Errorf("timeout waiting for configuration response")
|
||||||
|
}
|
||||||
|
}()
|
||||||
})
|
})
|
||||||
|
|
||||||
return <-resultChan
|
return <-resultChan
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
func NewManager(display *wlclient.Display) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
package wlroutput
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStateChanged_BothNil(t *testing.T) {
|
|
||||||
assert.True(t, stateChanged(nil, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OneNil(t *testing.T) {
|
|
||||||
s := &State{Serial: 1}
|
|
||||||
assert.True(t, stateChanged(s, nil))
|
|
||||||
assert.True(t, stateChanged(nil, s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_SerialDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{}}
|
|
||||||
b := &State{Serial: 2, Outputs: []Output{}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputNameDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "HDMI-A-1", Enabled: true}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputEnabledDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: false}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputPositionDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 0, Y: 0}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 1920, Y: 0}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputTransformDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 0}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 1}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputScaleDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 1.0}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 2.0}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputAdaptiveSyncDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 0}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 1}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_CurrentModeNilVsNonNil(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: &OutputMode{Width: 1920}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_CurrentModeDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{
|
|
||||||
Name: "eDP-1",
|
|
||||||
CurrentMode: &OutputMode{Width: 1920, Height: 1080, Refresh: 60000},
|
|
||||||
}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{
|
|
||||||
Name: "eDP-1",
|
|
||||||
CurrentMode: &OutputMode{Width: 2560, Height: 1440, Refresh: 60000},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Outputs[0].CurrentMode.Width = 1920
|
|
||||||
b.Outputs[0].CurrentMode.Height = 1080
|
|
||||||
b.Outputs[0].CurrentMode.Refresh = 144000
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_ModesLengthDiffers(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}}}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}, {Width: 1280}}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_Equal(t *testing.T) {
|
|
||||||
mode := OutputMode{Width: 1920, Height: 1080, Refresh: 60000, Preferred: true}
|
|
||||||
a := &State{
|
|
||||||
Serial: 5,
|
|
||||||
Outputs: []Output{{
|
|
||||||
Name: "eDP-1",
|
|
||||||
Description: "Built-in display",
|
|
||||||
Make: "BOE",
|
|
||||||
Model: "0x0ABC",
|
|
||||||
SerialNumber: "12345",
|
|
||||||
PhysicalWidth: 309,
|
|
||||||
PhysicalHeight: 174,
|
|
||||||
Enabled: true,
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
Transform: 0,
|
|
||||||
Scale: 1.0,
|
|
||||||
CurrentMode: &mode,
|
|
||||||
Modes: []OutputMode{mode},
|
|
||||||
AdaptiveSync: 0,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
b := &State{
|
|
||||||
Serial: 5,
|
|
||||||
Outputs: []Output{{
|
|
||||||
Name: "eDP-1",
|
|
||||||
Description: "Built-in display",
|
|
||||||
Make: "BOE",
|
|
||||||
Model: "0x0ABC",
|
|
||||||
SerialNumber: "12345",
|
|
||||||
PhysicalWidth: 309,
|
|
||||||
PhysicalHeight: 174,
|
|
||||||
Enabled: true,
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
Transform: 0,
|
|
||||||
Scale: 1.0,
|
|
||||||
CurrentMode: &mode,
|
|
||||||
Modes: []OutputMode{mode},
|
|
||||||
AdaptiveSync: 0,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
assert.False(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentGetState(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
state: &State{
|
|
||||||
Serial: 1,
|
|
||||||
Outputs: []Output{{Name: "eDP-1", Enabled: true}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s := m.GetState()
|
|
||||||
_ = s.Serial
|
|
||||||
_ = s.Outputs
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &State{
|
|
||||||
Serial: uint32(j),
|
|
||||||
Outputs: []Output{{Name: "eDP-1", Scale: float64(j % 3)}},
|
|
||||||
}
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
subID := string(rune('a' + id))
|
|
||||||
ch := m.Subscribe(subID)
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
m.Unsubscribe(subID)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapHeadsConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &headState{
|
|
||||||
id: key,
|
|
||||||
name: "test-head",
|
|
||||||
enabled: j%2 == 0,
|
|
||||||
scale: float64(j % 3),
|
|
||||||
modeIDs: []uint32{uint32(j)},
|
|
||||||
}
|
|
||||||
m.heads.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.heads.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.heads.Range(func(k uint32, v *headState) bool {
|
|
||||||
_ = v.name
|
|
||||||
_ = v.enabled
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.heads.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapModesConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &modeState{
|
|
||||||
id: key,
|
|
||||||
width: int32(1920 + j),
|
|
||||||
height: int32(1080 + j),
|
|
||||||
refresh: 60000,
|
|
||||||
preferred: j == 0,
|
|
||||||
}
|
|
||||||
m.modes.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.modes.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.modes.Range(func(k uint32, v *modeState) bool {
|
|
||||||
_ = v.width
|
|
||||||
_ = v.height
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.modes.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, m.dirty, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PostQueueFull(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
cmdq: make(chan cmd, 2),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
|
|
||||||
assert.Len(t, m.cmdq, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_GetStateNilState(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
s := m.GetState()
|
|
||||||
assert.NotNil(t, s.Outputs)
|
|
||||||
assert.Equal(t, uint32(0), s.Serial)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_FatalErrorChannel(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
fatalError: make(chan error, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := m.FatalError()
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
|
|
||||||
m.fatalError <- assert.AnError
|
|
||||||
err := <-ch
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputMode_Fields(t *testing.T) {
|
|
||||||
mode := OutputMode{
|
|
||||||
Width: 1920,
|
|
||||||
Height: 1080,
|
|
||||||
Refresh: 60000,
|
|
||||||
Preferred: true,
|
|
||||||
ID: 42,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, int32(1920), mode.Width)
|
|
||||||
assert.Equal(t, int32(1080), mode.Height)
|
|
||||||
assert.Equal(t, int32(60000), mode.Refresh)
|
|
||||||
assert.True(t, mode.Preferred)
|
|
||||||
assert.Equal(t, uint32(42), mode.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutput_Fields(t *testing.T) {
|
|
||||||
out := Output{
|
|
||||||
Name: "eDP-1",
|
|
||||||
Description: "Built-in display",
|
|
||||||
Make: "BOE",
|
|
||||||
Model: "0x0ABC",
|
|
||||||
SerialNumber: "12345",
|
|
||||||
PhysicalWidth: 309,
|
|
||||||
PhysicalHeight: 174,
|
|
||||||
Enabled: true,
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
Transform: 0,
|
|
||||||
Scale: 1.5,
|
|
||||||
AdaptiveSync: 1,
|
|
||||||
ID: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "eDP-1", out.Name)
|
|
||||||
assert.Equal(t, "Built-in display", out.Description)
|
|
||||||
assert.True(t, out.Enabled)
|
|
||||||
assert.Equal(t, float64(1.5), out.Scale)
|
|
||||||
assert.Equal(t, uint32(1), out.AdaptiveSync)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeadState_ModeIDsSlice(t *testing.T) {
|
|
||||||
head := &headState{
|
|
||||||
id: 1,
|
|
||||||
modeIDs: make([]uint32, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
head.modeIDs = append(head.modeIDs, 1, 2, 3)
|
|
||||||
assert.Len(t, head.modeIDs, 3)
|
|
||||||
assert.Equal(t, uint32(1), head.modeIDs[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_BothCurrentModeNil(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
|
|
||||||
assert.False(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_IndexOutOfBounds(t *testing.T) {
|
|
||||||
a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}}
|
|
||||||
b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}, {Name: "HDMI-A-1"}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "failed to get registry")
|
|
||||||
}
|
|
||||||
@@ -45,7 +45,7 @@ type cmd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
display wlclient.WaylandDisplay
|
display *wlclient.Display
|
||||||
ctx *wlclient.Context
|
ctx *wlclient.Context
|
||||||
registry *wlclient.Registry
|
registry *wlclient.Registry
|
||||||
manager *wlr_output_management.ZwlrOutputManagerV1
|
manager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
|
|||||||
@@ -20,3 +20,33 @@ func ExpandPath(path string) (string, error) {
|
|||||||
|
|
||||||
return expanded, nil
|
return expanded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func XDGConfigHome() string {
|
||||||
|
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
|
||||||
|
return configHome
|
||||||
|
}
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
return filepath.Join(home, ".config")
|
||||||
|
}
|
||||||
|
return filepath.Join(os.TempDir(), ".config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func XDGCacheHome() string {
|
||||||
|
if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" {
|
||||||
|
return cacheHome
|
||||||
|
}
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
return filepath.Join(home, ".cache")
|
||||||
|
}
|
||||||
|
return filepath.Join(os.TempDir(), ".cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func XDGDataHome() string {
|
||||||
|
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
|
||||||
|
return dataHome
|
||||||
|
}
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
return filepath.Join(home, ".local", "share")
|
||||||
|
}
|
||||||
|
return filepath.Join(os.TempDir(), ".local", "share")
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,3 +41,66 @@ func TestExpandPathAbsolute(t *testing.T) {
|
|||||||
t.Errorf("expected /absolute/path, got %s", result)
|
t.Errorf("expected /absolute/path, got %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXDGConfigHomeDefault(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "")
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("no home directory")
|
||||||
|
}
|
||||||
|
result := XDGConfigHome()
|
||||||
|
expected := filepath.Join(home, ".config")
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected %s, got %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDGConfigHomeCustom(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
|
||||||
|
result := XDGConfigHome()
|
||||||
|
if result != "/custom/config" {
|
||||||
|
t.Errorf("expected /custom/config, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDGCacheHomeDefault(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CACHE_HOME", "")
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("no home directory")
|
||||||
|
}
|
||||||
|
result := XDGCacheHome()
|
||||||
|
expected := filepath.Join(home, ".cache")
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected %s, got %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDGCacheHomeCustom(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CACHE_HOME", "/custom/cache")
|
||||||
|
result := XDGCacheHome()
|
||||||
|
if result != "/custom/cache" {
|
||||||
|
t.Errorf("expected /custom/cache, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDGDataHomeDefault(t *testing.T) {
|
||||||
|
t.Setenv("XDG_DATA_HOME", "")
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("no home directory")
|
||||||
|
}
|
||||||
|
result := XDGDataHome()
|
||||||
|
expected := filepath.Join(home, ".local", "share")
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected %s, got %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDGDataHomeCustom(t *testing.T) {
|
||||||
|
t.Setenv("XDG_DATA_HOME", "/custom/data")
|
||||||
|
result := XDGDataHome()
|
||||||
|
if result != "/custom/data" {
|
||||||
|
t.Errorf("expected /custom/data, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Runs go generate for each directory, but in parallel. Any arguments are appended to the
|
# Runs go generate for each directory, but in parallel. Any arguments are appended to the
|
||||||
# go generate command.
|
# go generate command.
|
||||||
# Usage: $ ./generatep [go generate arguments]
|
# Usage: $ ./generatep [go generate arguments]
|
||||||
# Print all generate commands: $ ./generatep -x
|
# Print all generate commands: $ ./generatep -x
|
||||||
|
|
||||||
cd ./wayland || exit 1
|
cd ./wayland
|
||||||
find . -type f -name '*.go' -exec dirname {} \; | sort -u | parallel -j 0 go generate "$1" {}/.
|
find . -type f -name '*.go' -exec dirname {} \; | sort -u | parallel -j 0 go generate $1 {}/.
|
||||||
|
|||||||
@@ -15,15 +15,6 @@ type Proxy interface {
|
|||||||
MarkZombie()
|
MarkZombie()
|
||||||
}
|
}
|
||||||
|
|
||||||
type WaylandDisplay interface {
|
|
||||||
Context() *Context
|
|
||||||
GetRegistry() (*Registry, error)
|
|
||||||
Roundtrip() error
|
|
||||||
Destroy() error
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ WaylandDisplay = (*Display)(nil)
|
|
||||||
|
|
||||||
type BaseProxy struct {
|
type BaseProxy struct {
|
||||||
ctx *Context
|
ctx *Context
|
||||||
id uint32
|
id uint32
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
)
|
)
|
||||||
@@ -29,12 +28,6 @@ func (ctx *Context) Register(p Proxy) {
|
|||||||
ctx.objects.Store(id, p)
|
ctx.objects.Store(id, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) RegisterWithID(p Proxy, id uint32) {
|
|
||||||
p.SetID(id)
|
|
||||||
p.SetContext(ctx)
|
|
||||||
ctx.objects.Store(id, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) Unregister(p Proxy) {
|
func (ctx *Context) Unregister(p Proxy) {
|
||||||
ctx.objects.Delete(p.ID())
|
ctx.objects.Delete(p.ID())
|
||||||
}
|
}
|
||||||
@@ -54,10 +47,6 @@ func (ctx *Context) Close() error {
|
|||||||
return ctx.conn.Close()
|
return ctx.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) SetReadDeadline(t time.Time) error {
|
|
||||||
return ctx.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
|
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
|
||||||
// respective wayland protocol.
|
// respective wayland protocol.
|
||||||
// Dispatch must be called on the same goroutine as other interactions with the Context.
|
// Dispatch must be called on the same goroutine as other interactions with the Context.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
dms-git (1.0.0+git2419.993f14a3) nightly; urgency=medium
|
dms-git (0.6.2+git2419.993f14a3) nightly; urgency=medium
|
||||||
|
|
||||||
* Major stable release v1.0.0
|
|
||||||
* widgets: make dank icon picker a popup
|
* widgets: make dank icon picker a popup
|
||||||
* Previous updates included in build
|
* Previous updates included in build
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
<service name="download_url">
|
<service name="download_url">
|
||||||
<param name="protocol">https</param>
|
<param name="protocol">https</param>
|
||||||
<param name="host">github.com</param>
|
<param name="host">github.com</param>
|
||||||
<param name="path">/AvengeMedia/DankMaterialShell/archive/refs/tags/v1.0.2.tar.gz</param>
|
<param name="path">/AvengeMedia/DankMaterialShell/archive/refs/tags/v0.6.2.tar.gz</param>
|
||||||
<param name="filename">dms-source.tar.gz</param>
|
<param name="filename">dms-source.tar.gz</param>
|
||||||
</service>
|
</service>
|
||||||
<!-- Download amd64 binary -->
|
<!-- Download amd64 binary -->
|
||||||
<service name="download_url">
|
<service name="download_url">
|
||||||
<param name="protocol">https</param>
|
<param name="protocol">https</param>
|
||||||
<param name="host">github.com</param>
|
<param name="host">github.com</param>
|
||||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.0.2/dms-distropkg-amd64.gz</param>
|
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v0.6.2/dms-distropkg-amd64.gz</param>
|
||||||
</service>
|
</service>
|
||||||
<!-- Download arm64 binary -->
|
<!-- Download arm64 binary -->
|
||||||
<service name="download_url">
|
<service name="download_url">
|
||||||
<param name="protocol">https</param>
|
<param name="protocol">https</param>
|
||||||
<param name="host">github.com</param>
|
<param name="host">github.com</param>
|
||||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.0.2/dms-distropkg-arm64.gz</param>
|
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v0.6.2/dms-distropkg-arm64.gz</param>
|
||||||
</service>
|
</service>
|
||||||
</services>
|
</services>
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
dms (1.0.2) stable; urgency=medium
|
dms (0.6.2) stable; urgency=medium
|
||||||
|
|
||||||
* Update to v1.0.2 stable release
|
* Update to v0.6.2 release
|
||||||
* Bug fixes and improvements
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> Thu, 12 Dec 2025 14:30:00 -0500
|
|
||||||
|
|
||||||
dms (1.0.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Update to v1.0.0 release
|
|
||||||
* Major stable release
|
|
||||||
* Fix binary download paths for OBS builds
|
* Fix binary download paths for OBS builds
|
||||||
* Native format: removed revisions
|
* Native format: removed revisions
|
||||||
|
|
||||||
|
|||||||
@@ -36,19 +36,15 @@ override_dh_auto_build:
|
|||||||
fi
|
fi
|
||||||
chmod +x dms
|
chmod +x dms
|
||||||
|
|
||||||
# Extract source if needed
|
|
||||||
if [ ! -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
if [ ! -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
||||||
if [ -f ../SOURCES/dms-source.tar.gz ]; then \
|
if [ -f ../SOURCES/dms-source.tar.gz ]; then \
|
||||||
tar -xzf ../SOURCES/dms-source.tar.gz; \
|
tar -xzf ../SOURCES/dms-source.tar.gz; \
|
||||||
elif [ -f dms-source.tar.gz ]; then \
|
elif [ -f dms-source.tar.gz ]; then \
|
||||||
tar -xzf dms-source.tar.gz; \
|
tar -xzf dms-source.tar.gz; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
if [ ! -d DankMaterialShell-$(UPSTREAM_VERSION) ] && [ -d DankMaterialShell-0.6.2 ]; then \
|
||||||
# Rename directory to match expected version
|
mv DankMaterialShell-0.6.2 DankMaterialShell-$(UPSTREAM_VERSION); \
|
||||||
SOURCE_DIR=$$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" ! -name "DankMaterialShell-$(UPSTREAM_VERSION)" | head -n1); \
|
fi; \
|
||||||
if [ -n "$$SOURCE_DIR" ]; then \
|
|
||||||
echo "Renaming $$SOURCE_DIR to DankMaterialShell-$(UPSTREAM_VERSION)"; \
|
|
||||||
mv "$$SOURCE_DIR" DankMaterialShell-$(UPSTREAM_VERSION); \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -56,11 +52,9 @@ override_dh_auto_install:
|
|||||||
install -Dm755 dms debian/dms/usr/bin/dms
|
install -Dm755 dms debian/dms/usr/bin/dms
|
||||||
|
|
||||||
mkdir -p debian/dms/usr/share/quickshell/dms debian/dms/usr/lib/systemd/user
|
mkdir -p debian/dms/usr/share/quickshell/dms debian/dms/usr/lib/systemd/user
|
||||||
# Ensure directory has correct version name for install step
|
# Handle directory name mismatch again for install step if needed
|
||||||
SOURCE_DIR=$$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" ! -name "DankMaterialShell-$(UPSTREAM_VERSION)" | head -n1); \
|
if [ ! -d DankMaterialShell-$(UPSTREAM_VERSION) ] && [ -d DankMaterialShell-0.6.2 ]; then \
|
||||||
if [ -n "$$SOURCE_DIR" ]; then \
|
mv DankMaterialShell-0.6.2 DankMaterialShell-$(UPSTREAM_VERSION); \
|
||||||
echo "Renaming $$SOURCE_DIR to DankMaterialShell-$(UPSTREAM_VERSION) for install"; \
|
|
||||||
mv "$$SOURCE_DIR" DankMaterialShell-$(UPSTREAM_VERSION); \
|
|
||||||
fi
|
fi
|
||||||
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
||||||
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ VCS: {{{ git_repo_vcs }}}
|
|||||||
Source0: {{{ git_repo_pack }}}
|
Source0: {{{ git_repo_pack }}}
|
||||||
|
|
||||||
BuildRequires: git-core
|
BuildRequires: git-core
|
||||||
|
BuildRequires: rpkg
|
||||||
# For the _tmpfilesdir macro.
|
# For the _tmpfilesdir macro.
|
||||||
BuildRequires: systemd-rpm-macros
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ VCS: {{{ git_repo_vcs }}}
|
|||||||
Source0: {{{ git_repo_pack }}}
|
Source0: {{{ git_repo_pack }}}
|
||||||
|
|
||||||
BuildRequires: git-core
|
BuildRequires: git-core
|
||||||
|
BuildRequires: rpkg
|
||||||
BuildRequires: gzip
|
BuildRequires: gzip
|
||||||
BuildRequires: golang >= 1.24
|
BuildRequires: golang >= 1.24
|
||||||
BuildRequires: make
|
BuildRequires: make
|
||||||
|
|||||||
@@ -9,19 +9,32 @@ let
|
|||||||
cfg = config.programs.dankMaterialShell;
|
cfg = config.programs.dankMaterialShell;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
qmlPath = "${dmsPkgs.dms-shell}/share/quickshell/dms";
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
|
pkgs.material-symbols
|
||||||
|
pkgs.inter
|
||||||
|
pkgs.fira-code
|
||||||
|
|
||||||
|
pkgs.ddcutil
|
||||||
|
pkgs.libsForQt5.qt5ct
|
||||||
|
pkgs.kdePackages.qt6ct
|
||||||
|
|
||||||
dmsPkgs.dms-shell
|
dmsPkgs.dms-shell
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
|
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
|
||||||
|
++ lib.optionals cfg.enableClipboard [
|
||||||
|
pkgs.cliphist
|
||||||
|
pkgs.wl-clipboard
|
||||||
|
]
|
||||||
++ lib.optionals cfg.enableVPN [
|
++ lib.optionals cfg.enableVPN [
|
||||||
pkgs.glib
|
pkgs.glib
|
||||||
pkgs.networkmanager
|
pkgs.networkmanager
|
||||||
]
|
]
|
||||||
|
++ lib.optional cfg.enableBrightnessControl pkgs.brightnessctl
|
||||||
|
++ lib.optional cfg.enableColorPicker pkgs.hyprpicker
|
||||||
++ 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.enableSystemSound pkgs.kdePackages.qtmultimedia;
|
||||||
plugins = lib.mapAttrs (name: plugin: {
|
|
||||||
source = plugin.src;
|
|
||||||
}) (lib.filterAttrs (n: v: v.enable) cfg.plugins);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,32 +139,10 @@ in
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
if [ -f session.json ]; then
|
if [ -f session.json ]; then
|
||||||
copy_wallpaper() {
|
if cp "$(${jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then
|
||||||
local path=$(${jq} -r ".$1 // empty" session.json)
|
mv session.json session.orig.json
|
||||||
if [ -f "$path" ]; then
|
${jq} '.wallpaperPath = "${cacheDir}/wallpaper.jpg"' session.orig.json > session.json
|
||||||
cp "$path" "$2"
|
fi
|
||||||
${jq} ".$1 = \"${cacheDir}/$2\"" session.json > session.tmp
|
|
||||||
mv session.tmp session.json
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_monitor_wallpapers() {
|
|
||||||
${jq} -r ".$1 // {} | to_entries[] | .key + \":\" + .value" session.json 2>/dev/null | while IFS=: read monitor path; do
|
|
||||||
local dest="$2-$(echo "$monitor" | tr -c '[:alnum:]' '-')"
|
|
||||||
if [ -f "$path" ]; then
|
|
||||||
cp "$path" "$dest"
|
|
||||||
${jq} --arg m "$monitor" --arg p "${cacheDir}/$dest" ".$1[\$m] = \$p" session.json > session.tmp
|
|
||||||
mv session.tmp session.json
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_wallpaper "wallpaperPath" "wallpaper"
|
|
||||||
copy_wallpaper "wallpaperPathLight" "wallpaper-light"
|
|
||||||
copy_wallpaper "wallpaperPathDark" "wallpaper-dark"
|
|
||||||
copy_monitor_wallpapers "monitorWallpapers" "wallpaper-monitor"
|
|
||||||
copy_monitor_wallpapers "monitorWallpapersLight" "wallpaper-monitor-light"
|
|
||||||
copy_monitor_wallpapers "monitorWallpapersDark" "wallpaper-monitor-dark"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
if [ -f settings.json ]; then
|
||||||
|
|||||||
@@ -44,12 +44,37 @@ in
|
|||||||
description = "The default session are only read if the session.json file don't exist";
|
description = "The default session are only read if the session.json file don't exist";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
plugins = lib.mkOption {
|
||||||
|
type = attrsOf (
|
||||||
|
types.submodule (
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to link this plugin";
|
||||||
|
};
|
||||||
|
src = lib.mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Source to link to DMS plugins directory";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
description = "DMS Plugins to install";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
programs.quickshell = {
|
programs.quickshell = {
|
||||||
enable = true;
|
enable = true;
|
||||||
inherit (cfg.quickshell) package;
|
inherit (cfg.quickshell) package;
|
||||||
|
|
||||||
|
configs.dms = common.qmlPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
@@ -57,6 +82,7 @@ in
|
|||||||
Description = "DankMaterialShell";
|
Description = "DankMaterialShell";
|
||||||
PartOf = [ config.wayland.systemd.target ];
|
PartOf = [ config.wayland.systemd.target ];
|
||||||
After = [ config.wayland.systemd.target ];
|
After = [ config.wayland.systemd.target ];
|
||||||
|
X-Restart-Triggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
@@ -72,10 +98,10 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
xdg.configFile = lib.mkMerge [
|
xdg.configFile = lib.mkMerge [
|
||||||
(lib.mapAttrs' (name: value: {
|
(lib.mapAttrs' (name: plugin: {
|
||||||
name = "DankMaterialShell/plugins/${name}";
|
name = "DankMaterialShell/plugins/${name}";
|
||||||
inherit value;
|
value.source = plugin.src;
|
||||||
}) common.plugins)
|
}) (lib.filterAttrs (n: v: v.enable) cfg.plugins))
|
||||||
{
|
{
|
||||||
"DankMaterialShell/default-settings.json" = lib.mkIf (cfg.default.settings != { }) {
|
"DankMaterialShell/default-settings.json" = lib.mkIf (cfg.default.settings != { }) {
|
||||||
source = jsonFormat.generate "default-settings.json" cfg.default.settings;
|
source = jsonFormat.generate "default-settings.json" cfg.default.settings;
|
||||||
|
|||||||
@@ -63,6 +63,25 @@ in
|
|||||||
allow-when-locked = true;
|
allow-when-locked = true;
|
||||||
action = dms-ipc "audio" "micmute";
|
action = dms-ipc "audio" "micmute";
|
||||||
};
|
};
|
||||||
|
"Mod+Alt+N" = {
|
||||||
|
allow-when-locked = true;
|
||||||
|
action = dms-ipc "night" "toggle";
|
||||||
|
hotkey-overlay.title = "Toggle Night Mode";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.attrsets.optionalAttrs cfg.enableSystemMonitoring {
|
||||||
|
"Mod+M" = {
|
||||||
|
action = dms-ipc "processlist" "toggle";
|
||||||
|
hotkey-overlay.title = "Toggle Process List";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.attrsets.optionalAttrs cfg.enableClipboard {
|
||||||
|
"Mod+V" = {
|
||||||
|
action = dms-ipc "clipboard" "toggle";
|
||||||
|
hotkey-overlay.title = "Toggle Clipboard Manager";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.attrsets.optionalAttrs cfg.enableBrightnessControl {
|
||||||
"XF86MonBrightnessUp" = {
|
"XF86MonBrightnessUp" = {
|
||||||
allow-when-locked = true;
|
allow-when-locked = true;
|
||||||
action = dms-ipc "brightness" "increment" "5" "";
|
action = dms-ipc "brightness" "increment" "5" "";
|
||||||
@@ -71,21 +90,6 @@ in
|
|||||||
allow-when-locked = true;
|
allow-when-locked = true;
|
||||||
action = dms-ipc "brightness" "decrement" "5" "";
|
action = dms-ipc "brightness" "decrement" "5" "";
|
||||||
};
|
};
|
||||||
"Mod+Alt+N" = {
|
|
||||||
allow-when-locked = true;
|
|
||||||
action = dms-ipc "night" "toggle";
|
|
||||||
hotkey-overlay.title = "Toggle Night Mode";
|
|
||||||
};
|
|
||||||
"Mod+V" = {
|
|
||||||
action = dms-ipc "clipboard" "toggle";
|
|
||||||
hotkey-overlay.title = "Toggle Clipboard Manager";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// lib.attrsets.optionalAttrs cfg.enableSystemMonitoring {
|
|
||||||
"Mod+M" = {
|
|
||||||
action = dms-ipc "processlist" "toggle";
|
|
||||||
hotkey-overlay.title = "Toggle Process List";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,6 +101,16 @@ in
|
|||||||
"run"
|
"run"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
++ lib.optionals cfg.enableClipboard [
|
||||||
|
{
|
||||||
|
command = [
|
||||||
|
"wl-paste"
|
||||||
|
"--watch"
|
||||||
|
"cliphist"
|
||||||
|
"store"
|
||||||
|
];
|
||||||
|
}
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
environment.etc."xdg/quickshell/dms".source = "${dmsPkgs.dms-shell}/share/quickshell/dms";
|
||||||
|
|
||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
description = "DankMaterialShell";
|
description = "DankMaterialShell";
|
||||||
path = lib.mkForce [ ];
|
path = lib.mkForce [ ];
|
||||||
@@ -29,7 +31,7 @@ in
|
|||||||
partOf = [ "graphical-session.target" ];
|
partOf = [ "graphical-session.target" ];
|
||||||
after = [ "graphical-session.target" ];
|
after = [ "graphical-session.target" ];
|
||||||
wantedBy = [ "graphical-session.target" ];
|
wantedBy = [ "graphical-session.target" ];
|
||||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
@@ -38,13 +40,5 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
environment.systemPackages = [ cfg.quickshell.package ] ++ common.packages;
|
environment.systemPackages = [ cfg.quickshell.package ] ++ common.packages;
|
||||||
|
|
||||||
environment.etc = lib.mapAttrs' (name: value: {
|
|
||||||
name = "xdg/quickshell/dms-plugins/${name}";
|
|
||||||
inherit value;
|
|
||||||
}) common.plugins;
|
|
||||||
|
|
||||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
|
||||||
services.accounts-daemon.enable = lib.mkDefault true;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,11 @@
|
|||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
path = [
|
|
||||||
"programs"
|
|
||||||
"dankMaterialShell"
|
|
||||||
];
|
|
||||||
|
|
||||||
builtInRemovedMsg = "This is now built-in in DMS and doesn't need additional dependencies.";
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
(lib.mkRemovedOptionModule (path ++ [ "enableBrightnessControl" ]) builtInRemovedMsg)
|
|
||||||
(lib.mkRemovedOptionModule (path ++ [ "enableColorPicker" ]) builtInRemovedMsg)
|
|
||||||
(lib.mkRemovedOptionModule (path ++ [ "enableClipboard" ]) builtInRemovedMsg)
|
|
||||||
(lib.mkRemovedOptionModule (
|
|
||||||
path ++ [ "enableSystemSound" ]
|
|
||||||
) "qtmultimedia is now included on dms-shell package.")
|
|
||||||
];
|
|
||||||
|
|
||||||
options.programs.dankMaterialShell = {
|
options.programs.dankMaterialShell = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell";
|
enable = lib.mkEnableOption "DankMaterialShell";
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||||
restartIfChanged = lib.mkOption {
|
restartIfChanged = lib.mkOption {
|
||||||
@@ -37,11 +23,26 @@ in
|
|||||||
default = true;
|
default = true;
|
||||||
description = "Add needed dependencies to use system monitoring widgets";
|
description = "Add needed dependencies to use system monitoring widgets";
|
||||||
};
|
};
|
||||||
|
enableClipboard = lib.mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Add needed dependencies to use the clipboard widget";
|
||||||
|
};
|
||||||
enableVPN = lib.mkOption {
|
enableVPN = lib.mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = "Add needed dependencies to use the VPN widget";
|
description = "Add needed dependencies to use the VPN widget";
|
||||||
};
|
};
|
||||||
|
enableBrightnessControl = lib.mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Add needed dependencies to have brightness/backlight support";
|
||||||
|
};
|
||||||
|
enableColorPicker = lib.mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Add needed dependencies to have color picking support";
|
||||||
|
};
|
||||||
enableDynamicTheming = lib.mkOption {
|
enableDynamicTheming = lib.mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
@@ -57,46 +58,15 @@ in
|
|||||||
default = true;
|
default = true;
|
||||||
description = "Add calendar events support via khal";
|
description = "Add calendar events support via khal";
|
||||||
};
|
};
|
||||||
|
enableSystemSound = lib.mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Add needed dependencies to have system sound support";
|
||||||
|
};
|
||||||
quickshell = {
|
quickshell = {
|
||||||
package = lib.mkPackageOption dmsPkgs "quickshell" {
|
package = lib.mkPackageOption dmsPkgs "quickshell" {
|
||||||
extraDescription = "The quickshell package to use (defaults to be built from source, in the commit 26531f due to unreleased features used by DMS).";
|
extraDescription = "The quickshell package to use (defaults to be built from source, in the commit 26531f due to unreleased features used by DMS).";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
plugins = lib.mkOption {
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submodule {
|
|
||||||
options = {
|
|
||||||
enable = lib.mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Whether to enable this plugin";
|
|
||||||
};
|
|
||||||
src = lib.mkOption {
|
|
||||||
type = types.either types.package types.path;
|
|
||||||
description = "Source of the plugin package or path";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
|
||||||
description = "DMS Plugins to install and enable";
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
{
|
|
||||||
DockerManager = {
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "LuckShiba";
|
|
||||||
repo = "DmsDockerManager";
|
|
||||||
rev = "v1.2.0";
|
|
||||||
sha256 = "sha256-VoJCaygWnKpv0s0pqTOmzZnPM922qPDMHk4EPcgVnaU=";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
AnotherPlugin = {
|
|
||||||
enable = true;
|
|
||||||
src = pkgs.another-plugin;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Version: 1.0.2
|
Version: 0.6.2
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
|
|
||||||
@@ -105,10 +105,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Fri Dec 12 2025 AvengeMedia <maintainer@avengemedia.com> - 1.0.2-1
|
|
||||||
- Update to stable v1.0.2 release
|
|
||||||
- Bug fixes and improvements
|
|
||||||
|
|
||||||
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2-1
|
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2-1
|
||||||
- Stable release build with pre-built binaries
|
- Stable release build with pre-built binaries
|
||||||
- Multi-arch support (x86_64, aarch64)
|
- Multi-arch support (x86_64, aarch64)
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Build SRPM locally with correct tarball and upload to Copr
|
|
||||||
# Usage: ./create-upload-copr.sh VERSION [RELEASE]
|
|
||||||
# Example: ./create-upload-copr.sh 1.0.0 4
|
|
||||||
|
|
||||||
VERSION="${1:-1.0.0}"
|
|
||||||
RELEASE="${2:-1}"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
||||||
|
|
||||||
echo "Building DMS v${VERSION}-${RELEASE} SRPM for Copr..."
|
|
||||||
|
|
||||||
# Setup build directories
|
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
|
||||||
cd ~/rpmbuild/SOURCES
|
|
||||||
|
|
||||||
# Create the corrected QML tarball locally
|
|
||||||
echo "Creating QML tarball with assets..."
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
# Copy quickshell contents to temp
|
|
||||||
cp -r quickshell/* "$TEMP_DIR/"
|
|
||||||
|
|
||||||
# Copy root LICENSE and CONTRIBUTING.md
|
|
||||||
cp LICENSE CONTRIBUTING.md "$TEMP_DIR/"
|
|
||||||
|
|
||||||
# Copy root assets directory (this is what was missing!)
|
|
||||||
cp -r assets "$TEMP_DIR/"
|
|
||||||
|
|
||||||
# Create tarball
|
|
||||||
cd "$TEMP_DIR"
|
|
||||||
tar --exclude='.git' \
|
|
||||||
--exclude='.github' \
|
|
||||||
--exclude='*.tar.gz' \
|
|
||||||
-czf ~/rpmbuild/SOURCES/dms-qml.tar.gz .
|
|
||||||
|
|
||||||
cd ~/rpmbuild/SOURCES
|
|
||||||
echo "Created dms-qml.tar.gz with md5sum: $(md5sum dms-qml.tar.gz | awk '{print $1}')"
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|
||||||
# Generate spec file
|
|
||||||
echo "Generating spec file..."
|
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
|
||||||
|
|
||||||
cat >~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
|
||||||
# Spec for DMS stable releases - Built locally
|
|
||||||
|
|
||||||
%global debug_package %{nil}
|
|
||||||
%global version VERSION_PLACEHOLDER
|
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
|
||||||
|
|
||||||
Name: dms
|
|
||||||
Version: %{version}
|
|
||||||
Release: RELEASE_PLACEHOLDER%{?dist}
|
|
||||||
Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
BuildRequires: gzip
|
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
|
||||||
Requires: accountsservice
|
|
||||||
Requires: dms-cli = %{version}-%{release}
|
|
||||||
Requires: dgop
|
|
||||||
|
|
||||||
Recommends: cava
|
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
%package -n dms-cli
|
|
||||||
Summary: DankMaterialShell CLI tool
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
|
||||||
case "%{_arch}" in
|
|
||||||
x86_64)
|
|
||||||
ARCH_SUFFIX="amd64"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v%{version}/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
%posttrans
|
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
# Signal running DMS instances to reload
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|
||||||
|
|
||||||
%files
|
|
||||||
%license LICENSE
|
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
%files -n dms-cli
|
|
||||||
%{_bindir}/dms
|
|
||||||
%{_datadir}/bash-completion/completions/dms
|
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
|
||||||
- Stable release VERSION_PLACEHOLDER
|
|
||||||
- Built locally with corrected tarball
|
|
||||||
SPECEOF
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
# Build SRPM
|
|
||||||
echo "Building SRPM..."
|
|
||||||
cd ~/rpmbuild/SPECS
|
|
||||||
rpmbuild -bs dms.spec
|
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/dms-"${VERSION}"-*.src.rpm | tail -n 1)
|
|
||||||
if [ ! -f "$SRPM" ]; then
|
|
||||||
echo "Error: SRPM not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "SRPM built successfully: $SRPM"
|
|
||||||
|
|
||||||
# Check if copr-cli is installed
|
|
||||||
if ! command -v copr-cli &>/dev/null; then
|
|
||||||
echo ""
|
|
||||||
echo "copr-cli is not installed. Install it with:"
|
|
||||||
echo " pip install copr-cli"
|
|
||||||
echo ""
|
|
||||||
echo "Then configure it with your Copr API token in ~/.config/copr"
|
|
||||||
echo ""
|
|
||||||
echo "SRPM is ready at: $SRPM"
|
|
||||||
echo "Upload manually with: copr-cli build avengemedia/dms $SRPM"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload to Copr
|
|
||||||
echo ""
|
|
||||||
echo "Uploading to Copr..."
|
|
||||||
if copr-cli build avengemedia/dms "$SRPM" --nowait; then
|
|
||||||
echo ""
|
|
||||||
echo "Build submitted successfully! Check status at:"
|
|
||||||
echo "https://copr.fedorainfracloud.org/coprs/avengemedia/dms/builds/"
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo "Copr upload failed. You can manually upload the SRPM:"
|
|
||||||
echo " copr-cli build avengemedia/dms $SRPM"
|
|
||||||
echo ""
|
|
||||||
echo "Or upload via web interface:"
|
|
||||||
echo " https://copr.fedorainfracloud.org/coprs/avengemedia/dms/builds/"
|
|
||||||
echo ""
|
|
||||||
echo "SRPM location: $SRPM"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
# Unified OBS status checker for dms packages
|
# Unified OBS status checker for dms packages
|
||||||
# Checks all platforms (Debian, OpenSUSE) and architectures (x86_64, aarch64)
|
# Checks all platforms (Debian, OpenSUSE) and architectures (x86_64, aarch64)
|
||||||
# Only pulls logs if build failed
|
# Only pulls logs if build failed
|
||||||
@@ -35,81 +35,81 @@ cd "$OBS_BASE" || {
|
|||||||
|
|
||||||
for pkg in "${PACKAGES[@]}"; do
|
for pkg in "${PACKAGES[@]}"; do
|
||||||
case "$pkg" in
|
case "$pkg" in
|
||||||
dms)
|
dms)
|
||||||
PROJECT="$OBS_BASE_PROJECT:dms"
|
PROJECT="$OBS_BASE_PROJECT:dms"
|
||||||
;;
|
;;
|
||||||
dms-git)
|
dms-git)
|
||||||
PROJECT="$OBS_BASE_PROJECT:dms-git"
|
PROJECT="$OBS_BASE_PROJECT:dms-git"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Error: Unknown package '$pkg'"
|
echo "Error: Unknown package '$pkg'"
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
(
|
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "=== $pkg ==="
|
echo "=== $pkg ==="
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|
||||||
# Checkout if needed
|
# Checkout if needed
|
||||||
if [[ ! -d "$PROJECT/$pkg" ]]; then
|
if [[ ! -d "$PROJECT/$pkg" ]]; then
|
||||||
osc co "$PROJECT/$pkg" 2>&1 | tail -1
|
osc co "$PROJECT/$pkg" 2>&1 | tail -1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$PROJECT/$pkg"
|
cd "$PROJECT/$pkg"
|
||||||
|
|
||||||
ALL_RESULTS=$(osc results 2>&1)
|
ALL_RESULTS=$(osc results 2>&1)
|
||||||
|
|
||||||
# Check each repository and architecture
|
# Check each repository and architecture
|
||||||
FAILED_BUILDS=()
|
FAILED_BUILDS=()
|
||||||
for repo in "${REPOS[@]}"; do
|
for repo in "${REPOS[@]}"; do
|
||||||
for arch in "${ARCHES[@]}"; do
|
for arch in "${ARCHES[@]}"; do
|
||||||
STATUS=$(echo "$ALL_RESULTS" | grep "$repo.*$arch" | awk '{print $NF}' | head -1)
|
STATUS=$(echo "$ALL_RESULTS" | grep "$repo.*$arch" | awk '{print $NF}' | head -1)
|
||||||
|
|
||||||
if [[ -n "$STATUS" ]]; then
|
if [[ -n "$STATUS" ]]; then
|
||||||
# Color code status
|
# Color code status
|
||||||
case "$STATUS" in
|
case "$STATUS" in
|
||||||
succeeded)
|
succeeded)
|
||||||
COLOR="\033[0;32m" # Green
|
COLOR="\033[0;32m" # Green
|
||||||
SYMBOL="✅"
|
SYMBOL="✅"
|
||||||
;;
|
;;
|
||||||
failed)
|
failed)
|
||||||
COLOR="\033[0;31m" # Red
|
COLOR="\033[0;31m" # Red
|
||||||
SYMBOL="❌"
|
SYMBOL="❌"
|
||||||
FAILED_BUILDS+=("$repo $arch")
|
FAILED_BUILDS+=("$repo $arch")
|
||||||
;;
|
;;
|
||||||
unresolvable)
|
unresolvable)
|
||||||
COLOR="\033[0;33m" # Yellow
|
COLOR="\033[0;33m" # Yellow
|
||||||
SYMBOL="⚠️"
|
SYMBOL="⚠️"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
COLOR="\033[0;37m" # White
|
COLOR="\033[0;37m" # White
|
||||||
SYMBOL="⏳"
|
SYMBOL="⏳"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
echo -e " $SYMBOL $repo $arch: ${COLOR}$STATUS\033[0m"
|
echo -e " $SYMBOL $repo $arch: ${COLOR}$STATUS\033[0m"
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
done
|
||||||
|
|
||||||
# Pull logs for failed builds
|
# Pull logs for failed builds
|
||||||
if [[ ${#FAILED_BUILDS[@]} -gt 0 ]]; then
|
if [[ ${#FAILED_BUILDS[@]} -gt 0 ]]; then
|
||||||
echo ""
|
|
||||||
echo " 📋 Fetching logs for failed builds..."
|
|
||||||
for build in "${FAILED_BUILDS[@]}"; do
|
|
||||||
read -r repo arch <<<"$build"
|
|
||||||
echo ""
|
|
||||||
echo " ────────────────────────────────────────────"
|
|
||||||
echo " Build log: $repo $arch"
|
|
||||||
echo " ────────────────────────────────────────────"
|
|
||||||
osc remotebuildlog "$PROJECT" "$pkg" "$repo" "$arch" 2>&1 | tail -100
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
)
|
echo " 📋 Fetching logs for failed builds..."
|
||||||
|
for build in "${FAILED_BUILDS[@]}"; do
|
||||||
|
read -r repo arch <<< "$build"
|
||||||
|
echo ""
|
||||||
|
echo " ────────────────────────────────────────────"
|
||||||
|
echo " Build log: $repo $arch"
|
||||||
|
echo " ────────────────────────────────────────────"
|
||||||
|
osc remotebuildlog "$PROJECT" "$pkg" "$repo" "$arch" 2>&1 | tail -100
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
cd - > /dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "Status check complete!"
|
echo "Status check complete!"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user