1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

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
This commit is contained in:
bbedward
2025-12-15 16:36:14 -05:00
parent bafe1c5fee
commit 2745116ac5
15 changed files with 2727 additions and 648 deletions

View File

@@ -238,7 +238,7 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
if proxy == nil || proxy.IsZombie() {
head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context())
head.SetID(objectID)
@@ -723,7 +723,7 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
if proxy == nil || proxy.IsZombie() {
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
@@ -761,8 +761,8 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
// Mode not yet registered, create it
if proxy == nil || proxy.IsZombie() {
// Mode not yet registered or zombie, create fresh
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)

View File

@@ -145,6 +145,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
head.name = e.Name
head.ready = true
m.post(func() {
m.updateState()
})
@@ -251,11 +252,11 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
m.heads.Delete(headID)
m.post(func() {
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.post(func() {
m.updateState()
})
})
@@ -310,11 +311,11 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
m.modes.Delete(modeID)
m.post(func() {
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.post(func() {
m.updateState()
})
})
@@ -328,6 +329,10 @@ func (m *Manager) updateState() {
return true
}
if !head.ready {
return true
}
modes := make([]OutputMode, 0)
var currentMode *OutputMode

View File

@@ -90,6 +90,7 @@ type headState struct {
modeIDs []uint32
adaptiveSync uint32
finished bool
ready bool
}
type modeState struct {

View File

@@ -273,8 +273,8 @@ Item {
anchors {
left: true
top: true
right: root.useSingleWindow ? true : undefined
bottom: root.useSingleWindow ? true : undefined
right: root.useSingleWindow
bottom: root.useSingleWindow
}
WlrLayershell.margins {
@@ -284,8 +284,8 @@ Item {
bottom: 0
}
implicitWidth: root.useSingleWindow ? undefined : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.useSingleWindow ? undefined : root.alignedHeight + (shadowBuffer * 2)
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible) {

View File

@@ -7,10 +7,11 @@ DankModal {
id: root
property string outputName: ""
property var position: undefined
property var mode: undefined
property var vrr: undefined
property int countdown: 15
property var changes: []
property int countdown: 10
signal confirmed
signal reverted
shouldBeVisible: false
allowStacking: true
@@ -23,23 +24,27 @@ DankModal {
repeat: true
running: root.shouldBeVisible
onTriggered: {
countdown--;
if (countdown <= 0) {
revert();
root.countdown--;
if (root.countdown <= 0) {
root.reverted();
root.close();
}
}
}
onOpened: {
countdown = 15;
countdown = 10;
countdownTimer.start();
}
onClosed: {
onDialogClosed: {
countdownTimer.stop();
}
onBackgroundClicked: revert
onBackgroundClicked: {
root.reverted();
root.close();
}
content: Component {
FocusScope {
@@ -50,12 +55,14 @@ DankModal {
implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => {
revert();
root.reverted();
root.close();
event.accepted = true;
}
Keys.onReturnPressed: event => {
confirm();
root.confirmed();
root.close();
event.accepted = true;
}
@@ -69,81 +76,42 @@ DankModal {
anchors.topMargin: Theme.spacingM
spacing: Theme.spacingM
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
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
}
StyledText {
text: I18n.tr("Confirm Display Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 80
height: 70
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
Column {
StyledText {
anchors.centerIn: parent
spacing: 4
StyledText {
text: I18n.tr("Reverting in:")
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
}
text: root.countdown + "s"
font.pixelSize: Theme.fontSizeXLarge * 1.5
color: Theme.primary
font.weight: Font.Bold
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: root.changes.length > 0
StyledText {
text: I18n.tr("Changes:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
Repeater {
model: root.changes
StyledText {
visible: position !== undefined && position !== null
text: I18n.tr("Position: ") + (position ? position.x + ", " + position.y : "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
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
StyledText {
required property var modelData
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
@@ -180,7 +148,10 @@ DankModal {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: revert
onClicked: {
root.reverted();
root.close();
}
}
}
@@ -206,7 +177,10 @@ DankModal {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: confirm
onClicked: {
root.confirmed();
root.close();
}
}
Behavior on color {
@@ -228,18 +202,11 @@ DankModal {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: revert
onClicked: {
root.reverted();
root.close();
}
}
}
}
function confirm() {
displaysTab.confirmChanges();
close();
}
function revert() {
displaysTab.revertChanges();
close();
}
}

View File

@@ -125,13 +125,45 @@ FocusScope {
}
Loader {
id: displaysLoader
id: displayConfigLoader
anchors.fill: parent
active: root.currentIndex === 6
active: root.currentIndex === 24
visible: active
focus: active
sourceComponent: DisplaysTab {}
sourceComponent: DisplayConfigTab {}
onActiveChanged: {
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) {

View File

@@ -58,7 +58,7 @@ FloatingWindow {
objectName: "settingsModal"
title: I18n.tr("Settings", "settings window title")
minimumSize: Qt.size(500, 400)
implicitWidth: 800
implicitWidth: 900
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
color: Theme.surfaceContainer
visible: false

View File

@@ -144,6 +144,32 @@ Rectangle {
"tabIndex": 2,
"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",
"text": I18n.tr("Network"),
@@ -157,12 +183,6 @@ Rectangle {
"icon": "computer",
"collapsedByDefault": true,
"children": [
{
"id": "displays",
"text": I18n.tr("Displays"),
"icon": "monitor",
"tabIndex": 6
},
{
"id": "printers",
"text": I18n.tr("Printers"),

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,12 +1,10 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: displaysTab
id: root
function formatGammaTime(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 {
anchors.fill: parent
clip: true
@@ -678,7 +569,7 @@ Item {
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunriseTime)
text: root.formatGammaTime(DisplayService.gammaSunriseTime)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
@@ -714,7 +605,7 @@ Item {
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunsetTime)
text: root.formatGammaTime(DisplayService.gammaSunsetTime)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
@@ -761,7 +652,7 @@ Item {
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaNextTransition)
text: root.formatGammaTime(DisplayService.gammaNextTransition)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
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);
}
}
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -328,36 +328,21 @@ Item {
}
}
Rectangle {
DankButton {
id: fixButton
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: Math.round(Theme.fontSizeMedium * 2.5)
radius: Theme.cornerRadius
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
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()
}
onClicked: KeybindsService.fixDmsBindsInclude()
}
}
}

View File

@@ -1,13 +1,19 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
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 var outputs: ({})
property var tagCount: 9
@@ -263,4 +269,80 @@ Singleton {
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;
}
}
}

View File

@@ -0,0 +1,125 @@
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) {
if (!outputsData || Object.keys(outputsData).length === 0)
return;
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
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;
const identifier = getOutputIdentifier(output, outputName);
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";
lines.push(monitorLine);
}
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";
}
}
}

View File

@@ -1,5 +1,5 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore
import QtQuick
@@ -23,6 +23,8 @@ Singleton {
property var windows: []
property var displayScales: ({})
property var _realOutputs: ({})
property bool inOverview: false
property int currentKeyboardLayoutIndex: 0
@@ -214,12 +216,12 @@ Singleton {
const ws = workspaces[w.workspace_id];
if (!ws) {
return {
window: w,
outputX: 999999,
outputY: 999999,
wsIdx: 999999,
col: 999999,
row: 999999
"window": w,
"outputX": 999999,
"outputY": 999999,
"wsIdx": 999999,
"col": 999999,
"row": 999999
};
}
@@ -232,12 +234,12 @@ Singleton {
const row = (pos && pos.length >= 2) ? pos[1] : 999999;
return {
window: w,
outputX: outputX,
outputY: outputY,
wsIdx: ws.idx,
col: col,
row: row
"window": w,
"outputX": outputX,
"outputY": outputY,
"wsIdx": ws.idx,
"col": col,
"row": row
};
});
@@ -598,7 +600,7 @@ Singleton {
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
Quickshell.execDetached({
command: command
"command": command
});
pendingScreenshotPath = "";
}
@@ -993,35 +995,35 @@ Singleton {
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const dmsWarning = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`;
`;
const configContent = dmsWarning + `layout {
gaps ${gaps}
gaps ${gaps}
border {
border {
width 2
}
}
focus-ring {
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`;
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`;
const alttabContent = dmsWarning + `recent-windows {
highlight {
highlight {
corner-radius ${cornerRadius}
}
}`;
}
}`;
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms";
@@ -1054,6 +1056,141 @@ window-rule {
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);
kdlContent += `output "${identifier}" {\n`;
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) {
kdlContent += ` variable-refresh-rate\n`;
}
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);
});
}
IpcHandler {
function screenshot(): string {
if (!CompositorService.isNiri) {