1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-22 19:15:24 -04:00

Compare commits

...

25 Commits

Author SHA1 Message Date
bbedward 672754b0b5 dankdash: fix weather tooltips
fixes #1065
2025-12-16 15:27:44 -05:00
bbedward 0d1553123b binds: accidentally deleted import 2025-12-16 15:16:44 -05:00
bbedward ba6c51c102 core: exit non-zero when SIGUSR1 is received (for systemd r estart) 2025-12-16 14:47:46 -05:00
bbedward d64206a9ff core: detect quickshell crash on SIGTERM 2025-12-16 14:44:22 -05:00
bbedward d9a1089039 displays: add hyprland HDR options 2025-12-16 14:12:51 -05:00
bbedward 55fe463405 displays: break monolith config down and allow floats/fix integer
writing (niri)
2025-12-16 13:36:00 -05:00
bbedward e84210e962 displays: fix niri hot corner config 2025-12-16 12:54:26 -05:00
bbedward ff506548d3 displays: add niri-specific layout options to configurator 2025-12-16 12:23:34 -05:00
arfan f6b09751e9 fix: update getWorkspaceIndex function to include index parameter also fix workspace padding number (#1062) 2025-12-16 11:32:21 -05:00
bbedward 3d863979c4 core: preserve quickshell exit code 2025-12-16 09:01:13 -05:00
purian23 2947ff4131 distro: Revise server side file handling 2025-12-16 01:08:12 -05:00
purian23 b8fca10896 Remove auto run on tags 2025-12-16 00:17:13 -05:00
purian23 33e45794d2 No run on push 2025-12-15 23:29:36 -05:00
purian23 42cc88ca65 Workflow update 2025-12-15 23:24:16 -05:00
purian23 0b7f2416ca distro: Bring up Stable 2025-12-15 23:10:24 -05:00
purian23 5d5c745ee5 Push the logs 2025-12-15 22:18:35 -05:00
purian23 e0429e4c60 distro: Re-add suffix 2025-12-15 21:31:13 -05:00
bbedward 0bece5287e dock: improve pinned app re-ordering feedback, fix vertical dock
ordering
fixes #1046
fixes #938
2025-12-15 20:46:36 -05:00
purian23 60b5e47836 update gitignore env 2025-12-15 19:06:43 -05:00
purian23 aa75b44790 distro: OBS version matching 2025-12-15 18:03:58 -05:00
bbedward 769f58caa9 displays: fix reverted state for position 2025-12-15 17:43:52 -05:00
bbedward e7facf740d update CHANGELOG 2025-12-15 17:18:59 -05:00
Austin Farmer 04921eef62 Move Ghostty Application Theming (#1047)
* Moved ghostty config

First test. Seems to work but probably broke something.

* Updated test
2025-12-15 17:16:46 -05:00
Oliver Portee 8863c42879 fix light mode/dark mode switch for stock themes (#1057) 2025-12-15 17:16:23 -05:00
bbedward 2745116ac5 displays: add configurator for niri, Hyprland, and MangoWC
- Configure position, VRR, orientation, resolution, refresh rate
- Split Display section into Configuration, Gamma, and Widgets
- MangoWC omits VRR because it doesnt have per-display VRR
- HDR configuration not present for Hyprland
2025-12-15 16:36:14 -05:00
50 changed files with 4928 additions and 1203 deletions
+57 -14
View File
@@ -7,13 +7,14 @@ on:
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"
tag_version:
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
required: false
default: ""
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:
tags:
- "v*"
schedule: schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds - cron: "0 */3 * * *" # Every 3 hours for dms-git builds
@@ -97,7 +98,7 @@ jobs:
# Rebuild requested - always proceed # Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)" echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates # Check each package and build list of those needing updates
@@ -161,16 +162,51 @@ jobs:
id: packages id: packages
run: | run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag push event - use the pushed tag
echo "packages=dms" >> $GITHUB_OUTPUT echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION" echo "Triggered by tag: $VERSION"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - dms-git only
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package" echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Use filtered packages from check-updates when package="all" and no rebuild requested # Manual workflow dispatch
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]]; then
# Determine version for dms stable
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
# For explicit dms selection, require tag_version
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
VERSION="${{ github.event.inputs.tag_version }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using specified tag: $VERSION"
else
echo "ERROR: tag_version is required when package=dms"
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
exit 1
fi
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
# For "all", auto-detect if tag_version not specified
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
VERSION="${{ github.event.inputs.tag_version }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using specified tag: $VERSION"
else
# Auto-detect latest release for "all"
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi
fi
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})" echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
else else
@@ -186,7 +222,7 @@ jobs:
run: | run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD) COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD) COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2") BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}" NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION" echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
@@ -207,14 +243,14 @@ jobs:
run: | run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD) COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD) COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2") BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}" NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION" echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
# Single changelog entry (git snapshots don't need history) # Single changelog entry (git snapshots don't need history)
CHANGELOG_DATE=$(date -R) CHANGELOG_DATE=$(date -R)
{ {
echo "dms-git ($NEW_VERSION) nightly; urgency=medium" echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
echo "" echo ""
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)" echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
echo "" echo ""
@@ -226,10 +262,15 @@ jobs:
run: | run: |
VERSION="${{ steps.packages.outputs.version }}" VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}" VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V" echo "==> Updating packaging files to version: $VERSION_NO_V"
# Update spec file
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Verify the update
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
# Single changelog entry (full history on OBS website) # Single changelog entry (full history on OBS website)
DATE_STR=$(date "+%a %b %d %Y") DATE_STR=$(date "+%a %b %d %Y")
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec) LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
@@ -256,13 +297,13 @@ jobs:
if [[ -f "distro/debian/dms/debian/changelog" ]]; then if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R) CHANGELOG_DATE=$(date -R)
{ {
echo "dms ($VERSION_NO_V) stable; urgency=medium" echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
echo "" echo ""
echo " * Update to $VERSION stable release" echo " * Update to $VERSION stable release"
echo "" echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog" } > "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V" echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
fi fi
- name: Install Go - name: Install Go
@@ -289,6 +330,7 @@ jobs:
- name: Upload to OBS - name: Upload to OBS
env: env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }} REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
TAG_VERSION: ${{ steps.packages.outputs.version }}
run: | run: |
PACKAGES="${{ steps.packages.outputs.packages }}" PACKAGES="${{ steps.packages.outputs.packages }}"
@@ -300,6 +342,7 @@ jobs:
MESSAGE="Automated update from GitHub Actions" MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}" MESSAGE="Update to ${{ steps.packages.outputs.version }}"
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check) # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
@@ -309,7 +352,7 @@ jobs:
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PKG to OBS..." echo "Uploading $PKG to OBS..."
if [[ -n "$REBUILD_RELEASE" ]]; then if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE" echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -350,7 +393,7 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
fi fi
+1 -1
View File
@@ -96,7 +96,7 @@ go.work
go.work.sum go.work.sum
# env file # env file
.env .env*
# Editor/IDE # Editor/IDE
# .idea/ # .idea/
+5
View File
@@ -1,6 +1,11 @@
This file is more of a quick reference so I know what to account for before next releases.
# 1.2.0 # 1.2.0
- Added clipboard and clipboard history integration - Added clipboard and clipboard history integration
- Added swipe to dismiss notification popups and from center - Added swipe to dismiss notification popups and from center
- Added paste from clipboard history view - requires wtype - Added paste from clipboard history view - requires wtype
- Optimize surface damage of OSD & Toast - Optimize surface damage of OSD & Toast
- Add monitor configurator (niri, Hyprland, MangoWC)
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
- requires intervention and doc update
+1 -1
View File
@@ -9,7 +9,7 @@ Type=dbus
BusName=org.freedesktop.Notifications BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always Restart=on-failure
RestartSec=1.23 RestartSec=1.23
TimeoutStopSec=10 TimeoutStopSec=10
+55 -20
View File
@@ -14,34 +14,63 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
## System Integration ## System Integration
**Wayland Protocols** ### Wayland Protocols (Client)
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
- `wp-viewporter` - Fractional scaling support
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
- `ext-workspace-v1` - Workspace protocol support
- `wlr-output-management-unstable-v1` - Display configuration
**DBus Interfaces** All Wayland protocols are consumed as a client - connecting to the compositor.
- NetworkManager/iwd - Network management
- logind - Session control and inhibit locks
- accountsservice - User account information
- CUPS - Printer management
- Custom IPC via unix socket (JSON API)
**Hardware Control** | Protocol | Purpose |
- DDC/CI protocol - External monitor brightness control (like `ddcutil`) | ----------------------------------------- | ----------------------------------------------------------- |
- Backlight control - Internal display brightness via `login1` or sysfs | `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
- LED control - Keyboard/device LED management | `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
- evdev input monitoring - Keyboard state tracking (caps lock, etc.) | `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
| `wlr-output-management-unstable-v1` | Display configuration |
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
| `ext-data-control-v1` | Clipboard history and persistence |
| `ext-workspace-v1` | Workspace integration |
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
### DBus Interfaces
**Client (consuming external services):**
| Interface | Purpose |
| -------------------------------- | --------------------------------------------- |
| `org.bluez` | Bluetooth management with pairing agent |
| `org.freedesktop.NetworkManager` | Network management |
| `net.connman.iwd` | iwd Wi-Fi backend |
| `org.freedesktop.network1` | systemd-networkd integration |
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
| `org.freedesktop.Accounts` | User account information |
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
| CUPS via IPP + D-Bus | Printer management with job notifications |
**Server (implementing interfaces):**
| Interface | Purpose |
| ----------------------------- | -------------------------------------- |
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
Custom IPC via unix socket (JSON API) for shell communication.
### Hardware Control
| Subsystem | Method | Purpose |
| --------- | ------------------- | ---------------------------------- |
| DDC/CI | I2C direct | External monitor brightness |
| Backlight | logind or sysfs | Internal display brightness |
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
| udev | netlink monitor | Backlight device updates (for OSD) |
### Plugin System
**Plugin System**
- Plugin registry integration - Plugin registry integration
- Plugin lifecycle management - Plugin lifecycle management
- Settings persistence - Settings persistence
## CLI Commands ## CLI Commands
- `dms run [-d]` - Start shell (optionally as daemon) - `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes - `dms restart` / `dms kill` - Manage running processes
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.) - `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
@@ -70,6 +99,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
Requires Go 1.24+ Requires Go 1.24+
**Development build:** **Development build:**
```bash ```bash
make # Build dms CLI make # Build dms CLI
make dankinstall # Build installer make dankinstall # Build installer
@@ -77,6 +107,7 @@ make test # Run tests
``` ```
**Distribution build:** **Distribution build:**
```bash ```bash
make dist # Build without update/greeter features make dist # Build without update/greeter features
``` ```
@@ -84,6 +115,7 @@ make dist # Build without update/greeter features
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64` Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
**Installation:** **Installation:**
```bash ```bash
sudo make install # Install to /usr/local/bin/dms sudo make install # Install to /usr/local/bin/dms
``` ```
@@ -91,6 +123,7 @@ sudo make install # Install to /usr/local/bin/dms
## Development ## Development
**Setup pre-commit hooks:** **Setup pre-commit hooks:**
```bash ```bash
git config core.hooksPath .githooks git config core.hooksPath .githooks
``` ```
@@ -98,6 +131,7 @@ git config core.hooksPath .githooks
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged. This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
**Regenerating Wayland Protocol Bindings:** **Regenerating Wayland Protocol Bindings:**
```bash ```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
@@ -105,6 +139,7 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
``` ```
**Module Structure:** **Module Structure:**
- `cmd/` - Binary entrypoints (dms, dankinstall) - `cmd/` - Binary entrypoints (dms, dankinstall)
- `internal/distros/` - Distribution-specific installation logic - `internal/distros/` - Distribution-specific installation logic
- `internal/proto/` - Wayland protocol bindings - `internal/proto/` - Wayland protocol bindings
+1 -1
View File
@@ -220,7 +220,7 @@ func getBaseVersion() string {
} }
// Fallback // Fallback
return "0.6.2" return "1.0.2"
} }
func startDebugServer() error { func startDebugServer() error {
+55 -9
View File
@@ -18,6 +18,25 @@ import (
type ipcTargets map[string]map[string][]string type ipcTargets map[string]map[string][]string
// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
if state == nil {
return 1
}
if code := state.ExitCode(); code != -1 {
return code
}
// Process was killed by signal - extract signal number
if status, ok := state.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
return 128 + int(status.Signal())
}
}
return 1
}
var isSessionManaged bool var isSessionManaged bool
func execDetachedRestart(targetPID int) { func execDetachedRestart(targetPID int) {
@@ -214,14 +233,28 @@ func runShellInteractive(session bool) {
for { for {
select { select {
case sig := <-sigChan: case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes if sig == syscall.SIGUSR1 {
if sig == syscall.SIGUSR1 && !isSessionManaged { if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...") log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid()) execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return return
} }
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
log.Infof("\nReceived signal %v, shutting down...", sig) log.Infof("\nReceived signal %v, shutting down...", sig)
cancel() cancel()
cmd.Process.Signal(syscall.SIGTERM) cmd.Process.Signal(syscall.SIGTERM)
@@ -235,7 +268,7 @@ func runShellInteractive(session bool) {
cmd.Process.Signal(syscall.SIGTERM) cmd.Process.Signal(syscall.SIGTERM)
} }
os.Remove(socketPath) os.Remove(socketPath)
os.Exit(1) os.Exit(getProcessExitCode(cmd.ProcessState))
} }
} }
} }
@@ -440,15 +473,28 @@ func runShellDaemon(session bool) {
for { for {
select { select {
case sig := <-sigChan: case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes if sig == syscall.SIGUSR1 {
if sig == syscall.SIGUSR1 && !isSessionManaged { if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...") log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid()) execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return return
} }
// All other signals: clean shutdown // Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
cancel() cancel()
cmd.Process.Signal(syscall.SIGTERM) cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath) os.Remove(socketPath)
@@ -460,7 +506,7 @@ func runShellDaemon(session bool) {
cmd.Process.Signal(syscall.SIGTERM) cmd.Process.Signal(syscall.SIGTERM)
} }
os.Remove(socketPath) os.Remove(socketPath)
os.Exit(1) os.Exit(getProcessExitCode(cmd.ProcessState))
} }
} }
} }
+7 -1
View File
@@ -265,7 +265,13 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
colorResult := DeploymentResult{ colorResult := DeploymentResult{
ConfigType: "Ghostty Colors", ConfigType: "Ghostty Colors",
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"), Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "themes", "dankcolors"),
}
themesDir := filepath.Dir(colorResult.Path)
if err := os.MkdirAll(themesDir, 0755); err != nil {
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
} }
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil { if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
+1 -1
View File
@@ -468,7 +468,7 @@ func TestHyprlandConfigStructure(t *testing.T) {
func TestGhosttyConfigStructure(t *testing.T) { func TestGhosttyConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyConfig, "window-decoration = false") assert.Contains(t, GhosttyConfig, "window-decoration = false")
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0") assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors") assert.Contains(t, GhosttyConfig, "theme = dankcolors")
} }
func TestGhosttyColorConfigStructure(t *testing.T) { func TestGhosttyColorConfigStructure(t *testing.T) {
+1 -1
View File
@@ -48,4 +48,4 @@ keybind = shift+enter=text:\n
gtk-single-instance = true gtk-single-instance = true
# Dank color generation # Dank color generation
config-file = ./config-dankcolors theme = dankcolors
@@ -238,7 +238,7 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy == nil { if proxy == nil || proxy.IsZombie() {
head := &ZwlrOutputHeadV1{} head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context()) head.SetContext(i.Context())
head.SetID(objectID) head.SetID(objectID)
@@ -723,7 +723,7 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy == nil { if proxy == nil || proxy.IsZombie() {
mode := &ZwlrOutputModeV1{} mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context()) mode.SetContext(i.Context())
mode.SetID(objectID) mode.SetID(objectID)
@@ -761,8 +761,8 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy == nil { if proxy == nil || proxy.IsZombie() {
// Mode not yet registered, create it // Mode not yet registered or zombie, create fresh
mode := &ZwlrOutputModeV1{} mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context()) mode.SetContext(i.Context())
mode.SetID(objectID) mode.SetID(objectID)
+13 -8
View File
@@ -145,6 +145,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name) log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
head.name = e.Name head.name = e.Name
head.ready = true
m.post(func() { m.post(func() {
m.updateState() m.updateState()
}) })
@@ -251,11 +252,11 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
m.heads.Delete(headID) m.heads.Delete(headID)
m.post(func() { m.wlMutex.Lock()
m.wlMutex.Lock() handle.Release()
handle.Release() m.wlMutex.Unlock()
m.wlMutex.Unlock()
m.post(func() {
m.updateState() m.updateState()
}) })
}) })
@@ -310,11 +311,11 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
m.modes.Delete(modeID) m.modes.Delete(modeID)
m.post(func() { m.wlMutex.Lock()
m.wlMutex.Lock() handle.Release()
handle.Release() m.wlMutex.Unlock()
m.wlMutex.Unlock()
m.post(func() {
m.updateState() m.updateState()
}) })
}) })
@@ -328,6 +329,10 @@ func (m *Manager) updateState() {
return true return true
} }
if !head.ready {
return true
}
modes := make([]OutputMode, 0) modes := make([]OutputMode, 0)
var currentMode *OutputMode var currentMode *OutputMode
+1
View File
@@ -90,6 +90,7 @@ type headState struct {
modeIDs []uint32 modeIDs []uint32
adaptiveSync uint32 adaptiveSync uint32
finished bool finished bool
ready bool
} }
type modeState struct { type modeState struct {
+1 -18
View File
@@ -1,4 +1,4 @@
dms-git (1.0.2+git2528.d336866f) nightly; urgency=medium dms-git (1.0.2+git2528.d336866fdb1) nightly; urgency=medium
* Git snapshot (commit 2528: d336866f) * Git snapshot (commit 2528: d336866f)
@@ -16,23 +16,6 @@ dms-git (1.0.2+git2518.a783d650) nightly; urgency=medium
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 15:11:40 +0000 -- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 15:11:40 +0000
dms-git (1.0.2+git2510.0f89886c) nightly; urgency=medium
* Git snapshot (commit 2510: 0f89886c)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:46:43 +0000
dms-git (1.0.2+git2507.b2ac9c6c) nightly; urgency=medium
* Git snapshot (commit 2507: b2ac9c6c)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:18:05 +0000
dms-git (1.0.2+git2505.82f881af) nightly; urgency=medium
* Git snapshot (commit 2505: 82f881af)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 05:55:03 +0000
dms-git (1.0.0+git2419.993f14a3) nightly; urgency=medium dms-git (1.0.0+git2419.993f14a3) nightly; urgency=medium
+3 -3
View File
@@ -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/v1.0.3.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/v1.0.3/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/v1.0.3/dms-distropkg-arm64.gz</param>
</service> </service>
</services> </services>
+8 -2
View File
@@ -1,6 +1,12 @@
dms (1.0.2ppa6) unstable; urgency=medium dms (1.0.3db1) unstable; urgency=medium
* Rebuild to fix repository metadata issues * Update to v1.0.3 stable release
-- Avenge Media <AvengeMedia.US@gmail.com> Mon, 16 Dec 2025 10:00:00 +0000
dms (1.0.2db1) unstable; urgency=medium
* Update to v1.0.2 stable release
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:47:39 +0000 -- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:47:39 +0000
+5 -2
View File
@@ -3,8 +3,8 @@
%global debug_package %{nil} %global debug_package %{nil}
Name: dms Name: dms
Version: 1.0.2 Version: 1.0.3
Release: 7%{?dist} Release: 1%{?dist}
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
License: MIT License: MIT
@@ -105,6 +105,9 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%changelog %changelog
* Mon Dec 16 2025 AvengeMedia <maintainer@avengemedia.com> - 1.0.3-1
- Update to stable v1.0.3 release
* Fri Dec 12 2025 AvengeMedia <maintainer@avengemedia.com> - 1.0.2-1 * Fri Dec 12 2025 AvengeMedia <maintainer@avengemedia.com> - 1.0.2-1
- Update to stable v1.0.2 release - Update to stable v1.0.2 release
- Bug fixes and improvements - Bug fixes and improvements
+62 -29
View File
@@ -7,8 +7,8 @@
# ./distro/scripts/obs-upload.sh dms "Update to v1.0.2" # ./distro/scripts/obs-upload.sh dms "Update to v1.0.2"
# ./distro/scripts/obs-upload.sh debian dms # ./distro/scripts/obs-upload.sh debian dms
# ./distro/scripts/obs-upload.sh opensuse dms-git # ./distro/scripts/obs-upload.sh opensuse dms-git
# ./distro/scripts/obs-upload.sh debian dms-git 2 # Rebuild with ppa2 suffix # ./distro/scripts/obs-upload.sh debian dms-git 2 # Rebuild with db2 suffix
# ./distro/scripts/obs-upload.sh dms-git --rebuild=2 # Rebuild with ppa2 suffix (flag syntax) # ./distro/scripts/obs-upload.sh dms-git --rebuild=2 # Rebuild with db2 suffix (flag syntax)
set -e set -e
@@ -126,8 +126,8 @@ check_obs_version_exists() {
OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs) OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs)
# Commit hash check for -git packages # Commit hash check for -git packages
if [[ "$CHECK_MODE" == "commit" ]] && [[ "$PACKAGE" == *"-git" ]]; then if [[ "$CHECK_MODE" == "commit" ]] && [[ "$PACKAGE" == *"-git" ]]; then
OBS_COMMIT=$(echo "$OBS_VERSION" | grep -oP '\.([a-f0-9]{8})(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "") OBS_COMMIT=$(echo "$OBS_VERSION" | grep -oP '\.([a-f0-9]{8})(db[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
NEW_COMMIT=$(echo "$VERSION" | grep -oP '\.([a-f0-9]{8})(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "") NEW_COMMIT=$(echo "$VERSION" | grep -oP '\.([a-f0-9]{8})(db[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
if [[ -n "$OBS_COMMIT" && -n "$NEW_COMMIT" && "$OBS_COMMIT" == "$NEW_COMMIT" ]]; then if [[ -n "$OBS_COMMIT" && -n "$NEW_COMMIT" && "$OBS_COMMIT" == "$NEW_COMMIT" ]]; then
echo "⚠️ Commit $NEW_COMMIT already exists in OBS (current version: $OBS_VERSION)" echo "⚠️ Commit $NEW_COMMIT already exists in OBS (current version: $OBS_VERSION)"
@@ -279,7 +279,8 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
# Apply rebuild suffix if specified (must happen before API check) # Apply rebuild suffix if specified (must happen before API check)
if [[ -n "$REBUILD_RELEASE" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then if [[ -n "$REBUILD_RELEASE" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
CHANGELOG_VERSION="${CHANGELOG_VERSION}ppa${REBUILD_RELEASE}" BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/db[0-9]*$//')
CHANGELOG_VERSION="${BASE_VERSION}db${REBUILD_RELEASE}"
echo " - Applied rebuild suffix: $CHANGELOG_VERSION" echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
fi fi
@@ -307,12 +308,16 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
else else
# Rebuild number specified - check if this exact version already exists (exact mode) # Rebuild number specified - check if this exact version already exists (exact mode)
if check_obs_version_exists "$OBS_PROJECT" "$PACKAGE" "$CHANGELOG_VERSION" "exact"; then if check_obs_version_exists "$OBS_PROJECT" "$PACKAGE" "$CHANGELOG_VERSION" "exact"; then
echo "==> Error: Version $CHANGELOG_VERSION already exists in OBS" echo "==> Version $CHANGELOG_VERSION already exists in OBS"
echo " This exact version (including ppa${REBUILD_RELEASE}) is already uploaded." echo " This exact version (including db${REBUILD_RELEASE}) is already uploaded."
echo " To rebuild with a different release number, try incrementing:" echo " Skipping upload - nothing to do."
echo ""
echo " 💡 To rebuild with a different release number, try incrementing:"
NEXT_NUM=$((REBUILD_RELEASE + 1)) NEXT_NUM=$((REBUILD_RELEASE + 1))
echo " ./distro/scripts/obs-upload.sh $PACKAGE $NEXT_NUM" echo " REBUILD_RELEASE=$NEXT_NUM"
exit 1 echo ""
echo "✓ Exiting gracefully (no changes needed)"
exit 0
fi fi
fi fi
fi fi
@@ -511,7 +516,7 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
if [[ -n "$URL_PROTOCOL" && -n "$URL_HOST" && -n "$URL_PATH" ]]; then if [[ -n "$URL_PROTOCOL" && -n "$URL_HOST" && -n "$URL_PATH" ]]; then
SOURCE_URL="${URL_PROTOCOL}://${URL_HOST}${URL_PATH}" SOURCE_URL="${URL_PROTOCOL}://${URL_HOST}${URL_PATH}"
echo " Downloading source from: $SOURCE_URL" echo "==> Downloading source from: $SOURCE_URL"
if wget -q -O "$TEMP_DIR/source-archive" "$SOURCE_URL" 2>/dev/null || if wget -q -O "$TEMP_DIR/source-archive" "$SOURCE_URL" 2>/dev/null ||
curl -L -f -s -o "$TEMP_DIR/source-archive" "$SOURCE_URL" 2>/dev/null; then curl -L -f -s -o "$TEMP_DIR/source-archive" "$SOURCE_URL" 2>/dev/null; then
@@ -534,9 +539,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
fi fi
SOURCE_DIR=$(cd "$SOURCE_DIR" && pwd) SOURCE_DIR=$(cd "$SOURCE_DIR" && pwd)
cd "$REPO_ROOT" cd "$REPO_ROOT"
if [[ "$(pwd)" != "$REPO_ROOT" ]]; then
echo "ERROR: Failed to return to REPO_ROOT. Expected: $REPO_ROOT, Got: $(pwd)"
exit 1
fi
else else
echo "Error: Failed to download source from $SOURCE_URL" echo "ERROR: Failed to download source from $SOURCE_URL"
echo "Tried both wget and curl. Please check the URL and network connectivity." echo "Attempted both wget and curl"
echo "Please check:"
echo " 1. URL is accessible: $SOURCE_URL"
echo " 2. _service file has correct version"
echo " 3. GitHub releases are available"
exit 1 exit 1
fi fi
fi fi
@@ -553,7 +566,7 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
exit 1 exit 1
fi fi
echo " Found source directory: $SOURCE_DIR" echo "==> Found source directory: $SOURCE_DIR"
# Vendor Go dependencies for dms-git # Vendor Go dependencies for dms-git
if [[ "$PACKAGE" == "dms-git" ]] && [[ -d "$SOURCE_DIR/core" ]]; then if [[ "$PACKAGE" == "dms-git" ]] && [[ -d "$SOURCE_DIR/core" ]]; then
@@ -712,6 +725,10 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
TARBALL_BASE=$(basename "$SOURCE_DIR") TARBALL_BASE=$(basename "$SOURCE_DIR")
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE" tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
cd "$REPO_ROOT" cd "$REPO_ROOT"
if [[ "$(pwd)" != "$REPO_ROOT" ]]; then
echo "ERROR: Failed to return to REPO_ROOT after tarball creation"
exit 1
fi
if [[ "$PACKAGE" == "dms" ]]; then if [[ "$PACKAGE" == "dms" ]]; then
TARBALL_DIR=$(tar -tzf "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null | head -1 | cut -d'/' -f1) TARBALL_DIR=$(tar -tzf "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null | head -1 | cut -d'/' -f1)
@@ -723,6 +740,10 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
rm -f "$WORK_DIR/$COMBINED_TARBALL" rm -f "$WORK_DIR/$COMBINED_TARBALL"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE" tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
cd "$REPO_ROOT" cd "$REPO_ROOT"
if [[ "$(pwd)" != "$REPO_ROOT" ]]; then
echo "ERROR: Failed to return to REPO_ROOT after tarball recreation"
exit 1
fi
fi fi
fi fi
@@ -796,23 +817,29 @@ EOF
fi fi
fi fi
cd "$WORK_DIR" echo "==> Ensuring we're in the OSC working directory"
cd "$WORK_DIR" || {
echo "ERROR: Cannot cd to WORK_DIR: $WORK_DIR"
echo "DEBUG: Current directory: $(pwd)"
echo "DEBUG: WORK_DIR exists: $(test -d "$WORK_DIR" && echo "yes" || echo "no")"
exit 1
}
echo "DEBUG: Successfully entered WORK_DIR: $(pwd)"
# Server-side cleanup via API # Server-side cleanup via API
echo "==> Cleaning old tarballs from OBS server (prevents downloading 100+ old versions)" echo "==> Cleaning old tarballs from OBS server (prevents downloading 100+ old versions)"
OBS_FILES=$(osc api "/source/$OBS_PROJECT/$PACKAGE" 2>/dev/null || echo "") OBS_FILES=$(osc api "/source/$OBS_PROJECT/$PACKAGE" 2>/dev/null || echo "")
if [[ -n "$OBS_FILES" ]]; then if [[ -n "$OBS_FILES" ]]; then
DELETED_COUNT=0 DELETED_COUNT=0
KEEP_PATTERN="" KEEP_CURRENT=""
if [[ -n "$CHANGELOG_VERSION" ]]; then if [[ -n "$CHANGELOG_VERSION" ]]; then
BASE_KEEP_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//') KEEP_CURRENT="${PACKAGE}_${CHANGELOG_VERSION}.tar.gz"
KEEP_PATTERN="${PACKAGE}_${BASE_KEEP_VERSION}" echo " Keeping only current version: ${KEEP_CURRENT}"
echo " Keeping tarballs matching: ${KEEP_PATTERN}*"
fi fi
for old_file in $(echo "$OBS_FILES" | grep -oP '(?<=name=")[^"]*\.(tar\.gz|tar\.xz|tar\.bz2)(?=")' || true); do for old_file in $(echo "$OBS_FILES" | grep -oP '(?<=name=")[^"]*\.(tar\.gz|tar\.xz|tar\.bz2)(?=")' || true); do
if [[ -n "$KEEP_PATTERN" ]] && [[ "$old_file" == ${KEEP_PATTERN}* ]]; then if [[ "$old_file" == "$KEEP_CURRENT" ]]; then
echo " - Keeping current version: $old_file" echo " - Keeping: $old_file"
continue continue
fi fi
@@ -835,14 +862,11 @@ else
echo " ⚠️ Could not fetch file list from server, skipping cleanup" echo " ⚠️ Could not fetch file list from server, skipping cleanup"
fi fi
# Fallback update with --server-side-source-service-files flag only syncs metadata (spec, dsc, _service) # Update working copy to latest revision (without expanding service files to avoid revision conflicts)
echo "==> Updating working copy" echo "==> Updating working copy"
if ! osc up --server-side-source-service-files 2>/dev/null; then if ! osc up 2>/dev/null; then
echo " Note: Using regular update (--server-side-source-service-files not supported)" echo "Error: Failed to update working copy"
if ! osc up; then exit 1
echo "Error: Failed to update working copy"
exit 1
fi
fi fi
# Ensure we're in WORK_DIR and it exists # Ensure we're in WORK_DIR and it exists
@@ -882,6 +906,15 @@ elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
fi fi
echo "" echo ""
if [[ "$(pwd)" != "$WORK_DIR" ]]; then
echo "ERROR: Lost directory context. Expected: $WORK_DIR, Got: $(pwd)"
cd "$WORK_DIR" || {
echo "FATAL: Cannot recover - unable to cd to WORK_DIR"
exit 1
}
echo "WARNING: Recovered directory context"
fi
osc addremove 2>&1 | grep -v "Git SCM package" || true osc addremove 2>&1 | grep -v "Git SCM package" || true
SOURCE_TARBALL="${PACKAGE}-source.tar.gz" SOURCE_TARBALL="${PACKAGE}-source.tar.gz"
@@ -908,7 +941,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
else else
echo "==> Committing to OBS" echo "==> Committing to OBS"
set +e set +e
osc commit -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs" osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
COMMIT_EXIT=${PIPESTATUS[0]} COMMIT_EXIT=${PIPESTATUS[0]}
set -e set -e
if [[ $COMMIT_EXIT -ne 0 ]]; then if [[ $COMMIT_EXIT -ne 0 ]]; then
+179 -96
View File
@@ -1,5 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
@@ -360,47 +360,49 @@ Singleton {
property string displayNameMode: "system" property string displayNameMode: "system"
property var screenPreferences: ({}) property var screenPreferences: ({})
property var showOnLastDisplay: ({}) property var showOnLastDisplay: ({})
property var niriOutputSettings: ({})
property var hyprlandOutputSettings: ({})
property var barConfigs: [ property var barConfigs: [
{ {
id: "default", "id": "default",
name: "Main Bar", "name": "Main Bar",
enabled: true, "enabled": true,
position: 0, "position": 0,
screenPreferences: ["all"], "screenPreferences": ["all"],
showOnLastDisplay: true, "showOnLastDisplay": true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"], "leftWidgets": ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"], "centerWidgets": ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"], "rightWidgets": ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4, "spacing": 4,
innerPadding: 4, "innerPadding": 4,
bottomGap: 0, "bottomGap": 0,
transparency: 1.0, "transparency": 1.0,
widgetTransparency: 1.0, "widgetTransparency": 1.0,
squareCorners: false, "squareCorners": false,
noBackground: false, "noBackground": false,
gothCornersEnabled: false, "gothCornersEnabled": false,
gothCornerRadiusOverride: false, "gothCornerRadiusOverride": false,
gothCornerRadiusValue: 12, "gothCornerRadiusValue": 12,
borderEnabled: false, "borderEnabled": false,
borderColor: "surfaceText", "borderColor": "surfaceText",
borderOpacity: 1.0, "borderOpacity": 1.0,
borderThickness: 1, "borderThickness": 1,
widgetOutlineEnabled: false, "widgetOutlineEnabled": false,
widgetOutlineColor: "primary", "widgetOutlineColor": "primary",
widgetOutlineOpacity: 1.0, "widgetOutlineOpacity": 1.0,
widgetOutlineThickness: 1, "widgetOutlineThickness": 1,
fontScale: 1.0, "fontScale": 1.0,
autoHide: false, "autoHide": false,
autoHideDelay: 250, "autoHideDelay": 250,
openOnOverview: false, "openOnOverview": false,
visible: true, "visible": true,
popupGapsAuto: true, "popupGapsAuto": true,
popupGapsManual: 4, "popupGapsManual": 4,
maximizeDetection: true, "maximizeDetection": true,
scrollEnabled: true, "scrollEnabled": true,
scrollXBehavior: "column", "scrollXBehavior": "column",
scrollYBehavior: "workspace" "scrollYBehavior": "workspace"
} }
] ]
@@ -458,25 +460,25 @@ Singleton {
const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0 const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
settings_file="$config_dir/settings.ini" settings_file="$config_dir/settings.ini"
if [ -f "$settings_file" ]; then if [ -f "$settings_file" ]; then
if grep -q "^gtk-icon-theme-name=" "$settings_file"; then if grep -q "^gtk-icon-theme-name=" "$settings_file"; then
sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file" sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file"
else else
if grep -q "\\[Settings\\]" "$settings_file"; then if grep -q "\\[Settings\\]" "$settings_file"; then
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file" sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
else else
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file" echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
fi
fi fi
else fi
else
echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file" echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file"
fi fi
done done
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
pkill -HUP -f 'gtk' 2>/dev/null || true`; pkill -HUP -f 'gtk' 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", configScript]); Quickshell.execDetached(["sh", "-lc", configScript]);
} }
@@ -489,36 +491,36 @@ pkill -HUP -f 'gtk' 2>/dev/null || true`;
const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''"); const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''");
const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true
update_qt_icon_theme() { update_qt_icon_theme() {
local config_file="$1" local config_file="$1"
local theme_name="$2" local theme_name="$2"
if [ -f "$config_file" ]; then if [ -f "$config_file" ]; then
if grep -q "^\\[Appearance\\]" "$config_file"; then if grep -q "^\\[Appearance\\]" "$config_file"; then
if grep -q "^icon_theme=" "$config_file"; then if grep -q "^icon_theme=" "$config_file"; then
sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file" sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file"
else else
sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file" sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file"
fi fi
else else
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file" printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
fi fi
else else
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file" printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
fi fi
} }
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}' update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}' update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`; rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", script]); Quickshell.execDetached(["sh", "-lc", script]);
} }
readonly property var _hooks: ({ readonly property var _hooks: ({
applyStoredTheme: applyStoredTheme, "applyStoredTheme": applyStoredTheme,
regenSystemThemes: regenSystemThemes, "regenSystemThemes": regenSystemThemes,
updateNiriLayout: updateNiriLayout, "updateNiriLayout": updateNiriLayout,
applyStoredIconTheme: applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
updateBarConfigs: updateBarConfigs "updateBarConfigs": updateBarConfigs
}) })
function set(key, value) { function set(key, value) {
@@ -723,7 +725,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
let leftBar = 0; let leftBar = 0;
let rightBar = 0; let rightBar = 0;
for (let i = 0; i < enabledBars.length; i++) { for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i]; const other = enabledBars[i];
if (other.id === barConfig.id) if (other.id === barConfig.id)
continue; continue;
@@ -793,7 +795,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
if (barConfig) { if (barConfig) {
const enabledBars = getEnabledBarConfigs(); const enabledBars = getEnabledBarConfigs();
for (let i = 0; i < enabledBars.length; i++) { for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i]; const other = enabledBars[i];
if (other.id === barConfig.id) if (other.id === barConfig.id)
continue; continue;
@@ -925,7 +927,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const conflicts = []; const conflicts = [];
const enabledBars = getEnabledBarConfigs(); const enabledBars = getEnabledBarConfigs();
for (let i = 0; i < enabledBars.length; i++) { for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i]; const other = enabledBars[i];
if (other.id === barId) if (other.id === barId)
continue; continue;
@@ -938,9 +940,9 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const hasAll = barScreens.includes("all") || otherScreens.includes("all"); const hasAll = barScreens.includes("all") || otherScreens.includes("all");
if (hasAll) { if (hasAll) {
conflicts.push({ conflicts.push({
barId: other.id, "barId": other.id,
barName: other.name, "barName": other.name,
reason: "Same position on all screens" "reason": "Same position on all screens"
}); });
continue; continue;
} }
@@ -948,9 +950,9 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const overlapping = barScreens.some(screen => otherScreens.includes(screen)); const overlapping = barScreens.some(screen => otherScreens.includes(screen));
if (overlapping) { if (overlapping) {
conflicts.push({ conflicts.push({
barId: other.id, "barId": other.id,
barName: other.name, "barName": other.name,
reason: "Same position on overlapping screens" "reason": "Same position on overlapping screens"
}); });
} }
} }
@@ -972,7 +974,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
function getScreensSortedByPosition() { function getScreensSortedByPosition() {
const screens = []; const screens = [];
for (let i = 0; i < Quickshell.screens.length; i++) { for (var i = 0; i < Quickshell.screens.length; i++) {
screens.push(Quickshell.screens[i]); screens.push(Quickshell.screens[i]);
} }
screens.sort((a, b) => { screens.sort((a, b) => {
@@ -989,7 +991,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const sorted = getScreensSortedByPosition(); const sorted = getScreensSortedByPosition();
let modelCount = 0; let modelCount = 0;
let screenIndex = -1; let screenIndex = -1;
for (let i = 0; i < sorted.length; i++) { for (var i = 0; i < sorted.length; i++) {
if (sorted[i].model === screen.model) { if (sorted[i].model === screen.model) {
if (sorted[i].name === screen.name) { if (sorted[i].name === screen.name) {
screenIndex = modelCount; screenIndex = modelCount;
@@ -1187,7 +1189,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
spacing: spacing "spacing": spacing
}); });
} }
if (typeof NiriService !== "undefined" && CompositorService.isNiri) { if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
@@ -1216,7 +1218,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
return; return;
} }
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
position: position "position": position
}); });
} }
@@ -1224,7 +1226,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
leftWidgets: order "leftWidgets": order
}); });
updateListModel(leftWidgetsModel, order); updateListModel(leftWidgetsModel, order);
} }
@@ -1234,7 +1236,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
centerWidgets: order "centerWidgets": order
}); });
updateListModel(centerWidgetsModel, order); updateListModel(centerWidgetsModel, order);
} }
@@ -1244,7 +1246,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
rightWidgets: order "rightWidgets": order
}); });
updateListModel(rightWidgetsModel, order); updateListModel(rightWidgetsModel, order);
} }
@@ -1257,9 +1259,9 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
leftWidgets: defaultLeft, "leftWidgets": defaultLeft,
centerWidgets: defaultCenter, "centerWidgets": defaultCenter,
rightWidgets: defaultRight "rightWidgets": defaultRight
}); });
} }
updateListModel(leftWidgetsModel, defaultLeft); updateListModel(leftWidgetsModel, defaultLeft);
@@ -1307,7 +1309,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const defaultBar = barConfigs[0] || getBarConfig("default"); const defaultBar = barConfigs[0] || getBarConfig("default");
if (defaultBar) { if (defaultBar) {
updateBarConfig(defaultBar.id, { updateBarConfig(defaultBar.id, {
visible: !defaultBar.visible "visible": !defaultBar.visible
}); });
} }
} }
@@ -1345,6 +1347,87 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
return settings ? JSON.parse(JSON.stringify(settings)) : {}; return settings ? JSON.parse(JSON.stringify(settings)) : {};
} }
function getNiriOutputSetting(outputId, key, defaultValue) {
if (!niriOutputSettings[outputId])
return defaultValue;
return niriOutputSettings[outputId][key] !== undefined ? niriOutputSettings[outputId][key] : defaultValue;
}
function setNiriOutputSetting(outputId, key, value) {
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
if (!updated[outputId])
updated[outputId] = {};
updated[outputId][key] = value;
niriOutputSettings = updated;
saveSettings();
}
function getNiriOutputSettings(outputId) {
const settings = niriOutputSettings[outputId];
return settings ? JSON.parse(JSON.stringify(settings)) : {};
}
function setNiriOutputSettings(outputId, settings) {
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
updated[outputId] = settings;
niriOutputSettings = updated;
saveSettings();
}
function removeNiriOutputSettings(outputId) {
if (!niriOutputSettings[outputId])
return;
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
delete updated[outputId];
niriOutputSettings = updated;
saveSettings();
}
function getHyprlandOutputSetting(outputId, key, defaultValue) {
if (!hyprlandOutputSettings[outputId])
return defaultValue;
return hyprlandOutputSettings[outputId][key] !== undefined ? hyprlandOutputSettings[outputId][key] : defaultValue;
}
function setHyprlandOutputSetting(outputId, key, value) {
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
if (!updated[outputId])
updated[outputId] = {};
updated[outputId][key] = value;
hyprlandOutputSettings = updated;
saveSettings();
}
function removeHyprlandOutputSetting(outputId, key) {
if (!hyprlandOutputSettings[outputId] || !(key in hyprlandOutputSettings[outputId]))
return;
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
delete updated[outputId][key];
hyprlandOutputSettings = updated;
saveSettings();
}
function getHyprlandOutputSettings(outputId) {
const settings = hyprlandOutputSettings[outputId];
return settings ? JSON.parse(JSON.stringify(settings)) : {};
}
function setHyprlandOutputSettings(outputId, settings) {
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
updated[outputId] = settings;
hyprlandOutputSettings = updated;
saveSettings();
}
function removeHyprlandOutputSettings(outputId) {
if (!hyprlandOutputSettings[outputId])
return;
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
delete updated[outputId];
hyprlandOutputSettings = updated;
saveSettings();
}
ListModel { ListModel {
id: leftWidgetsModel id: leftWidgetsModel
} }
+2 -1
View File
@@ -918,6 +918,7 @@ Singleton {
function buildMatugenColorsFromTheme(darkTheme, lightTheme) { function buildMatugenColorsFromTheme(darkTheme, lightTheme) {
const colors = {}; const colors = {};
const isLight = SessionData !== "undefined" && SessionData.isLightMode;
function addColor(matugenKey, darkVal, lightVal) { function addColor(matugenKey, darkVal, lightVal) {
if (!darkVal && !lightVal) if (!darkVal && !lightVal)
@@ -930,7 +931,7 @@ Singleton {
"color": String(lightVal || darkVal) "color": String(lightVal || darkVal)
}, },
"default": { "default": {
"color": String(darkVal || lightVal) "color": String((isLight && lightVal) ? lightVal : darkVal)
} }
}; };
} }
@@ -258,6 +258,8 @@ var SPEC = {
displayNameMode: { def: "system" }, displayNameMode: { def: "system" },
screenPreferences: { def: {} }, screenPreferences: { def: {} },
showOnLastDisplay: { def: {} }, showOnLastDisplay: { def: {} },
niriOutputSettings: { def: {} },
hyprlandOutputSettings: { def: {} },
barConfigs: { def: [{ barConfigs: { def: [{
id: "default", id: "default",
+4 -4
View File
@@ -273,8 +273,8 @@ Item {
anchors { anchors {
left: true left: true
top: true top: true
right: root.useSingleWindow ? true : undefined right: root.useSingleWindow
bottom: root.useSingleWindow ? true : undefined bottom: root.useSingleWindow
} }
WlrLayershell.margins { WlrLayershell.margins {
@@ -284,8 +284,8 @@ Item {
bottom: 0 bottom: 0
} }
implicitWidth: root.useSingleWindow ? undefined : root.alignedWidth + (shadowBuffer * 2) implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.useSingleWindow ? undefined : root.alignedHeight + (shadowBuffer * 2) implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
+51 -84
View File
@@ -7,10 +7,11 @@ DankModal {
id: root id: root
property string outputName: "" property string outputName: ""
property var position: undefined property var changes: []
property var mode: undefined property int countdown: 10
property var vrr: undefined
property int countdown: 15 signal confirmed
signal reverted
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
@@ -23,23 +24,27 @@ DankModal {
repeat: true repeat: true
running: root.shouldBeVisible running: root.shouldBeVisible
onTriggered: { onTriggered: {
countdown--; root.countdown--;
if (countdown <= 0) { if (root.countdown <= 0) {
revert(); root.reverted();
root.close();
} }
} }
} }
onOpened: { onOpened: {
countdown = 15; countdown = 10;
countdownTimer.start(); countdownTimer.start();
} }
onClosed: { onDialogClosed: {
countdownTimer.stop(); countdownTimer.stop();
} }
onBackgroundClicked: revert onBackgroundClicked: {
root.reverted();
root.close();
}
content: Component { content: Component {
FocusScope { FocusScope {
@@ -50,12 +55,14 @@ DankModal {
implicitHeight: mainColumn.implicitHeight implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
revert(); root.reverted();
root.close();
event.accepted = true; event.accepted = true;
} }
Keys.onReturnPressed: event => { Keys.onReturnPressed: event => {
confirm(); root.confirmed();
root.close();
event.accepted = true; event.accepted = true;
} }
@@ -69,81 +76,42 @@ DankModal {
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
spacing: Theme.spacingM spacing: Theme.spacingM
Column { StyledText {
width: parent.width text: I18n.tr("Confirm Display Changes")
spacing: Theme.spacingXS font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
StyledText { font.weight: Font.Medium
text: I18n.tr("Confirm Display Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Display settings for ") + outputName
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
}
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 80 height: 70
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest color: Theme.surfaceContainerHighest
Column { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 text: root.countdown + "s"
font.pixelSize: Theme.fontSizeXLarge * 1.5
StyledText { color: Theme.primary
text: I18n.tr("Reverting in:") font.weight: Font.Bold
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: countdown + "s"
font.pixelSize: Theme.fontSizeXLarge * 1.5
color: Theme.primary
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
} }
} }
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: root.changes.length > 0
StyledText { Repeater {
text: I18n.tr("Changes:") model: root.changes
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
StyledText { StyledText {
visible: position !== undefined && position !== null required property var modelData
text: I18n.tr("Position: ") + (position ? position.x + ", " + position.y : "") text: modelData
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceVariantText
} }
StyledText {
visible: mode !== undefined && mode !== null && mode !== ""
text: I18n.tr("Mode: ") + (mode || "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
visible: vrr !== undefined && vrr !== null
text: I18n.tr("VRR: ") + (vrr ? I18n.tr("Enabled") : I18n.tr("Disabled"))
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
} }
} }
@@ -180,7 +148,10 @@ DankModal {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: revert onClicked: {
root.reverted();
root.close();
}
} }
} }
@@ -206,7 +177,10 @@ DankModal {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: confirm onClicked: {
root.confirmed();
root.close();
}
} }
Behavior on color { Behavior on color {
@@ -228,18 +202,11 @@ DankModal {
iconName: "close" iconName: "close"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: revert onClicked: {
root.reverted();
root.close();
}
} }
} }
} }
function confirm() {
displaysTab.confirmChanges();
close();
}
function revert() {
displaysTab.revertChanges();
close();
}
} }
+57 -51
View File
@@ -32,9 +32,8 @@ FocusScope {
} }
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -48,9 +47,8 @@ FocusScope {
sourceComponent: TimeWeatherTab {} sourceComponent: TimeWeatherTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -66,9 +64,8 @@ FocusScope {
} }
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -84,9 +81,8 @@ FocusScope {
} }
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -100,9 +96,8 @@ FocusScope {
sourceComponent: WorkspacesTab {} sourceComponent: WorkspacesTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -118,25 +113,53 @@ FocusScope {
} }
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
Loader { Loader {
id: displaysLoader id: displayConfigLoader
anchors.fill: parent anchors.fill: parent
active: root.currentIndex === 6 active: root.currentIndex === 24
visible: active visible: active
focus: active focus: active
sourceComponent: DisplaysTab {} sourceComponent: DisplayConfigTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: gammaControlLoader
anchors.fill: parent
active: root.currentIndex === 25
visible: active
focus: active
sourceComponent: GammaControlTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: displayWidgetsLoader
anchors.fill: parent
active: root.currentIndex === 26
visible: active
focus: active
sourceComponent: DisplayWidgetsTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -150,9 +173,8 @@ FocusScope {
sourceComponent: NetworkTab {} sourceComponent: NetworkTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -166,9 +188,8 @@ FocusScope {
sourceComponent: PrinterTab {} sourceComponent: PrinterTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -182,9 +203,8 @@ FocusScope {
sourceComponent: LauncherTab {} sourceComponent: LauncherTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -198,9 +218,8 @@ FocusScope {
sourceComponent: ThemeColorsTab {} sourceComponent: ThemeColorsTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -214,9 +233,8 @@ FocusScope {
sourceComponent: LockScreenTab {} sourceComponent: LockScreenTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -232,9 +250,8 @@ FocusScope {
} }
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -248,9 +265,8 @@ FocusScope {
sourceComponent: AboutTab {} sourceComponent: AboutTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -264,9 +280,8 @@ FocusScope {
sourceComponent: TypographyMotionTab {} sourceComponent: TypographyMotionTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -280,9 +295,8 @@ FocusScope {
sourceComponent: SoundsTab {} sourceComponent: SoundsTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -296,9 +310,8 @@ FocusScope {
sourceComponent: MediaPlayerTab {} sourceComponent: MediaPlayerTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -312,9 +325,8 @@ FocusScope {
sourceComponent: NotificationsTab {} sourceComponent: NotificationsTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -328,9 +340,8 @@ FocusScope {
sourceComponent: OSDTab {} sourceComponent: OSDTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -344,9 +355,8 @@ FocusScope {
sourceComponent: RunningAppsTab {} sourceComponent: RunningAppsTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -360,9 +370,8 @@ FocusScope {
sourceComponent: SystemUpdaterTab {} sourceComponent: SystemUpdaterTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -376,9 +385,8 @@ FocusScope {
sourceComponent: PowerSleepTab {} sourceComponent: PowerSleepTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -394,9 +402,8 @@ FocusScope {
} }
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
@@ -410,9 +417,8 @@ FocusScope {
sourceComponent: ClipboardTab {} sourceComponent: ClipboardTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
} }
+1 -1
View File
@@ -58,7 +58,7 @@ FloatingWindow {
objectName: "settingsModal" objectName: "settingsModal"
title: I18n.tr("Settings", "settings window title") title: I18n.tr("Settings", "settings window title")
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 900
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940 implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
color: Theme.surfaceContainer color: Theme.surfaceContainer
visible: false visible: false
+26 -6
View File
@@ -144,6 +144,32 @@ Rectangle {
"tabIndex": 2, "tabIndex": 2,
"shortcutsOnly": true "shortcutsOnly": true
}, },
{
"id": "displays",
"text": I18n.tr("Displays"),
"icon": "monitor",
"collapsedByDefault": true,
"children": [
{
"id": "display_config",
"text": I18n.tr("Configuration") + " (Beta)",
"icon": "display_settings",
"tabIndex": 24
},
{
"id": "display_gamma",
"text": I18n.tr("Gamma Control"),
"icon": "brightness_6",
"tabIndex": 25
},
{
"id": "display_widgets",
"text": I18n.tr("Widgets", "settings_displays"),
"icon": "widgets",
"tabIndex": 26
}
]
},
{ {
"id": "network", "id": "network",
"text": I18n.tr("Network"), "text": I18n.tr("Network"),
@@ -157,12 +183,6 @@ Rectangle {
"icon": "computer", "icon": "computer",
"collapsedByDefault": true, "collapsedByDefault": true,
"children": [ "children": [
{
"id": "displays",
"text": I18n.tr("Displays"),
"icon": "monitor",
"tabIndex": 6
},
{ {
"id": "printers", "id": "printers",
"text": I18n.tr("Printers"), "text": I18n.tr("Printers"),
@@ -548,7 +548,7 @@ Item {
} }
} }
function getWorkspaceIndex(modelData) { function getWorkspaceIndex(modelData, index) {
let isPlaceholder; let isPlaceholder;
if (root.useExtWorkspace) { if (root.useExtWorkspace) {
isPlaceholder = modelData?.hidden === true; isPlaceholder = modelData?.hidden === true;
@@ -976,7 +976,7 @@ Item {
StyledText { StyledText {
id: wsIndexText id: wsIndexText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.getWorkspaceIndex(modelData) text: root.getWorkspaceIndex(modelData, index)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale) font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
@@ -1203,12 +1203,12 @@ Item {
Loader { Loader {
id: indexLoader id: indexLoader
anchors.fill: parent anchors.fill: parent
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps active: SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
sourceComponent: Item { sourceComponent: Item {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
return root.getWorkspaceIndex(modelData); return root.getWorkspaceIndex(modelData, index);
} }
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale) font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
+6 -8
View File
@@ -94,11 +94,10 @@ Item {
Timer { Timer {
id: hoverDelayTwo id: hoverDelayTwo
interval: 1000 interval: 300
repeat: false repeat: false
onTriggered: { onTriggered: {
const p = refreshButtonMouseAreaTwo.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS); refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), refreshButtonTwo, 0, 0, "left");
refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), p.x, p.y, null);
} }
} }
@@ -118,7 +117,7 @@ Item {
} }
} }
DankTooltip { DankTooltipV2 {
id: refreshButtonTooltipTwo id: refreshButtonTooltipTwo
} }
@@ -820,11 +819,10 @@ Item {
Timer { Timer {
id: hoverDelay id: hoverDelay
interval: 1000 interval: 300
repeat: false repeat: false
onTriggered: { onTriggered: {
const p = refreshButtonMouseArea.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS); refreshButtonTooltip.show(I18n.tr("Refresh Weather"), refreshButton, 0, 0, "left");
refreshButtonTooltip.show(I18n.tr("Refresh Weather"), p.x, p.y, null);
} }
} }
@@ -844,7 +842,7 @@ Item {
} }
} }
DankTooltip { DankTooltipV2 {
id: refreshButtonTooltip id: refreshButtonTooltip
} }
+132 -98
View File
@@ -19,9 +19,10 @@ Item {
property bool longPressing: false property bool longPressing: false
property bool dragging: false property bool dragging: false
property point dragStartPos: Qt.point(0, 0) property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0) property real dragAxisOffset: 0
property int targetIndex: -1 property int targetIndex: -1
property int originalIndex: -1 property int originalIndex: -1
property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
property bool showWindowTitle: false property bool showWindowTitle: false
property string windowTitle: "" property string windowTitle: ""
property bool isHovered: mouseArea.containsMouse && !dragging property bool isHovered: mouseArea.containsMouse && !dragging
@@ -95,7 +96,7 @@ Item {
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []; return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
} }
onIsHoveredChanged: { onIsHoveredChanged: {
if (mouseArea.pressed) if (mouseArea.pressed || dragging)
return; return;
if (isHovered) { if (isHovered) {
exitAnimation.stop(); exitAnimation.stop();
@@ -128,8 +129,8 @@ Item {
running: false running: false
NumberAnimation { NumberAnimation {
target: iconTransform target: root
property: animateX ? "x" : "y" property: "hoverAnimOffset"
to: animationDirection * animationDistance * 0.25 to: animationDirection * animationDistance * 0.25
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -137,8 +138,8 @@ Item {
} }
NumberAnimation { NumberAnimation {
target: iconTransform target: root
property: animateX ? "x" : "y" property: "hoverAnimOffset"
to: animationDirection * animationDistance * 0.2 to: animationDirection * animationDistance * 0.2
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -150,8 +151,8 @@ Item {
id: exitAnimation id: exitAnimation
running: false running: false
target: iconTransform target: root
property: animateX ? "x" : "y" property: "hoverAnimOffset"
to: 0 to: 0
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -187,16 +188,79 @@ Item {
} }
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex);
}
longPressing = false; const wasDragging = dragging;
dragging = false; const didReorder = wasDragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps;
dragOffset = Qt.point(0, 0);
targetIndex = -1; if (didReorder)
originalIndex = -1; dockApps.movePinnedApp(originalIndex, targetIndex);
longPressing = false;
dragging = false;
dragAxisOffset = 0;
targetIndex = -1;
originalIndex = -1;
if (dockApps && !didReorder) {
dockApps.draggedIndex = -1;
dockApps.dropTargetIndex = -1;
}
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
handleLeftClick();
}
function handleLeftClick() {
if (!appData)
return;
switch (appData.type) {
case "pinned":
if (!appData.appId)
return;
const pinnedEntry = cachedDesktopEntry;
if (pinnedEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": pinnedEntry.name || appData.appId,
"icon": pinnedEntry.icon || "",
"exec": pinnedEntry.exec || "",
"comment": pinnedEntry.comment || ""
});
}
SessionService.launchDesktopEntry(pinnedEntry);
break;
case "window":
const windowToplevel = getToplevelObject();
if (windowToplevel)
windowToplevel.activate();
break;
case "grouped":
if (appData.windowCount === 0) {
if (!appData.appId)
return;
const groupedEntry = cachedDesktopEntry;
if (groupedEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": groupedEntry.name || appData.appId,
"icon": groupedEntry.icon || "",
"exec": groupedEntry.exec || "",
"comment": groupedEntry.comment || ""
});
}
SessionService.launchDesktopEntry(groupedEntry);
} else if (appData.windowCount === 1) {
const groupedToplevel = getToplevelObject();
if (groupedToplevel)
groupedToplevel.activate();
} else if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen);
}
break;
} }
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
@@ -206,90 +270,47 @@ Item {
dragging = true; dragging = true;
targetIndex = index; targetIndex = index;
originalIndex = index; originalIndex = index;
if (dockApps) {
dockApps.draggedIndex = index;
dockApps.dropTargetIndex = index;
}
} }
} }
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y); if (!dragging || !dockApps)
if (dockApps) { return;
const threshold = actualIconSize;
let newTargetIndex = targetIndex; const axisOffset = isVertical ? (mouse.y - dragStartPos.y) : (mouse.x - dragStartPos.x);
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) { dragAxisOffset = axisOffset;
newTargetIndex = targetIndex + 1;
} else if (dragOffset.x < -threshold && targetIndex > 0) { const spacing = Math.min(8, Math.max(4, actualIconSize * 0.08));
newTargetIndex = targetIndex - 1; const itemSize = actualIconSize * 1.2 + spacing;
} const slotOffset = Math.round(axisOffset / itemSize);
if (newTargetIndex !== targetIndex) { const newTargetIndex = Math.max(0, Math.min(dockApps.pinnedAppCount - 1, originalIndex + slotOffset));
targetIndex = newTargetIndex;
dragStartPos = Qt.point(mouse.x, mouse.y); if (newTargetIndex !== targetIndex) {
} targetIndex = newTargetIndex;
} dockApps.dropTargetIndex = newTargetIndex;
} }
} }
onClicked: mouse => { onClicked: mouse => {
if (!appData || longPressing) { if (!appData)
return; return;
}
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.MiddleButton) {
if (appData.type === "pinned") { switch (appData.type) {
if (appData && appData.appId) { case "window":
const desktopEntry = cachedDesktopEntry; appData.toplevel?.close();
if (desktopEntry) { break;
AppUsageHistoryData.addAppUsage({ case "grouped":
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
}
SessionService.launchDesktopEntry(desktopEntry);
}
} else if (appData.type === "window") {
const toplevel = getToplevelObject();
if (toplevel) {
toplevel.activate();
}
} else if (appData.type === "grouped") {
if (appData.windowCount === 0) {
if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry;
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
}
SessionService.launchDesktopEntry(desktopEntry);
}
} else if (appData.windowCount === 1) {
// For single window, activate directly
const toplevel = getToplevelObject();
if (toplevel) {
console.log("Activating grouped app window:", appData.windowTitle);
toplevel.activate();
} else {
console.warn("No toplevel found for grouped app");
}
} else {
if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen);
}
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData?.type === "window") {
appData?.toplevel?.close();
} else if (appData?.type === "grouped") {
if (contextMenu) { if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell"; const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen); contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen);
} }
} else if (appData && appData.appId) { break;
default:
if (!appData.appId)
return;
const desktopEntry = cachedDesktopEntry; const desktopEntry = cachedDesktopEntry;
if (desktopEntry) { if (desktopEntry) {
AppUsageHistoryData.addAppUsage({ AppUsageHistoryData.addAppUsage({
@@ -301,26 +322,39 @@ Item {
}); });
} }
SessionService.launchDesktopEntry(desktopEntry); SessionService.launchDesktopEntry(desktopEntry);
break;
} }
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
if (contextMenu && appData) { if (!contextMenu)
const shouldHidePin = appData.appId === "org.quickshell"; return;
contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen); const shouldHidePin = appData.appId === "org.quickshell";
} else { contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen);
console.warn("No context menu or appData available");
}
} }
} }
} }
property real hoverAnimOffset: 0
Item { Item {
id: visualContent id: visualContent
anchors.fill: parent anchors.fill: parent
transform: Translate { transform: Translate {
id: iconTransform id: iconTransform
x: 0 x: {
y: 0 if (dragging && !isVertical)
return dragAxisOffset;
if (!dragging && isVertical)
return hoverAnimOffset;
return 0;
}
y: {
if (dragging && isVertical)
return dragAxisOffset;
if (!dragging && !isVertical)
return hoverAnimOffset;
return 0;
}
} }
Rectangle { Rectangle {
+211 -163
View File
@@ -1,11 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -17,6 +14,9 @@ Item {
property bool isVertical: false property bool isVertical: false
property var dockScreen: null property var dockScreen: null
property real iconSize: 40 property real iconSize: 40
property int draggedIndex: -1
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
clip: false clip: false
implicitWidth: isVertical ? appLayout.height : appLayout.width implicitWidth: isVertical ? appLayout.height : appLayout.width
@@ -24,18 +24,18 @@ Item {
function movePinnedApp(fromIndex, toIndex) { function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) { if (fromIndex === toIndex) {
return return;
} }
const currentPinned = [...(SessionData.pinnedApps || [])] const currentPinned = [...(SessionData.pinnedApps || [])];
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) { if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
return return;
} }
const movedApp = currentPinned.splice(fromIndex, 1)[0] const movedApp = currentPinned.splice(fromIndex, 1)[0];
currentPinned.splice(toIndex, 0, movedApp) currentPinned.splice(toIndex, 0, movedApp);
SessionData.setPinnedApps(currentPinned) SessionData.setPinnedApps(currentPinned);
} }
Item { Item {
@@ -53,202 +53,250 @@ Item {
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08)) spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
Repeater { Repeater {
id: repeater id: repeater
property var dockItems: [] property var dockItems: []
model: ScriptModel { model: ScriptModel {
values: repeater.dockItems values: repeater.dockItems
objectProp: "uniqueKey" objectProp: "uniqueKey"
} }
Component.onCompleted: updateModel() Component.onCompleted: updateModel()
function updateModel() { function updateModel() {
const items = [] const items = [];
const pinnedApps = [...(SessionData.pinnedApps || [])] const pinnedApps = [...(SessionData.pinnedApps || [])];
const sortedToplevels = CompositorService.sortedToplevels const sortedToplevels = CompositorService.sortedToplevels;
if (root.groupByApp) { if (root.groupByApp) {
const appGroups = new Map() const appGroups = new Map();
pinnedApps.forEach(rawAppId => { pinnedApps.forEach(rawAppId => {
const appId = Paths.moddedAppId(rawAppId) const appId = Paths.moddedAppId(rawAppId);
appGroups.set(appId, {
appId: appId,
isPinned: true,
windows: []
})
})
sortedToplevels.forEach((toplevel, index) => {
const rawAppId = toplevel.appId || "unknown"
const appId = Paths.moddedAppId(rawAppId)
if (!appGroups.has(appId)) {
appGroups.set(appId, { appGroups.set(appId, {
appId: appId, appId: appId,
isPinned: false, isPinned: true,
windows: [] windows: []
}) });
});
sortedToplevels.forEach((toplevel, index) => {
const rawAppId = toplevel.appId || "unknown";
const appId = Paths.moddedAppId(rawAppId);
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
isPinned: false,
windows: []
});
}
appGroups.get(appId).windows.push({
toplevel: toplevel,
index: index
});
});
const pinnedGroups = [];
const unpinnedGroups = [];
Array.from(appGroups.entries()).forEach(([appId, group]) => {
const firstWindow = group.windows.length > 0 ? group.windows[0] : null;
const item = {
uniqueKey: "grouped_" + appId,
type: "grouped",
appId: appId,
toplevel: firstWindow ? firstWindow.toplevel : null,
isPinned: group.isPinned,
isRunning: group.windows.length > 0,
windowCount: group.windows.length,
allWindows: group.windows
};
if (group.isPinned) {
pinnedGroups.push(item);
} else {
unpinnedGroups.push(item);
}
});
pinnedGroups.forEach(item => items.push(item));
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
});
} }
appGroups.get(appId).windows.push({ unpinnedGroups.forEach(item => items.push(item));
toplevel: toplevel, root.pinnedAppCount = pinnedGroups.length;
index: index } else {
}) pinnedApps.forEach(rawAppId => {
}) const appId = Paths.moddedAppId(rawAppId);
items.push({
uniqueKey: "pinned_" + appId,
type: "pinned",
appId: appId,
toplevel: null,
isPinned: true,
isRunning: false
});
});
const pinnedGroups = [] root.pinnedAppCount = pinnedApps.length;
const unpinnedGroups = []
Array.from(appGroups.entries()).forEach(([appId, group]) => { if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
const firstWindow = group.windows.length > 0 ? group.windows[0] : null items.push({
uniqueKey: "separator_ungrouped",
const item = { type: "separator",
uniqueKey: "grouped_" + appId, appId: "__SEPARATOR__",
type: "grouped", toplevel: null,
appId: appId, isPinned: false,
toplevel: firstWindow ? firstWindow.toplevel : null, isRunning: false
isPinned: group.isPinned, });
isRunning: group.windows.length > 0,
windowCount: group.windows.length,
allWindows: group.windows
} }
if (group.isPinned) { sortedToplevels.forEach((toplevel, index) => {
pinnedGroups.push(item) let uniqueKey = "window_" + index;
} else { if (CompositorService.isHyprland && Hyprland.toplevels) {
unpinnedGroups.push(item) const hyprlandToplevels = Array.from(Hyprland.toplevels.values);
} for (let i = 0; i < hyprlandToplevels.length; i++) {
}) if (hyprlandToplevels[i].wayland === toplevel) {
uniqueKey = "window_" + hyprlandToplevels[i].address;
pinnedGroups.forEach(item => items.push(item)) break;
}
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
})
}
unpinnedGroups.forEach(item => items.push(item))
root.pinnedAppCount = pinnedGroups.length
} else {
pinnedApps.forEach(rawAppId => {
const appId = Paths.moddedAppId(rawAppId)
items.push({
uniqueKey: "pinned_" + appId,
type: "pinned",
appId: appId,
toplevel: null,
isPinned: true,
isRunning: false
})
})
root.pinnedAppCount = pinnedApps.length
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
items.push({
uniqueKey: "separator_ungrouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
})
}
sortedToplevels.forEach((toplevel, index) => {
let uniqueKey = "window_" + index
if (CompositorService.isHyprland && Hyprland.toplevels) {
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
for (let i = 0; i < hyprlandToplevels.length; i++) {
if (hyprlandToplevels[i].wayland === toplevel) {
uniqueKey = "window_" + hyprlandToplevels[i].address
break
} }
} }
items.push({
uniqueKey: uniqueKey,
type: "window",
appId: Paths.moddedAppId(toplevel.appId),
toplevel: toplevel,
isPinned: false,
isRunning: true
});
});
}
dockItems = items;
}
delegate: Item {
id: delegateItem
property alias dockButton: button
property var itemData: modelData
clip: false
z: button.dragging ? 100 : 0
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
property real shiftOffset: {
if (root.draggedIndex < 0 || !itemData.isPinned || itemData.type === "separator")
return 0;
if (model.index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const myIdx = model.index;
const shiftAmount = root.iconSize * 1.2 + layoutFlow.spacing;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
x: root.isVertical ? 0 : delegateItem.shiftOffset
y: root.isVertical ? delegateItem.shiftOffset : 0
Behavior on x {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
} }
items.push({ Behavior on y {
uniqueKey: uniqueKey, enabled: !root.suppressShiftAnimation
type: "window", NumberAnimation {
appId: Paths.moddedAppId(toplevel.appId), duration: 150
toplevel: toplevel, easing.type: Easing.OutCubic
isPinned: false, }
isRunning: true }
}) }
})
}
dockItems = items Rectangle {
} visible: itemData.type === "separator"
width: root.isVertical ? root.iconSize * 0.5 : 2
height: root.isVertical ? 2 : root.iconSize * 0.5
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
delegate: Item { DockAppButton {
id: delegateItem id: button
property alias dockButton: button visible: itemData.type !== "separator"
property var itemData: modelData anchors.centerIn: parent
clip: false
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2) width: delegateItem.width
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize) height: delegateItem.height
actualIconSize: root.iconSize
Rectangle { appData: itemData
visible: itemData.type === "separator" contextMenu: root.contextMenu
width: root.isVertical ? root.iconSize * 0.5 : 2 dockApps: root
height: root.isVertical ? 2 : root.iconSize * 0.5 index: model.index
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) parentDockScreen: root.dockScreen
radius: 1
anchors.centerIn: parent
}
DockAppButton { showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
id: button windowTitle: {
visible: itemData.type !== "separator" const title = itemData?.toplevel?.title || "(Unnamed)";
anchors.centerIn: parent return title.length > 50 ? title.substring(0, 47) + "..." : title;
}
width: delegateItem.width
height: delegateItem.height
actualIconSize: root.iconSize
appData: itemData
contextMenu: root.contextMenu
dockApps: root
index: model.index
parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: {
const title = itemData?.toplevel?.title || "(Unnamed)"
return title.length > 50 ? title.substring(0, 47) + "..." : title
} }
} }
} }
} }
}
} }
Connections { Connections {
target: CompositorService target: CompositorService
function onToplevelsChanged() { function onToplevelsChanged() {
repeater.updateModel() repeater.updateModel();
} }
} }
Connections { Connections {
target: SessionData target: SessionData
function onPinnedAppsChanged() { function onPinnedAppsChanged() {
repeater.updateModel() root.suppressShiftAnimation = true;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
repeater.updateModel();
Qt.callLater(() => {
root.suppressShiftAnimation = false;
});
} }
} }
onGroupByAppChanged: { onGroupByAppChanged: {
repeater.updateModel() repeater.updateModel();
} }
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,275 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string outputName: ""
property var outputData: null
property bool expanded: false
width: parent.width
spacing: 0
Rectangle {
width: parent.width
height: headerRow.implicitHeight + Theme.spacingS * 2
color: headerMouse.containsMouse ? Theme.withAlpha(Theme.primary, 0.1) : "transparent"
radius: Theme.cornerRadius / 2
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.expanded ? "expand_more" : "chevron_right"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Compositor Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: headerMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
}
}
Column {
id: settingsColumn
width: parent.width
spacing: Theme.spacingS
visible: root.expanded
topPadding: Theme.spacingS
property int currentBitdepth: {
DisplayConfigState.pendingHyprlandChanges;
return DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "bitdepth", 8);
}
property bool is10Bit: currentBitdepth === 10
property string currentCm: {
DisplayConfigState.pendingHyprlandChanges;
return DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto");
}
property bool isHdrMode: currentCm === "hdr" || currentCm === "hdredid"
DankToggle {
width: parent.width
text: I18n.tr("10-bit Color")
description: I18n.tr("Enable 10-bit color depth for wider color gamut and HDR support")
checked: settingsColumn.is10Bit
onToggled: checked => {
if (checked) {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "bitdepth", 10);
} else {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "bitdepth", null);
if (settingsColumn.isHdrMode)
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto");
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: settingsColumn.is10Bit
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
DankDropdown {
width: parent.width
text: I18n.tr("Color Gamut")
addHorizontalPadding: true
currentValue: {
DisplayConfigState.pendingHyprlandChanges;
const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto");
return cmLabelMap[val] || I18n.tr("Auto (Wide)");
}
options: [I18n.tr("Auto (Wide)"), I18n.tr("Wide (BT2020)"), "DCI-P3", "Apple P3", "Adobe RGB", "EDID", "HDR", I18n.tr("HDR (EDID)")]
property var cmValueMap: ({
[I18n.tr("Auto (Wide)")]: "auto",
[I18n.tr("Wide (BT2020)")]: "wide",
"DCI-P3": "dcip3",
"Apple P3": "dp3",
"Adobe RGB": "adobe",
"EDID": "edid",
"HDR": "hdr",
[I18n.tr("HDR (EDID)")]: "hdredid"
})
property var cmLabelMap: ({
"auto": I18n.tr("Auto (Wide)"),
"wide": I18n.tr("Wide (BT2020)"),
"dcip3": "DCI-P3",
"dp3": "Apple P3",
"adobe": "Adobe RGB",
"edid": "EDID",
"hdr": "HDR",
"hdredid": I18n.tr("HDR (EDID)")
})
onValueChanged: value => {
const cmValue = cmValueMap[value] || "auto";
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "colorManagement", cmValue);
}
}
Rectangle {
width: parent.width - Theme.spacingM * 2
anchors.horizontalCenter: parent.horizontalCenter
height: warningColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius / 2
color: Theme.withAlpha(Theme.warning, 0.15)
border.color: Theme.withAlpha(Theme.warning, 0.3)
border.width: 1
visible: settingsColumn.isHdrMode
Column {
id: warningColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: Theme.iconSize - 4
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Experimental Feature")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("HDR mode is experimental. Verify your monitor supports HDR before enabling.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: settingsColumn.isHdrMode
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
StyledText {
text: I18n.tr("HDR Tone Mapping")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
leftPadding: Theme.spacingM
}
Row {
width: parent.width - Theme.spacingM * 2
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("SDR Brightness")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: "1.0 - 2.0"
text: {
DisplayConfigState.pendingHyprlandChanges;
const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", null);
return val !== null ? val.toString() : "";
}
onEditingFinished: {
const trimmed = text.trim();
if (!trimmed) {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", null);
return;
}
const val = parseFloat(trimmed);
if (isNaN(val) || val < 0.1 || val > 5.0)
return;
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", parseFloat(val.toFixed(2)));
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("SDR Saturation")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: "0.5 - 1.5"
text: {
DisplayConfigState.pendingHyprlandChanges;
const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", null);
return val !== null ? val.toString() : "";
}
onEditingFinished: {
const trimmed = text.trim();
if (!trimmed) {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", null);
return;
}
const val = parseFloat(trimmed);
if (isNaN(val) || val < 0.0 || val > 3.0)
return;
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", parseFloat(val.toFixed(2)));
}
}
}
}
}
}
}
}
@@ -0,0 +1,88 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
width: parent.width
height: warningContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
readonly property bool showError: DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included
readonly property bool showSetup: !DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included
color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent"
border.color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
border.width: 1
visible: (showError || showSetup) && DisplayConfigState.hasOutputBackend && !DisplayConfigState.checkingInclude
Column {
id: warningContent
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: {
if (root.showSetup)
return I18n.tr("First Time Setup");
if (root.showError)
return I18n.tr("Outputs Include Missing");
return "";
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
}
StyledText {
text: {
if (root.showSetup)
return I18n.tr("Click 'Setup' to create the outputs config and add include to your compositor config.");
if (root.showError)
return I18n.tr("dms/outputs config exists but is not included in your compositor config. Display changes won't persist.");
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankButton {
id: fixButton
visible: root.showError || root.showSetup
text: {
if (DisplayConfigState.fixingInclude)
return I18n.tr("Fixing...");
if (root.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !DisplayConfigState.fixingInclude
anchors.verticalCenter: parent.verticalCenter
onClicked: DisplayConfigState.fixOutputsInclude()
}
}
}
}
@@ -0,0 +1,49 @@
import QtQuick
import qs.Common
Rectangle {
id: root
width: parent.width
height: 280
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.color: Theme.outline
border.width: 1
Item {
id: canvas
anchors.fill: parent
anchors.margins: Theme.spacingL
property var bounds: DisplayConfigState.getOutputBounds()
property real scaleFactor: {
if (bounds.width === 0 || bounds.height === 0)
return 0.1;
const padding = Theme.spacingL * 2;
const scaleX = (width - padding) / bounds.width;
const scaleY = (height - padding) / bounds.height;
return Math.min(scaleX, scaleY);
}
property point offset: Qt.point((width - bounds.width * scaleFactor) / 2 - bounds.minX * scaleFactor, (height - bounds.height * scaleFactor) / 2 - bounds.minY * scaleFactor)
Connections {
target: DisplayConfigState
function onAllOutputsChanged() {
canvas.bounds = DisplayConfigState.getOutputBounds();
}
}
Repeater {
model: DisplayConfigState.allOutputs ? Object.keys(DisplayConfigState.allOutputs) : []
delegate: MonitorRect {
required property string modelData
outputName: modelData
outputData: DisplayConfigState.allOutputs[modelData]
canvasScaleFactor: canvas.scaleFactor
canvasOffset: canvas.offset
}
}
}
}
@@ -0,0 +1,158 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
required property string outputName
required property var outputData
required property real canvasScaleFactor
required property point canvasOffset
property bool isConnected: outputData?.connected ?? false
property bool isDragging: false
property point originalLogical: Qt.point(0, 0)
property point snappedLogical: Qt.point(0, 0)
property bool isValidPosition: true
property var physSize: DisplayConfigState.getPhysicalSize(outputData)
property var logicalSize: DisplayConfigState.getLogicalSize(outputData)
x: isDragging ? x : (outputData?.logical?.x ?? 0) * canvasScaleFactor + canvasOffset.x
y: isDragging ? y : (outputData?.logical?.y ?? 0) * canvasScaleFactor + canvasOffset.y
width: logicalSize.w * canvasScaleFactor
height: logicalSize.h * canvasScaleFactor
radius: Theme.cornerRadius
opacity: isConnected ? 1.0 : 0.5
color: {
if (!isConnected)
return Theme.surfaceContainerHighest;
if (!isValidPosition)
return Theme.withAlpha(Theme.error, 0.3);
if (isDragging)
return Theme.withAlpha(Theme.primary, 0.4);
if (dragArea.containsMouse)
return Theme.withAlpha(Theme.primary, 0.2);
return Theme.surfaceContainerHigh;
}
border.color: {
if (!isConnected)
return Theme.outline;
if (!isValidPosition)
return Theme.error;
if (isDragging)
return Theme.primary;
if (CompositorService.getFocusedScreen()?.name === outputName)
return Theme.primary;
return Theme.outline;
}
border.width: isDragging ? 3 : 2
z: isDragging ? 100 : (isConnected ? 1 : 0)
Rectangle {
id: snapPreview
visible: root.isDragging && root.isValidPosition
x: root.snappedLogical.x * root.canvasScaleFactor + root.canvasOffset.x - root.x
y: root.snappedLogical.y * root.canvasScaleFactor + root.canvasOffset.y - root.y
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: "transparent"
border.color: Theme.primary
border.width: 2
opacity: 0.6
}
Column {
anchors.centerIn: parent
spacing: 2
DankIcon {
name: root.isConnected ? "desktop_windows" : "desktop_access_disabled"
size: Math.min(24, Math.min(root.width * 0.3, root.height * 0.25))
color: root.isConnected ? (root.isValidPosition ? Theme.primary : Theme.error) : Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: DisplayConfigState.getOutputDisplayName(root.outputData, root.outputName)
font.pixelSize: Math.max(10, Math.min(14, root.width * 0.12))
font.weight: Font.Medium
color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
elide: Text.ElideMiddle
width: Math.min(implicitWidth, root.width - 8)
}
StyledText {
text: root.isConnected ? (root.physSize.w + "x" + root.physSize.h) : I18n.tr("Disconnected")
font.pixelSize: Math.max(8, Math.min(11, root.width * 0.09))
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
enabled: root.isConnected
cursorShape: !root.isConnected ? Qt.ArrowCursor : (root.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor)
drag.target: root.isConnected ? root : null
drag.axis: Drag.XAndYAxis
drag.threshold: 0
onPressed: mouse => {
if (!root.isConnected)
return;
root.isDragging = true;
root.originalLogical = Qt.point(root.outputData?.logical?.x ?? 0, root.outputData?.logical?.y ?? 0);
root.snappedLogical = root.originalLogical;
root.isValidPosition = true;
}
onPositionChanged: mouse => {
if (!root.isDragging || !root.isConnected)
return;
let posX = Math.round((root.x - root.canvasOffset.x) / root.canvasScaleFactor);
let posY = Math.round((root.y - root.canvasOffset.y) / root.canvasScaleFactor);
const size = DisplayConfigState.getLogicalSize(root.outputData);
const snapped = DisplayConfigState.snapToEdges(root.outputName, posX, posY, size.w, size.h);
root.snappedLogical = snapped;
root.isValidPosition = !DisplayConfigState.checkOverlap(root.outputName, snapped.x, snapped.y, size.w, size.h);
}
onReleased: {
if (!root.isDragging || !root.isConnected)
return;
root.isDragging = false;
const size = DisplayConfigState.getLogicalSize(root.outputData);
const finalX = root.snappedLogical.x;
const finalY = root.snappedLogical.y;
if (DisplayConfigState.checkOverlap(root.outputName, finalX, finalY, size.w, size.h)) {
root.isValidPosition = true;
return;
}
if (finalX === root.originalLogical.x && finalY === root.originalLogical.y)
return;
DisplayConfigState.initOriginalOutputs();
DisplayConfigState.backendUpdateOutputPosition(root.outputName, finalX, finalY);
DisplayConfigState.setPendingChange(root.outputName, "position", {
"x": finalX,
"y": finalY
});
}
}
Drag.active: dragArea.drag.active && root.isConnected
}
@@ -0,0 +1,368 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string outputName: ""
property var outputData: null
property bool expanded: false
width: parent.width
spacing: 0
Rectangle {
width: parent.width
height: headerRow.implicitHeight + Theme.spacingS * 2
color: headerMouse.containsMouse ? Theme.withAlpha(Theme.primary, 0.1) : "transparent"
radius: Theme.cornerRadius / 2
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.expanded ? "expand_more" : "chevron_right"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Compositor Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: headerMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: root.expanded
topPadding: Theme.spacingS
DankToggle {
width: parent.width
text: I18n.tr("Disable Output")
checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "disabled", false)
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "disabled", checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Focus at Startup")
checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "focusAtStartup", false)
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "focusAtStartup", checked)
}
DankDropdown {
width: parent.width
text: I18n.tr("Hot Corners")
addHorizontalPadding: true
property var hotCornersData: {
void (DisplayConfigState.pendingNiriChanges);
return DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "hotCorners", null);
}
currentValue: {
if (!hotCornersData)
return I18n.tr("Inherit");
if (hotCornersData.off)
return I18n.tr("Off");
const corners = hotCornersData.corners || [];
if (corners.length === 0)
return I18n.tr("Inherit");
if (corners.length === 4)
return I18n.tr("All");
return I18n.tr("Select...");
}
options: [I18n.tr("Inherit"), I18n.tr("Off"), I18n.tr("All"), I18n.tr("Select...")]
onValueChanged: value => {
switch (value) {
case I18n.tr("Inherit"):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", null);
break;
case I18n.tr("Off"):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"off": true
});
break;
case I18n.tr("All"):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"corners": ["top-left", "top-right", "bottom-left", "bottom-right"]
});
break;
case I18n.tr("Select..."):
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"corners": []
});
break;
}
}
}
Item {
width: parent.width
height: hotCornersGroup.implicitHeight
clip: true
property var hotCornersData: {
void (DisplayConfigState.pendingNiriChanges);
return DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "hotCorners", null);
}
visible: hotCornersData && !hotCornersData.off && hotCornersData.corners !== undefined
DankButtonGroup {
id: hotCornersGroup
anchors.horizontalCenter: parent.horizontalCenter
selectionMode: "multi"
checkEnabled: false
buttonHeight: 32
buttonPadding: parent.width < 400 ? Theme.spacingXS : Theme.spacingM
minButtonWidth: parent.width < 400 ? 28 : 56
textSize: parent.width < 400 ? 11 : Theme.fontSizeMedium
model: [I18n.tr("Top Left"), I18n.tr("Top Right"), I18n.tr("Bottom Left"), I18n.tr("Bottom Right")]
property var cornerKeys: ["top-left", "top-right", "bottom-left", "bottom-right"]
currentSelection: {
const hcData = parent.hotCornersData;
if (!hcData?.corners)
return [];
return hcData.corners.map(key => {
const idx = cornerKeys.indexOf(key);
return idx >= 0 ? model[idx] : null;
}).filter(v => v !== null);
}
onSelectionChanged: (index, selected) => {
const corners = currentSelection.map(label => {
const idx = model.indexOf(label);
return idx >= 0 ? cornerKeys[idx] : null;
}).filter(v => v !== null);
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", {
"corners": corners
});
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
Item {
width: parent.width
height: layoutColumn.implicitHeight
Column {
id: layoutColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Layout Overrides")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Override global layout settings for this output")
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceVariantText, 0.7)
wrapMode: Text.WordWrap
width: parent.width
}
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Window Gaps (px)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: I18n.tr("Inherit")
text: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
if (layout?.gaps === undefined)
return "";
return layout.gaps.toString();
}
onEditingFinished: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
const trimmed = text.trim();
if (!trimmed) {
delete layout.gaps;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
const val = parseInt(trimmed);
if (isNaN(val) || val < 0)
return;
layout.gaps = val;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout);
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Default Width (%)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: I18n.tr("Inherit")
text: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
if (!layout?.defaultColumnWidth)
return "";
if (layout.defaultColumnWidth.type !== "proportion")
return "";
const percent = layout.defaultColumnWidth.value * 100;
return parseFloat(percent.toFixed(4)).toString();
}
onEditingFinished: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
const trimmed = text.trim().replace("%", "");
if (!trimmed) {
delete layout.defaultColumnWidth;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
const val = parseFloat(trimmed);
if (isNaN(val) || val <= 0 || val > 100)
return;
layout.defaultColumnWidth = {
"type": "proportion",
"value": parseFloat((val / 100).toFixed(6))
};
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Preset Widths (%)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "e.g. 33.33, 50, 66.67"
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceVariantText, 0.7)
}
DankTextField {
width: parent.width
height: 40
placeholderText: I18n.tr("Inherit")
text: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
const presets = layout?.presetColumnWidths || [];
if (presets.length === 0)
return "";
return presets.filter(p => p.type === "proportion").map(p => parseFloat((p.value * 100).toFixed(4))).join(", ");
}
onEditingFinished: {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
const trimmed = text.trim();
if (!trimmed) {
delete layout.presetColumnWidths;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
const parts = trimmed.split(/[,\s]+/).filter(s => s);
const presets = [];
for (const part of parts) {
const val = parseFloat(part.replace("%", ""));
if (!isNaN(val) && val > 0 && val <= 100)
presets.push({
"type": "proportion",
"value": parseFloat((val / 100).toFixed(6))
});
}
if (presets.length === 0) {
delete layout.presetColumnWidths;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
return;
}
presets.sort((a, b) => a.value - b.value);
layout.presetColumnWidths = presets;
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout);
}
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Center Single Column")
property var layoutData: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null)
checked: layoutData?.alwaysCenterSingleColumn ?? false
onToggled: checked => {
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {};
if (checked) {
layout.alwaysCenterSingleColumn = true;
} else {
delete layout.alwaysCenterSingleColumn;
}
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null);
}
}
}
}
@@ -0,0 +1,54 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
width: parent.width
height: messageContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: messageContent
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Monitor Configuration")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display configuration is not available. WLR output management protocol not supported.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
@@ -0,0 +1,278 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
StyledRect {
id: root
required property string outputName
required property var outputData
property bool isConnected: outputData?.connected ?? false
width: parent.width
height: settingsColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, isConnected ? 0.5 : 0.3)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
opacity: isConnected ? 1.0 : 0.7
Column {
id: settingsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: root.isConnected ? "desktop_windows" : "desktop_access_disabled"
size: Theme.iconSize - 4
color: root.isConnected ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - (disconnectedBadge.visible ? disconnectedBadge.width + Theme.spacingS : 0)
spacing: 2
StyledText {
text: DisplayConfigState.getOutputDisplayName(root.outputData, root.outputName)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText
}
StyledText {
text: (root.outputData?.model ?? "") + (root.outputData?.make ? " - " + root.outputData.make : "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Rectangle {
id: disconnectedBadge
visible: !root.isConnected
width: disconnectedText.implicitWidth + Theme.spacingM
height: disconnectedText.implicitHeight + Theme.spacingXS
radius: height / 2
color: Theme.withAlpha(Theme.outline, 0.3)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: disconnectedText
text: I18n.tr("Disconnected")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.centerIn: parent
}
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Resolution & Refresh")
visible: root.isConnected
currentValue: {
const pendingMode = DisplayConfigState.getPendingValue(root.outputName, "mode");
if (pendingMode)
return pendingMode;
const data = DisplayConfigState.outputs[root.outputName];
if (!data?.modes || data?.current_mode === undefined)
return "Auto";
const mode = data.modes[data.current_mode];
return mode ? DisplayConfigState.formatMode(mode) : "Auto";
}
options: {
const data = DisplayConfigState.outputs[root.outputName];
if (!data?.modes)
return ["Auto"];
const opts = [];
for (var i = 0; i < data.modes.length; i++) {
opts.push(DisplayConfigState.formatMode(data.modes[i]));
}
return opts;
}
onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "mode", value)
}
StyledText {
visible: !root.isConnected
text: I18n.tr("Configuration will be preserved when this display reconnects")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: root.isConnected
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Scale")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Item {
id: scaleContainer
width: parent.width
height: scaleDropdown.visible ? scaleDropdown.height : scaleInput.height
property bool customMode: false
property string currentScale: {
const pendingScale = DisplayConfigState.getPendingValue(root.outputName, "scale");
if (pendingScale !== undefined)
return parseFloat(pendingScale.toFixed(2)).toString();
const scale = DisplayConfigState.outputs[root.outputName]?.logical?.scale ?? 1.0;
return parseFloat(scale.toFixed(2)).toString();
}
DankDropdown {
id: scaleDropdown
width: parent.width
dropdownWidth: parent.width
visible: !scaleContainer.customMode
currentValue: scaleContainer.currentScale
options: {
const standard = ["0.5", "0.75", "1", "1.25", "1.5", "1.75", "2", "2.5", "3", I18n.tr("Custom...")];
const current = scaleContainer.currentScale;
if (standard.slice(0, -1).includes(current))
return standard;
const opts = [...standard.slice(0, -1), current, standard[standard.length - 1]];
return opts.sort((a, b) => {
if (a === I18n.tr("Custom..."))
return 1;
if (b === I18n.tr("Custom..."))
return -1;
return parseFloat(a) - parseFloat(b);
});
}
onValueChanged: value => {
if (value === I18n.tr("Custom...")) {
scaleContainer.customMode = true;
scaleInput.text = scaleContainer.currentScale;
scaleInput.forceActiveFocus();
scaleInput.selectAll();
return;
}
DisplayConfigState.setPendingChange(root.outputName, "scale", parseFloat(value));
}
}
DankTextField {
id: scaleInput
width: parent.width
height: 40
visible: scaleContainer.customMode
placeholderText: "0.5 - 4.0"
function applyValue() {
const val = parseFloat(text);
if (isNaN(val) || val < 0.25 || val > 4) {
text = scaleContainer.currentScale;
scaleContainer.customMode = false;
return;
}
DisplayConfigState.setPendingChange(root.outputName, "scale", parseFloat(val.toFixed(2)));
scaleContainer.customMode = false;
}
onAccepted: applyValue()
onEditingFinished: applyValue()
Keys.onEscapePressed: {
text = scaleContainer.currentScale;
scaleContainer.customMode = false;
}
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Transform")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankDropdown {
width: parent.width
dropdownWidth: parent.width
currentValue: {
const pendingTransform = DisplayConfigState.getPendingValue(root.outputName, "transform");
if (pendingTransform)
return DisplayConfigState.getTransformLabel(pendingTransform);
const data = DisplayConfigState.outputs[root.outputName];
return DisplayConfigState.getTransformLabel(data?.logical?.transform ?? "Normal");
}
options: [I18n.tr("Normal"), I18n.tr("90°"), I18n.tr("180°"), I18n.tr("270°"), I18n.tr("Flipped"), I18n.tr("Flipped 90°"), I18n.tr("Flipped 180°"), I18n.tr("Flipped 270°")]
onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "transform", DisplayConfigState.getTransformValue(value))
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Variable Refresh Rate")
visible: root.isConnected && !CompositorService.isDwl && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
checked: {
const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
if (pendingVrr !== undefined)
return pendingVrr;
return DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false;
}
onToggled: checked => DisplayConfigState.setPendingChange(root.outputName, "vrr", checked)
}
DankToggle {
width: parent.width
text: I18n.tr("VRR On-Demand")
description: I18n.tr("VRR activates only when applications request it")
visible: root.isConnected && CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "vrrOnDemand", false)
onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "vrrOnDemand", checked)
}
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.2)
visible: compositorSettingsLoader.active
}
Loader {
id: compositorSettingsLoader
width: parent.width
active: root.isConnected && compositorSettingsSource !== ""
source: compositorSettingsSource
property string compositorSettingsSource: {
switch (CompositorService.compositor) {
case "niri":
return "NiriOutputSettings.qml";
case "hyprland":
return "HyprlandOutputSettings.qml";
default:
return "";
}
}
onLoaded: {
item.outputName = root.outputName;
item.outputData = root.outputData;
}
}
}
}
@@ -0,0 +1,175 @@
import QtQuick
import qs.Common
import qs.Modals
import qs.Services
import qs.Widgets
import qs.Modules.Settings.DisplayConfig
Item {
id: root
Connections {
target: DisplayConfigState
function onChangesApplied(changeDescriptions) {
confirmationModal.changes = changeDescriptions;
confirmationModal.open();
}
function onChangesConfirmed() {
}
function onChangesReverted() {
}
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
IncludeWarningBox {
width: parent.width
}
StyledRect {
width: parent.width
height: monitorConfigSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
visible: DisplayConfigState.hasOutputBackend
Column {
id: monitorConfigSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - (displayFormatColumn.visible ? displayFormatColumn.width + Theme.spacingM : 0)
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Monitor Configuration")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Arrange displays and configure resolution, refresh rate, and VRR")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Column {
id: displayFormatColumn
visible: !CompositorService.isDwl
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Config Format")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
DankButtonGroup {
id: displayFormatGroup
model: [I18n.tr("Name"), I18n.tr("Model")]
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const newMode = index === 1 ? "model" : "system";
DisplayConfigState.setOriginalDisplayNameMode(SettingsData.displayNameMode);
SettingsData.displayNameMode = newMode;
}
Connections {
target: SettingsData
function onDisplayNameModeChanged() {
displayFormatGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0;
}
}
}
}
}
MonitorCanvas {
width: parent.width
}
Column {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: DisplayConfigState.allOutputs ? Object.keys(DisplayConfigState.allOutputs) : []
delegate: OutputCard {
required property string modelData
outputName: modelData
outputData: DisplayConfigState.allOutputs[modelData]
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: DisplayConfigState.hasPendingChanges
layoutDirection: Qt.RightToLeft
DankButton {
text: I18n.tr("Apply Changes")
iconName: "check"
onClicked: DisplayConfigState.applyChanges()
}
DankButton {
text: I18n.tr("Discard")
backgroundColor: "transparent"
textColor: Theme.surfaceText
onClicked: DisplayConfigState.discardChanges()
}
}
}
}
NoBackendMessage {
width: parent.width
visible: !DisplayConfigState.hasOutputBackend
}
}
}
DisplayConfirmationModal {
id: confirmationModal
onConfirmed: DisplayConfigState.confirmChanges()
onReverted: DisplayConfigState.revertChanges()
}
}
@@ -0,0 +1,497 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
function getBarComponentsFromSettings() {
const bars = SettingsData.barConfigs || [];
return bars.map(bar => ({
"id": "bar:" + bar.id,
"name": bar.name || "Bar",
"description": I18n.tr("Individual bar configuration"),
"icon": "toolbar",
"barId": bar.id
}));
}
property var variantComponents: getVariantComponentsList()
function getVariantComponentsList() {
return [...getBarComponentsFromSettings(),
{
"id": "dock",
"name": I18n.tr("Application Dock"),
"description": I18n.tr("Bottom dock for pinned and running applications"),
"icon": "dock"
},
{
"id": "notifications",
"name": I18n.tr("Notification Popups"),
"description": I18n.tr("Notification toast popups"),
"icon": "notifications"
},
{
"id": "wallpaper",
"name": I18n.tr("Wallpaper"),
"description": I18n.tr("Desktop background images"),
"icon": "wallpaper"
},
{
"id": "osd",
"name": I18n.tr("On-Screen Displays"),
"description": I18n.tr("Volume, brightness, and other system OSDs"),
"icon": "picture_in_picture"
},
{
"id": "toast",
"name": I18n.tr("Toast Messages"),
"description": I18n.tr("System toast notifications"),
"icon": "campaign"
},
{
"id": "notepad",
"name": I18n.tr("Notepad Slideout"),
"description": I18n.tr("Quick note-taking slideout panel"),
"icon": "sticky_note_2"
}
];
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
variantComponents = getVariantComponentsList();
}
}
function getScreenPreferences(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
const barConfig = SettingsData.getBarConfig(barId);
return barConfig?.screenPreferences || ["all"];
}
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
}
function setScreenPreferences(componentId, screenNames) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
SettingsData.updateBarConfig(barId, {
"screenPreferences": screenNames
});
return;
}
var prefs = SettingsData.screenPreferences || {};
var newPrefs = Object.assign({}, prefs);
newPrefs[componentId] = screenNames;
SettingsData.set("screenPreferences", newPrefs);
}
function getShowOnLastDisplay(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
const barConfig = SettingsData.getBarConfig(barId);
return barConfig?.showOnLastDisplay ?? true;
}
return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false;
}
function setShowOnLastDisplay(componentId, enabled) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
SettingsData.updateBarConfig(barId, {
"showOnLastDisplay": enabled
});
return;
}
var prefs = SettingsData.showOnLastDisplay || {};
var newPrefs = Object.assign({}, prefs);
newPrefs[componentId] = enabled;
SettingsData.set("showOnLastDisplay", newPrefs);
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: screensInfoSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Connected Displays")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Configure which displays show shell components")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Available Screens (") + Quickshell.screens.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Item {
width: 1
height: 1
Layout.fillWidth: true
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Display Name Format")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
DankButtonGroup {
id: displayModeGroup
model: [I18n.tr("Name"), I18n.tr("Model")]
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.displayNameMode = index === 1 ? "model" : "system";
SettingsData.saveSettings();
}
Connections {
target: SettingsData
function onDisplayNameModeChanged() {
displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0;
}
}
}
}
}
}
Repeater {
model: Quickshell.screens
delegate: Rectangle {
width: parent.width
height: screenRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 0
Row {
id: screenRow
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
name: "desktop_windows"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS / 2
StyledText {
text: SettingsData.getScreenDisplayName(modelData)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
property var currentMode: wlrOutput?.currentMode
StyledText {
text: {
if (parent.currentMode) {
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz";
}
return modelData.width + "×" + modelData.height;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
Repeater {
model: root.variantComponents
delegate: StyledRect {
width: parent.width
height: componentSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: componentSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Show on screens:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
property string componentId: modelData.id
width: parent.width
spacing: Theme.spacingXS
DankToggle {
width: parent.width
text: I18n.tr("All displays")
description: I18n.tr("Show on all connected displays")
checked: {
var prefs = root.getScreenPreferences(parent.componentId);
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
}
onToggled: checked => {
if (checked) {
root.setScreenPreferences(parent.componentId, ["all"]);
} else {
root.setScreenPreferences(parent.componentId, []);
const cid = parent.componentId;
if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) {
root.setShowOnLastDisplay(cid, true);
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show on Last Display")
description: I18n.tr("Always show when there's only one connected display")
checked: root.getShowOnLastDisplay(parent.componentId)
visible: {
const prefs = root.getScreenPreferences(parent.componentId);
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
const cid = parent.componentId;
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:");
return !isAll && isRelevantComponent;
}
onToggled: checked => {
root.setShowOnLastDisplay(parent.componentId, checked);
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: {
var prefs = root.getScreenPreferences(parent.componentId);
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: {
var prefs = root.getScreenPreferences(parent.componentId);
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
}
Repeater {
model: Quickshell.screens
delegate: DankToggle {
property var screenData: modelData
property string componentId: parent.parent.componentId
width: parent.width
text: SettingsData.getScreenDisplayName(screenData)
description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name)
checked: {
var prefs = root.getScreenPreferences(componentId);
if (typeof prefs[0] === "string" && prefs[0] === "all")
return false;
return SettingsData.isScreenInPreferences(screenData, prefs);
}
onToggled: checked => {
var currentPrefs = root.getScreenPreferences(componentId);
if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") {
currentPrefs = [];
}
const screenModelIndex = SettingsData.getScreenModelIndex(screenData);
var newPrefs = currentPrefs.filter(pref => {
if (typeof pref === "string")
return false;
if (pref.modelIndex !== undefined && screenModelIndex >= 0) {
return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex);
}
return pref.name !== screenData.name || pref.model !== screenData.model;
});
if (checked) {
const prefObj = {
"name": screenData.name,
"model": screenData.model || ""
};
if (screenModelIndex >= 0) {
prefObj.modelIndex = screenModelIndex;
}
newPrefs.push(prefObj);
}
root.setScreenPreferences(componentId, newPrefs);
}
}
}
}
}
}
}
}
}
}
}
}
}
@@ -1,12 +1,10 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: displaysTab id: root
function formatGammaTime(isoString) { function formatGammaTime(isoString) {
if (!isoString) if (!isoString)
@@ -21,113 +19,6 @@ Item {
} }
} }
function getBarComponentsFromSettings() {
const bars = SettingsData.barConfigs || [];
return bars.map(bar => ({
"id": "bar:" + bar.id,
"name": bar.name || "Bar",
"description": I18n.tr("Individual bar configuration"),
"icon": "toolbar",
"barId": bar.id
}));
}
property var variantComponents: getVariantComponentsList()
function getVariantComponentsList() {
return [...getBarComponentsFromSettings(),
{
"id": "dock",
"name": I18n.tr("Application Dock"),
"description": I18n.tr("Bottom dock for pinned and running applications"),
"icon": "dock"
},
{
"id": "notifications",
"name": I18n.tr("Notification Popups"),
"description": I18n.tr("Notification toast popups"),
"icon": "notifications"
},
{
"id": "wallpaper",
"name": I18n.tr("Wallpaper"),
"description": I18n.tr("Desktop background images"),
"icon": "wallpaper"
},
{
"id": "osd",
"name": I18n.tr("On-Screen Displays"),
"description": I18n.tr("Volume, brightness, and other system OSDs"),
"icon": "picture_in_picture"
},
{
"id": "toast",
"name": I18n.tr("Toast Messages"),
"description": I18n.tr("System toast notifications"),
"icon": "campaign"
},
{
"id": "notepad",
"name": I18n.tr("Notepad Slideout"),
"description": I18n.tr("Quick note-taking slideout panel"),
"icon": "sticky_note_2"
},
];
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
variantComponents = getVariantComponentsList();
}
}
function getScreenPreferences(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
const barConfig = SettingsData.getBarConfig(barId);
return barConfig?.screenPreferences || ["all"];
}
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
}
function setScreenPreferences(componentId, screenNames) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
SettingsData.updateBarConfig(barId, {
screenPreferences: screenNames
});
return;
}
var prefs = SettingsData.screenPreferences || {};
var newPrefs = Object.assign({}, prefs);
newPrefs[componentId] = screenNames;
SettingsData.set("screenPreferences", newPrefs);
}
function getShowOnLastDisplay(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
const barConfig = SettingsData.getBarConfig(barId);
return barConfig?.showOnLastDisplay ?? true;
}
return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false;
}
function setShowOnLastDisplay(componentId, enabled) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
SettingsData.updateBarConfig(barId, {
showOnLastDisplay: enabled
});
return;
}
var prefs = SettingsData.showOnLastDisplay || {};
var newPrefs = Object.assign({}, prefs);
newPrefs[componentId] = enabled;
SettingsData.set("showOnLastDisplay", newPrefs);
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -678,7 +569,7 @@ Item {
} }
StyledText { StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunriseTime) text: root.formatGammaTime(DisplayService.gammaSunriseTime)
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -714,7 +605,7 @@ Item {
} }
StyledText { StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunsetTime) text: root.formatGammaTime(DisplayService.gammaSunsetTime)
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -761,7 +652,7 @@ Item {
} }
StyledText { StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaNextTransition) text: root.formatGammaTime(DisplayService.gammaNextTransition)
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -773,371 +664,6 @@ Item {
} }
} }
} }
StyledRect {
width: parent.width
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: screensInfoSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Connected Displays")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Configure which displays show shell components")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Available Screens (") + Quickshell.screens.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Item {
width: 1
height: 1
Layout.fillWidth: true
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Display Name Format")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
DankButtonGroup {
id: displayModeGroup
model: [I18n.tr("Name"), I18n.tr("Model")]
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.displayNameMode = index === 1 ? "model" : "system";
SettingsData.saveSettings();
}
Connections {
target: SettingsData
function onDisplayNameModeChanged() {
displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0;
}
}
}
}
}
}
Repeater {
model: Quickshell.screens
delegate: Rectangle {
width: parent.width
height: screenRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 0
Row {
id: screenRow
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
name: "desktop_windows"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS / 2
StyledText {
text: SettingsData.getScreenDisplayName(modelData)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
property var currentMode: wlrOutput?.currentMode
StyledText {
text: {
if (parent.currentMode) {
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz";
}
return modelData.width + "×" + modelData.height;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
Repeater {
model: displaysTab.variantComponents
delegate: StyledRect {
width: parent.width
height: componentSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: componentSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Show on screens:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
property string componentId: modelData.id
width: parent.width
spacing: Theme.spacingXS
DankToggle {
width: parent.width
text: I18n.tr("All displays")
description: I18n.tr("Show on all connected displays")
checked: {
var prefs = displaysTab.getScreenPreferences(parent.componentId);
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
}
onToggled: checked => {
if (checked) {
displaysTab.setScreenPreferences(parent.componentId, ["all"]);
} else {
displaysTab.setScreenPreferences(parent.componentId, []);
const cid = parent.componentId;
if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) {
displaysTab.setShowOnLastDisplay(cid, true);
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show on Last Display")
description: I18n.tr("Always show when there's only one connected display")
checked: displaysTab.getShowOnLastDisplay(parent.componentId)
visible: {
const prefs = displaysTab.getScreenPreferences(parent.componentId);
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
const cid = parent.componentId;
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:");
return !isAll && isRelevantComponent;
}
onToggled: checked => {
displaysTab.setShowOnLastDisplay(parent.componentId, checked);
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: {
var prefs = displaysTab.getScreenPreferences(parent.componentId);
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: {
var prefs = displaysTab.getScreenPreferences(parent.componentId);
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
}
Repeater {
model: Quickshell.screens
delegate: DankToggle {
property var screenData: modelData
property string componentId: parent.parent.componentId
width: parent.width
text: SettingsData.getScreenDisplayName(screenData)
description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name)
checked: {
var prefs = displaysTab.getScreenPreferences(componentId);
if (typeof prefs[0] === "string" && prefs[0] === "all")
return false;
return SettingsData.isScreenInPreferences(screenData, prefs);
}
onToggled: checked => {
var currentPrefs = displaysTab.getScreenPreferences(componentId);
if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") {
currentPrefs = [];
}
const screenModelIndex = SettingsData.getScreenModelIndex(screenData);
var newPrefs = currentPrefs.filter(pref => {
if (typeof pref === "string")
return false;
if (pref.modelIndex !== undefined && screenModelIndex >= 0) {
return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex);
}
return pref.name !== screenData.name || pref.model !== screenData.model;
});
if (checked) {
const prefObj = {
name: screenData.name,
model: screenData.model || ""
};
if (screenModelIndex >= 0) {
prefObj.modelIndex = screenModelIndex;
}
newPrefs.push(prefObj);
}
displaysTab.setScreenPreferences(componentId, newPrefs);
}
}
}
}
}
}
}
}
}
}
} }
} }
} }
+12 -27
View File
@@ -328,36 +328,21 @@ Item {
} }
} }
Rectangle { DankButton {
id: fixButton id: fixButton
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: Math.round(Theme.fontSizeMedium * 2.5)
radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...")
if (warningBox.showSetup)
return I18n.tr("Setup")
return I18n.tr("Fix Now")
}
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !KeybindsService.fixing
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: KeybindsService.fixDmsBindsInclude()
StyledText {
id: fixButtonText
text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...");
if (warningBox.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surface
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: !KeybindsService.fixing
onClicked: KeybindsService.fixDmsBindsInclude()
}
} }
} }
} }
@@ -32,15 +32,17 @@ StyledRect {
readonly property bool collapsed: collapsible && !expanded readonly property bool collapsed: collapsible && !expanded
readonly property bool hasHeader: root.title !== "" || root.iconName !== "" readonly property bool hasHeader: root.title !== "" || root.iconName !== ""
property bool animationsEnabled: false property bool userToggledCollapse: false
Component.onCompleted: Qt.callLater(() => animationsEnabled = true)
Behavior on height { Behavior on height {
enabled: root.animationsEnabled enabled: root.userToggledCollapse
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
onRunningChanged: {
if (!running)
root.userToggledCollapse = false;
}
} }
} }
@@ -98,6 +100,7 @@ StyledRect {
onClicked: { onClicked: {
if (!root.collapsible) if (!root.collapsible)
return; return;
root.userToggledCollapse = true;
root.expanded = !root.expanded; root.expanded = !root.expanded;
} }
} }
@@ -108,14 +111,6 @@ StyledRect {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: !root.collapsed visible: !root.collapsed
opacity: root.collapsed ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
} }
} }
+74 -15
View File
@@ -234,23 +234,82 @@ PanelWindow {
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { Item {
id: detailsText width: parent.width - Theme.spacingS * 2
text: ToastService.currentDetails height: detailsText.implicitHeight
font.pixelSize: Theme.fontSizeSmall anchors.horizontalCenter: parent.horizontalCenter
color: { visible: ToastService.currentDetails.length > 0
switch (ToastService.currentLevel) {
case ToastService.levelError: StyledText {
case ToastService.levelWarn: id: detailsText
return SessionData.isLightMode ? Theme.surfaceText : Theme.background; text: ToastService.currentDetails
default: font.pixelSize: Theme.fontSizeSmall
return Theme.surfaceText; color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
default:
return Theme.surfaceText;
}
}
anchors.left: parent.left
anchors.right: copyDetailsButton.left
anchors.rightMargin: Theme.spacingS
wrapMode: Text.Wrap
}
DankActionButton {
id: copyDetailsButton
iconName: "content_copy"
iconSize: Theme.iconSizeSmall
iconColor: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
default:
return Theme.surfaceText;
}
}
buttonSize: Theme.iconSizeSmall + 8
anchors.right: parent.right
anchors.top: parent.top
property bool showTooltip: false
onClicked: {
Quickshell.execDetached(["dms", "cl", "copy", ToastService.currentDetails]);
showTooltip = true;
detailsTooltipTimer.start();
}
Timer {
id: detailsTooltipTimer
interval: 1500
onTriggered: copyDetailsButton.showTooltip = false
}
Rectangle {
visible: copyDetailsButton.showTooltip
width: detailsTooltipLabel.implicitWidth + 16
height: detailsTooltipLabel.implicitHeight + 8
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyDetailsButton.width / 2
StyledText {
id: detailsTooltipLabel
anchors.centerIn: parent
text: root.copiedText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
} }
} }
visible: ToastService.currentDetails.length > 0
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.Wrap
} }
Rectangle { Rectangle {
+82
View File
@@ -1,13 +1,19 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common
Singleton { Singleton {
id: root id: root
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string mangoDmsDir: configDir + "/mango/dms"
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
property bool dwlAvailable: false property bool dwlAvailable: false
property var outputs: ({}) property var outputs: ({})
property var tagCount: 9 property var tagCount: 9
@@ -263,4 +269,80 @@ Singleton {
return Array.from(visibleTags).sort((a, b) => a - b); return Array.from(visibleTags).sort((a, b) => a - b);
} }
function generateOutputsConfig(outputsData) {
if (!outputsData || Object.keys(outputsData).length === 0)
return;
let lines = ["# Auto-generated by DMS - do not edit manually", "# VRR is global: set adaptive_sync=1 in config.conf", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
let width = 1920;
let height = 1080;
let refreshRate = 60;
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode];
if (mode) {
width = mode.width || 1920;
height = mode.height || 1080;
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000);
}
}
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const scale = output.logical?.scale ?? 1.0;
const transform = transformToMango(output.logical?.transform ?? "Normal");
const rule = [outputName, "0.55", "1", "tile", transform, scale, x, y, width, height, refreshRate].join(",");
lines.push("monitorrule=" + rule);
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("DwlService: Failed to write outputs config:", output);
return;
}
console.info("DwlService: Generated outputs config at", outputsPath);
if (CompositorService.isDwl)
reloadConfig();
});
}
function reloadConfig() {
Proc.runCommand("mango-reload", ["mmsg", "-d", "reload_config"], (output, exitCode) => {
if (exitCode !== 0)
console.warn("DwlService: mmsg reload_config failed:", output);
});
}
function transformToMango(transform) {
switch (transform) {
case "Normal":
return 0;
case "90":
return 1;
case "180":
return 2;
case "270":
return 3;
case "Flipped":
return 4;
case "Flipped90":
return 5;
case "Flipped180":
return 6;
case "Flipped270":
return 7;
default:
return 0;
}
}
} }
+175
View File
@@ -0,0 +1,175 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import qs.Common
Singleton {
id: root
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string hyprDmsDir: configDir + "/hypr/dms"
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model)
return "desc:" + output.make + " " + output.model;
return outputName;
}
function generateOutputsConfig(outputsData, hyprlandSettings) {
if (!outputsData || Object.keys(outputsData).length === 0)
return;
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
let monitorv2Blocks = [];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
const identifier = getOutputIdentifier(output, outputName);
const outputSettings = settings[identifier] || {};
let resolution = "preferred";
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode];
if (mode)
resolution = mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3);
}
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const position = x + "x" + y;
const scale = output.logical?.scale ?? 1.0;
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale;
const transform = transformToHyprland(output.logical?.transform ?? "Normal");
if (transform !== 0)
monitorLine += ", transform, " + transform;
if (output.vrr_supported && output.vrr_enabled)
monitorLine += ", vrr, 1";
if (outputSettings.bitdepth && outputSettings.bitdepth !== 8)
monitorLine += ", bitdepth, " + outputSettings.bitdepth;
if (outputSettings.colorManagement && outputSettings.colorManagement !== "auto")
monitorLine += ", cm, " + outputSettings.colorManagement;
if (outputSettings.sdrBrightness !== undefined && outputSettings.sdrBrightness !== 1.0)
monitorLine += ", sdrbrightness, " + outputSettings.sdrBrightness;
if (outputSettings.sdrSaturation !== undefined && outputSettings.sdrSaturation !== 1.0)
monitorLine += ", sdrsaturation, " + outputSettings.sdrSaturation;
lines.push(monitorLine);
const needsMonitorv2 = outputSettings.supportsHdr || outputSettings.supportsWideColor ||
outputSettings.sdrMinLuminance !== undefined || outputSettings.sdrMaxLuminance !== undefined ||
outputSettings.minLuminance !== undefined || outputSettings.maxLuminance !== undefined ||
outputSettings.maxAvgLuminance !== undefined;
if (needsMonitorv2) {
let block = "monitorv2 {\n";
block += " output = " + identifier + "\n";
if (outputSettings.supportsWideColor)
block += " supports_wide_color = true\n";
if (outputSettings.supportsHdr)
block += " supports_hdr = true\n";
if (outputSettings.sdrMinLuminance !== undefined)
block += " sdr_min_luminance = " + outputSettings.sdrMinLuminance + "\n";
if (outputSettings.sdrMaxLuminance !== undefined)
block += " sdr_max_luminance = " + outputSettings.sdrMaxLuminance + "\n";
if (outputSettings.minLuminance !== undefined)
block += " min_luminance = " + outputSettings.minLuminance + "\n";
if (outputSettings.maxLuminance !== undefined)
block += " max_luminance = " + outputSettings.maxLuminance + "\n";
if (outputSettings.maxAvgLuminance !== undefined)
block += " max_avg_luminance = " + outputSettings.maxAvgLuminance + "\n";
block += "}";
monitorv2Blocks.push(block);
}
}
if (monitorv2Blocks.length > 0) {
lines.push("");
for (const block of monitorv2Blocks)
lines.push(block);
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("HyprlandService: Failed to write outputs config:", output);
return;
}
console.info("HyprlandService: Generated outputs config at", outputsPath);
if (CompositorService.isHyprland)
reloadConfig();
});
}
function reloadConfig() {
Proc.runCommand("hyprctl-reload", ["hyprctl", "reload"], (output, exitCode) => {
if (exitCode !== 0)
console.warn("HyprlandService: hyprctl reload failed:", output);
});
}
function transformToHyprland(transform) {
switch (transform) {
case "Normal":
return 0;
case "90":
return 1;
case "180":
return 2;
case "270":
return 3;
case "Flipped":
return 4;
case "Flipped90":
return 5;
case "Flipped180":
return 6;
case "Flipped270":
return 7;
default:
return 0;
}
}
function hyprlandToTransform(value) {
switch (value) {
case 0:
return "Normal";
case 1:
return "90";
case 2:
return "180";
case 3:
return "270";
case 4:
return "Flipped";
case 5:
return "Flipped90";
case 6:
return "Flipped180";
case 7:
return "Flipped270";
default:
return "Normal";
}
}
}
+238 -34
View File
@@ -1,5 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
@@ -23,6 +23,8 @@ Singleton {
property var windows: [] property var windows: []
property var displayScales: ({}) property var displayScales: ({})
property var _realOutputs: ({})
property bool inOverview: false property bool inOverview: false
property int currentKeyboardLayoutIndex: 0 property int currentKeyboardLayoutIndex: 0
@@ -214,12 +216,12 @@ Singleton {
const ws = workspaces[w.workspace_id]; const ws = workspaces[w.workspace_id];
if (!ws) { if (!ws) {
return { return {
window: w, "window": w,
outputX: 999999, "outputX": 999999,
outputY: 999999, "outputY": 999999,
wsIdx: 999999, "wsIdx": 999999,
col: 999999, "col": 999999,
row: 999999 "row": 999999
}; };
} }
@@ -232,12 +234,12 @@ Singleton {
const row = (pos && pos.length >= 2) ? pos[1] : 999999; const row = (pos && pos.length >= 2) ? pos[1] : 999999;
return { return {
window: w, "window": w,
outputX: outputX, "outputX": outputX,
outputY: outputY, "outputY": outputY,
wsIdx: ws.idx, "wsIdx": ws.idx,
col: col, "col": col,
row: row "row": row
}; };
}); });
@@ -578,7 +580,6 @@ Singleton {
const windowIndex = windows.findIndex(w => w.id === data.id); const windowIndex = windows.findIndex(w => w.id === data.id);
if (windowIndex < 0) if (windowIndex < 0)
return; return;
const updatedWindows = [...windows]; const updatedWindows = [...windows];
const updatedWindow = {}; const updatedWindow = {};
for (let prop in updatedWindows[windowIndex]) { for (let prop in updatedWindows[windowIndex]) {
@@ -598,7 +599,7 @@ Singleton {
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR"); const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path]; const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
Quickshell.execDetached({ Quickshell.execDetached({
command: command "command": command
}); });
pendingScreenshotPath = ""; pendingScreenshotPath = "";
} }
@@ -993,35 +994,35 @@ Singleton {
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4; const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const dmsWarning = `// ! DO NOT EDIT ! const dmsWarning = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS ! // ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN ! // ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE ! // ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`; `;
const configContent = dmsWarning + `layout { const configContent = dmsWarning + `layout {
gaps ${gaps} gaps ${gaps}
border { border {
width 2 width 2
} }
focus-ring { focus-ring {
width 2 width 2
} }
} }
window-rule { window-rule {
geometry-corner-radius ${cornerRadius} geometry-corner-radius ${cornerRadius}
clip-to-geometry true clip-to-geometry true
tiled-state true tiled-state true
draw-border-with-background false draw-border-with-background false
}`; }`;
const alttabContent = dmsWarning + `recent-windows { const alttabContent = dmsWarning + `recent-windows {
highlight { highlight {
corner-radius ${cornerRadius} corner-radius ${cornerRadius}
} }
}`; }`;
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms"; const niriDmsDir = configDir + "/niri/dms";
@@ -1054,6 +1055,209 @@ window-rule {
writeBlurruleProcess.running = true; writeBlurruleProcess.running = true;
} }
function updateOutputPosition(outputName, x, y) {
if (!outputs || !outputs[outputName])
return;
const updatedOutputs = {};
for (const name in outputs) {
const output = outputs[name];
if (name === outputName && output.logical) {
updatedOutputs[name] = JSON.parse(JSON.stringify(output));
updatedOutputs[name].logical.x = x;
updatedOutputs[name].logical.y = y;
} else {
updatedOutputs[name] = output;
}
}
outputs = updatedOutputs;
}
function applyOutputConfig(outputName, config, callback) {
if (!CompositorService.isNiri || !outputName) {
if (callback)
callback(false, "Invalid config");
return;
}
const commands = [];
if (config.position !== undefined) {
commands.push(`niri msg output "${outputName}" position ${config.position.x} ${config.position.y}`);
}
if (config.mode !== undefined) {
commands.push(`niri msg output "${outputName}" mode ${config.mode}`);
}
if (config.vrr !== undefined) {
commands.push(`niri msg output "${outputName}" vrr ${config.vrr ? "on" : "off"}`);
}
if (config.scale !== undefined) {
commands.push(`niri msg output "${outputName}" scale ${config.scale}`);
}
if (config.transform !== undefined) {
commands.push(`niri msg output "${outputName}" transform "${config.transform}"`);
}
if (commands.length === 0) {
if (callback)
callback(true, "No changes");
return;
}
const fullCommand = commands.join(" && ");
Proc.runCommand("niri-output-config", ["sh", "-c", fullCommand], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to apply output config:", output);
if (callback)
callback(false, output);
return;
}
console.info("NiriService: Applied output config for", outputName);
fetchOutputs();
if (callback)
callback(true, "Success");
});
}
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
const serial = output.serial || "Unknown";
return output.make + " " + output.model + " " + serial;
}
return outputName;
}
function generateOutputsConfig(outputsData) {
const data = outputsData || outputs;
if (!data || Object.keys(data).length === 0)
return;
let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`;
for (const outputName in data) {
const output = data[outputName];
const identifier = getOutputIdentifier(output, outputName);
const niriSettings = SettingsData.getNiriOutputSettings(identifier);
kdlContent += `output "${identifier}" {\n`;
if (niriSettings.disabled) {
kdlContent += ` off\n}\n\n`;
continue;
}
if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) {
const mode = output.modes[output.current_mode];
kdlContent += ` mode "${mode.width}x${mode.height}@${(mode.refresh_rate / 1000).toFixed(3)}"\n`;
}
if (output.logical) {
if (output.logical.scale && output.logical.scale !== 1.0) {
kdlContent += ` scale ${output.logical.scale}\n`;
}
if (output.logical.transform && output.logical.transform !== "Normal") {
const transformMap = {
"Normal": "normal",
"90": "90",
"180": "180",
"270": "270",
"Flipped": "flipped",
"Flipped90": "flipped-90",
"Flipped180": "flipped-180",
"Flipped270": "flipped-270"
};
kdlContent += ` transform "${transformMap[output.logical.transform] || "normal"}"\n`;
}
if (output.logical.x !== undefined && output.logical.y !== undefined) {
kdlContent += ` position x=${output.logical.x} y=${output.logical.y}\n`;
}
}
if (output.vrr_enabled) {
const vrrOnDemand = niriSettings.vrrOnDemand ?? false;
kdlContent += vrrOnDemand ? ` variable-refresh-rate on-demand=true\n` : ` variable-refresh-rate\n`;
}
if (niriSettings.focusAtStartup) {
kdlContent += ` focus-at-startup\n`;
}
if (niriSettings.backdropColor) {
kdlContent += ` backdrop-color "${niriSettings.backdropColor}"\n`;
}
kdlContent += generateHotCornersBlock(niriSettings);
kdlContent += generateLayoutBlock(niriSettings);
kdlContent += `}\n\n`;
}
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms";
const outputsPath = niriDmsDir + "/outputs.kdl";
Proc.runCommand("niri-write-outputs", ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${kdlContent}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to write outputs config:", output);
return;
}
console.info("NiriService: Generated outputs config at", outputsPath);
});
}
function generateHotCornersBlock(niriSettings) {
if (!niriSettings.hotCorners)
return "";
const hc = niriSettings.hotCorners;
if (hc.off)
return ` hot-corners {\n off\n }\n`;
const corners = hc.corners || [];
if (corners.length === 0)
return "";
let block = ` hot-corners {\n`;
for (const corner of corners) {
block += ` ${corner}\n`;
}
block += ` }\n`;
return block;
}
function generateLayoutBlock(niriSettings) {
if (!niriSettings.layout)
return "";
const layout = niriSettings.layout;
const hasSettings = layout.gaps !== undefined || layout.defaultColumnWidth || layout.presetColumnWidths || layout.alwaysCenterSingleColumn !== undefined;
if (!hasSettings)
return "";
let block = ` layout {\n`;
if (layout.gaps !== undefined)
block += ` gaps ${layout.gaps}\n`;
if (layout.defaultColumnWidth?.type === "proportion") {
const val = layout.defaultColumnWidth.value;
const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString();
block += ` default-column-width { proportion ${formatted}; }\n`;
}
if (layout.presetColumnWidths && layout.presetColumnWidths.length > 0) {
block += ` preset-column-widths {\n`;
for (const preset of layout.presetColumnWidths) {
if (preset.type === "proportion") {
const val = preset.value;
const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString();
block += ` proportion ${formatted}\n`;
}
}
block += ` }\n`;
}
if (layout.alwaysCenterSingleColumn !== undefined)
block += layout.alwaysCenterSingleColumn ? ` always-center-single-column\n` : ` always-center-single-column false\n`;
block += ` }\n`;
return block;
}
IpcHandler { IpcHandler {
function screenshot(): string { function screenshot(): string {
if (!CompositorService.isNiri) { if (!CompositorService.isNiri) {
+6 -4
View File
@@ -16,7 +16,7 @@ Rectangle {
property int buttonHeight: 40 property int buttonHeight: 40
property int horizontalPadding: Theme.spacingL property int horizontalPadding: Theme.spacingL
signal clicked() signal clicked
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64) width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
height: buttonHeight height: buttonHeight
@@ -29,9 +29,11 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: { color: {
if (pressed) return Theme.primaryPressed if (pressed)
if (hovered) return Theme.primaryHover return Theme.primaryPressed;
return "transparent" if (hovered)
return Theme.primaryHover;
return "transparent";
} }
Behavior on color { Behavior on color {
+14 -1
View File
@@ -18,6 +18,7 @@ Flow {
property int buttonPadding: Theme.spacingL property int buttonPadding: Theme.spacingL
property int checkIconSize: Theme.iconSizeSmall property int checkIconSize: Theme.iconSizeSmall
property int textSize: Theme.fontSizeMedium property int textSize: Theme.fontSizeMedium
property bool userInteracted: false
signal selectionChanged(int index, bool selected) signal selectionChanged(int index, bool selected)
signal animationCompleted() signal animationCompleted()
@@ -27,7 +28,10 @@ Flow {
Timer { Timer {
id: animationTimer id: animationTimer
interval: Theme.shortDuration interval: Theme.shortDuration
onTriggered: root.animationCompleted() onTriggered: {
root.userInteracted = false;
root.animationCompleted();
}
} }
function isSelected(index) { function isSelected(index) {
@@ -38,6 +42,7 @@ Flow {
} }
function selectItem(index) { function selectItem(index) {
userInteracted = true;
if (multiSelect) { if (multiSelect) {
const modelValue = model[index] const modelValue = model[index]
let newSelection = [...currentSelection] let newSelection = [...currentSelection]
@@ -93,6 +98,7 @@ Flow {
bottomRightRadius: (isLast || selected) ? Theme.cornerRadius : 4 bottomRightRadius: (isLast || selected) ? Theme.cornerRadius : 4
Behavior on width { Behavior on width {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -100,6 +106,7 @@ Flow {
} }
Behavior on topLeftRadius { Behavior on topLeftRadius {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -107,6 +114,7 @@ Flow {
} }
Behavior on topRightRadius { Behavior on topRightRadius {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -114,6 +122,7 @@ Flow {
} }
Behavior on bottomLeftRadius { Behavior on bottomLeftRadius {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -121,6 +130,7 @@ Flow {
} }
Behavior on bottomRightRadius { Behavior on bottomRightRadius {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -128,6 +138,7 @@ Flow {
} }
Behavior on color { Behavior on color {
enabled: root.userInteracted
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -176,6 +187,7 @@ Flow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Behavior on opacity { Behavior on opacity {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
@@ -183,6 +195,7 @@ Flow {
} }
Behavior on scale { Behavior on scale {
enabled: root.userInteracted
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
+1 -1
View File
@@ -1,3 +1,3 @@
[templates.dmsghostty] [templates.dmsghostty]
input_path = 'SHELL_DIR/matugen/templates/ghostty.conf' input_path = 'SHELL_DIR/matugen/templates/ghostty.conf'
output_path = '~/.config/ghostty/config-dankcolors' output_path = '~/.config/ghostty/themes/dankcolors'