1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-04 03:22:12 -04:00

Compare commits

...

9 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
23 changed files with 3241 additions and 1773 deletions

View File

@@ -9,7 +9,7 @@ Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always
Restart=on-failure
RestartSec=1.23
TimeoutStopSec=10

View File

@@ -14,34 +14,63 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
## System Integration
**Wayland Protocols**
- `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
### Wayland Protocols (Client)
**DBus Interfaces**
- NetworkManager/iwd - Network management
- logind - Session control and inhibit locks
- accountsservice - User account information
- CUPS - Printer management
- Custom IPC via unix socket (JSON API)
All Wayland protocols are consumed as a client - connecting to the compositor.
**Hardware Control**
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
- Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
| Protocol | Purpose |
| ----------------------------------------- | ----------------------------------------------------------- |
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
| `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 lifecycle management
- Settings persistence
## CLI Commands
- `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes
- `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+
**Development build:**
```bash
make # Build dms CLI
make dankinstall # Build installer
@@ -77,6 +107,7 @@ make test # Run tests
```
**Distribution build:**
```bash
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`
**Installation:**
```bash
sudo make install # Install to /usr/local/bin/dms
```
@@ -91,6 +123,7 @@ sudo make install # Install to /usr/local/bin/dms
## Development
**Setup pre-commit hooks:**
```bash
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.
**Regenerating Wayland Protocol Bindings:**
```bash
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 \
@@ -105,6 +139,7 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
```
**Module Structure:**
- `cmd/` - Binary entrypoints (dms, dankinstall)
- `internal/distros/` - Distribution-specific installation logic
- `internal/proto/` - Wayland protocol bindings

View File

@@ -233,14 +233,28 @@ func runShellInteractive(session bool) {
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
if sig == syscall.SIGUSR1 {
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...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
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)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
@@ -459,15 +473,28 @@ func runShellDaemon(session bool) {
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
if sig == syscall.SIGUSR1 {
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...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
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()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)

View File

@@ -1,5 +1,5 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore
import QtQuick
@@ -360,47 +360,49 @@ Singleton {
property string displayNameMode: "system"
property var screenPreferences: ({})
property var showOnLastDisplay: ({})
property var niriOutputSettings: ({})
property var hyprlandOutputSettings: ({})
property var barConfigs: [
{
id: "default",
name: "Main Bar",
enabled: true,
position: 0,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4,
innerPadding: 4,
bottomGap: 0,
transparency: 1.0,
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
borderEnabled: false,
borderColor: "surfaceText",
borderOpacity: 1.0,
borderThickness: 1,
widgetOutlineEnabled: false,
widgetOutlineColor: "primary",
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
autoHide: false,
autoHideDelay: 250,
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace"
"id": "default",
"name": "Main Bar",
"enabled": true,
"position": 0,
"screenPreferences": ["all"],
"showOnLastDisplay": true,
"leftWidgets": ["launcherButton", "workspaceSwitcher", "focusedWindow"],
"centerWidgets": ["music", "clock", "weather"],
"rightWidgets": ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
"spacing": 4,
"innerPadding": 4,
"bottomGap": 0,
"transparency": 1.0,
"widgetTransparency": 1.0,
"squareCorners": false,
"noBackground": false,
"gothCornersEnabled": false,
"gothCornerRadiusOverride": false,
"gothCornerRadiusValue": 12,
"borderEnabled": false,
"borderColor": "surfaceText",
"borderOpacity": 1.0,
"borderThickness": 1,
"widgetOutlineEnabled": false,
"widgetOutlineColor": "primary",
"widgetOutlineOpacity": 1.0,
"widgetOutlineThickness": 1,
"fontScale": 1.0,
"autoHide": false,
"autoHideDelay": 250,
"openOnOverview": false,
"visible": true,
"popupGapsAuto": true,
"popupGapsManual": 4,
"maximizeDetection": true,
"scrollEnabled": true,
"scrollXBehavior": "column",
"scrollYBehavior": "workspace"
}
]
@@ -458,25 +460,25 @@ Singleton {
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
settings_file="$config_dir/settings.ini"
if [ -f "$settings_file" ]; then
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
settings_file="$config_dir/settings.ini"
if [ -f "$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
if grep -q "\\[Settings\\]" "$settings_file"; then
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
else
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
fi
if grep -q "\\[Settings\\]" "$settings_file"; then
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
else
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
fi
else
fi
else
echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file"
fi
done
fi
done
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
pkill -HUP -f 'gtk' 2>/dev/null || true`;
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
pkill -HUP -f 'gtk' 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", configScript]);
}
@@ -489,36 +491,36 @@ pkill -HUP -f 'gtk' 2>/dev/null || true`;
const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''");
const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true
update_qt_icon_theme() {
local config_file="$1"
local theme_name="$2"
if [ -f "$config_file" ]; then
if grep -q "^\\[Appearance\\]" "$config_file"; then
if grep -q "^icon_theme=" "$config_file"; then
update_qt_icon_theme() {
local config_file="$1"
local theme_name="$2"
if [ -f "$config_file" ]; then
if grep -q "^\\[Appearance\\]" "$config_file"; then
if grep -q "^icon_theme=" "$config_file"; then
sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file"
else
else
sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file"
fi
else
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
fi
else
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
fi
}
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`;
fi
else
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
fi
else
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
fi
}
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", script]);
}
readonly property var _hooks: ({
applyStoredTheme: applyStoredTheme,
regenSystemThemes: regenSystemThemes,
updateNiriLayout: updateNiriLayout,
applyStoredIconTheme: applyStoredIconTheme,
updateBarConfigs: updateBarConfigs
"applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes,
"updateNiriLayout": updateNiriLayout,
"applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs
})
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 rightBar = 0;
for (let i = 0; i < enabledBars.length; i++) {
for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i];
if (other.id === barConfig.id)
continue;
@@ -793,7 +795,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
if (barConfig) {
const enabledBars = getEnabledBarConfigs();
for (let i = 0; i < enabledBars.length; i++) {
for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i];
if (other.id === barConfig.id)
continue;
@@ -925,7 +927,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const conflicts = [];
const enabledBars = getEnabledBarConfigs();
for (let i = 0; i < enabledBars.length; i++) {
for (var i = 0; i < enabledBars.length; i++) {
const other = enabledBars[i];
if (other.id === barId)
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");
if (hasAll) {
conflicts.push({
barId: other.id,
barName: other.name,
reason: "Same position on all screens"
"barId": other.id,
"barName": other.name,
"reason": "Same position on all screens"
});
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));
if (overlapping) {
conflicts.push({
barId: other.id,
barName: other.name,
reason: "Same position on overlapping screens"
"barId": other.id,
"barName": other.name,
"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() {
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.sort((a, b) => {
@@ -989,7 +991,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
const sorted = getScreensSortedByPosition();
let modelCount = 0;
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].name === screen.name) {
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");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
spacing: spacing
"spacing": spacing
});
}
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
@@ -1216,7 +1218,7 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
return;
}
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");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
leftWidgets: order
"leftWidgets": 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");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
centerWidgets: order
"centerWidgets": 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");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
rightWidgets: order
"rightWidgets": 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");
if (defaultBar) {
updateBarConfig(defaultBar.id, {
leftWidgets: defaultLeft,
centerWidgets: defaultCenter,
rightWidgets: defaultRight
"leftWidgets": defaultLeft,
"centerWidgets": defaultCenter,
"rightWidgets": defaultRight
});
}
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");
if (defaultBar) {
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)) : {};
}
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 {
id: leftWidgetsModel
}

View File

@@ -258,6 +258,8 @@ var SPEC = {
displayNameMode: { def: "system" },
screenPreferences: { def: {} },
showOnLastDisplay: { def: {} },
niriOutputSettings: { def: {} },
hyprlandOutputSettings: { def: {} },
barConfigs: { def: [{
id: "default",

View File

@@ -32,9 +32,8 @@ FocusScope {
}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -48,9 +47,8 @@ FocusScope {
sourceComponent: TimeWeatherTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -66,9 +64,8 @@ FocusScope {
}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -84,9 +81,8 @@ FocusScope {
}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -100,9 +96,8 @@ FocusScope {
sourceComponent: WorkspacesTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -118,9 +113,8 @@ FocusScope {
}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -134,9 +128,8 @@ FocusScope {
sourceComponent: DisplayConfigTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -150,9 +143,8 @@ FocusScope {
sourceComponent: GammaControlTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -166,9 +158,8 @@ FocusScope {
sourceComponent: DisplayWidgetsTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -182,9 +173,8 @@ FocusScope {
sourceComponent: NetworkTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -198,9 +188,8 @@ FocusScope {
sourceComponent: PrinterTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -214,9 +203,8 @@ FocusScope {
sourceComponent: LauncherTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -230,9 +218,8 @@ FocusScope {
sourceComponent: ThemeColorsTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -246,9 +233,8 @@ FocusScope {
sourceComponent: LockScreenTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -264,9 +250,8 @@ FocusScope {
}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -280,9 +265,8 @@ FocusScope {
sourceComponent: AboutTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -296,9 +280,8 @@ FocusScope {
sourceComponent: TypographyMotionTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -312,9 +295,8 @@ FocusScope {
sourceComponent: SoundsTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -328,9 +310,8 @@ FocusScope {
sourceComponent: MediaPlayerTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -344,9 +325,8 @@ FocusScope {
sourceComponent: NotificationsTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -360,9 +340,8 @@ FocusScope {
sourceComponent: OSDTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -376,9 +355,8 @@ FocusScope {
sourceComponent: RunningAppsTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -392,9 +370,8 @@ FocusScope {
sourceComponent: SystemUpdaterTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -408,9 +385,8 @@ FocusScope {
sourceComponent: PowerSleepTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -426,9 +402,8 @@ FocusScope {
}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
@@ -442,9 +417,8 @@ FocusScope {
sourceComponent: ClipboardTab {}
onActiveChanged: {
if (active && item) {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}

View File

@@ -548,7 +548,7 @@ Item {
}
}
function getWorkspaceIndex(modelData) {
function getWorkspaceIndex(modelData, index) {
let isPlaceholder;
if (root.useExtWorkspace) {
isPlaceholder = modelData?.hidden === true;
@@ -976,7 +976,7 @@ Item {
StyledText {
id: wsIndexText
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
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
@@ -1203,12 +1203,12 @@ Item {
Loader {
id: indexLoader
anchors.fill: parent
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
active: SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
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
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)

View File

@@ -94,11 +94,10 @@ Item {
Timer {
id: hoverDelayTwo
interval: 1000
interval: 300
repeat: false
onTriggered: {
const p = refreshButtonMouseAreaTwo.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS);
refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), p.x, p.y, null);
refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), refreshButtonTwo, 0, 0, "left");
}
}
@@ -118,7 +117,7 @@ Item {
}
}
DankTooltip {
DankTooltipV2 {
id: refreshButtonTooltipTwo
}
@@ -820,11 +819,10 @@ Item {
Timer {
id: hoverDelay
interval: 1000
interval: 300
repeat: false
onTriggered: {
const p = refreshButtonMouseArea.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS);
refreshButtonTooltip.show(I18n.tr("Refresh Weather"), p.x, p.y, null);
refreshButtonTooltip.show(I18n.tr("Refresh Weather"), refreshButton, 0, 0, "left");
}
}
@@ -844,7 +842,7 @@ Item {
}
}
DankTooltip {
DankTooltipV2 {
id: refreshButtonTooltip
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)));
}
}
}
}
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}

View File

@@ -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);
}
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -32,15 +32,17 @@ StyledRect {
readonly property bool collapsed: collapsible && !expanded
readonly property bool hasHeader: root.title !== "" || root.iconName !== ""
property bool animationsEnabled: false
Component.onCompleted: Qt.callLater(() => animationsEnabled = true)
property bool userToggledCollapse: false
Behavior on height {
enabled: root.animationsEnabled
enabled: root.userToggledCollapse
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
onRunningChanged: {
if (!running)
root.userToggledCollapse = false;
}
}
}
@@ -98,6 +100,7 @@ StyledRect {
onClicked: {
if (!root.collapsible)
return;
root.userToggledCollapse = true;
root.expanded = !root.expanded;
}
}
@@ -108,14 +111,6 @@ StyledRect {
width: parent.width
spacing: Theme.spacingM
visible: !root.collapsed
opacity: root.collapsed ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}

View File

@@ -234,23 +234,82 @@ PanelWindow {
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
StyledText {
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
default:
return Theme.surfaceText;
Item {
width: parent.width - Theme.spacingS * 2
height: detailsText.implicitHeight
anchors.horizontalCenter: parent.horizontalCenter
visible: ToastService.currentDetails.length > 0
StyledText {
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
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 {

View File

@@ -14,21 +14,27 @@ Singleton {
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
if (SettingsData.displayNameMode === "model" && output.make && output.model)
return "desc:" + output.make + " " + output.model;
}
return outputName;
}
function generateOutputsConfig(outputsData) {
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];
@@ -39,10 +45,8 @@ Singleton {
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const position = x + "x" + y;
const scale = output.logical?.scale ?? 1.0;
const identifier = getOutputIdentifier(output, outputName);
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale;
const transform = transformToHyprland(output.logical?.transform ?? "Normal");
@@ -52,7 +56,53 @@ Singleton {
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("");

View File

@@ -580,7 +580,6 @@ Singleton {
const windowIndex = windows.findIndex(w => w.id === data.id);
if (windowIndex < 0)
return;
const updatedWindows = [...windows];
const updatedWindow = {};
for (let prop in updatedWindows[windowIndex]) {
@@ -1140,8 +1139,15 @@ Singleton {
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`;
@@ -1172,9 +1178,21 @@ Singleton {
}
if (output.vrr_enabled) {
kdlContent += ` variable-refresh-rate\n`;
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`;
}
@@ -1191,6 +1209,55 @@ Singleton {
});
}
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 {
function screenshot(): string {
if (!CompositorService.isNiri) {

View File

@@ -16,7 +16,7 @@ Rectangle {
property int buttonHeight: 40
property int horizontalPadding: Theme.spacingL
signal clicked()
signal clicked
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
height: buttonHeight
@@ -29,9 +29,11 @@ Rectangle {
anchors.fill: parent
radius: parent.radius
color: {
if (pressed) return Theme.primaryPressed
if (hovered) return Theme.primaryHover
return "transparent"
if (pressed)
return Theme.primaryPressed;
if (hovered)
return Theme.primaryHover;
return "transparent";
}
Behavior on color {

View File

@@ -18,6 +18,7 @@ Flow {
property int buttonPadding: Theme.spacingL
property int checkIconSize: Theme.iconSizeSmall
property int textSize: Theme.fontSizeMedium
property bool userInteracted: false
signal selectionChanged(int index, bool selected)
signal animationCompleted()
@@ -27,7 +28,10 @@ Flow {
Timer {
id: animationTimer
interval: Theme.shortDuration
onTriggered: root.animationCompleted()
onTriggered: {
root.userInteracted = false;
root.animationCompleted();
}
}
function isSelected(index) {
@@ -38,6 +42,7 @@ Flow {
}
function selectItem(index) {
userInteracted = true;
if (multiSelect) {
const modelValue = model[index]
let newSelection = [...currentSelection]
@@ -93,6 +98,7 @@ Flow {
bottomRightRadius: (isLast || selected) ? Theme.cornerRadius : 4
Behavior on width {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -100,6 +106,7 @@ Flow {
}
Behavior on topLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -107,6 +114,7 @@ Flow {
}
Behavior on topRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -114,6 +122,7 @@ Flow {
}
Behavior on bottomLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -121,6 +130,7 @@ Flow {
}
Behavior on bottomRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -128,6 +138,7 @@ Flow {
}
Behavior on color {
enabled: root.userInteracted
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -176,6 +187,7 @@ Flow {
anchors.verticalCenter: parent.verticalCenter
Behavior on opacity {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -183,6 +195,7 @@ Flow {
}
Behavior on scale {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing