1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Compare commits

...

11 Commits

Author SHA1 Message Date
bbedward
53fb927e36 niri: color and layout config generation 2025-10-02 12:34:17 -04:00
bbedward
fb5aa0313e cleanup debug logs, fix center section plugins 2025-10-02 12:24:45 -04:00
bbedward
9b41eecbf1 Fix reactivity, different settings structure, etc, etc. 2025-10-02 12:13:49 -04:00
bbedward
ae461b1caf Merge branch 'master' of github.com:bbedward/DankMaterialShell into wip/plugins 2025-10-02 00:22:32 -04:00
bbedward
57e36d6710 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-02 00:13:43 -04:00
bbedward
a7c4f09c5b always blockLoading on fileview 2025-10-02 00:13:29 -04:00
bbedward
554ef16e49 moar 2025-10-01 23:37:03 -04:00
bbedward
082321f860 de-dupe env 2025-10-01 22:13:03 -04:00
bbedward
df4f7b8c9e Set XDG_SESSION_TYPE in greeter
Always wayland, which is probably fine for our use case. Fixes gnome
2025-10-01 22:10:29 -04:00
bbedward
3f1742f074 lock+greeter: show keyboard layout widget, spacing adjustments 2025-10-01 21:08:55 -04:00
bbedward
4560d5c2d5 Fix overview auto hide bar 2025-10-01 18:16:27 -04:00
46 changed files with 1584 additions and 613 deletions

View File

@@ -1353,7 +1353,7 @@ Singleton {
id: settingsFile id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: isGreeterMode blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
watchChanges: !isGreeterMode watchChanges: !isGreeterMode

View File

@@ -79,7 +79,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Automatically lock after" text: "Automatically lock after"
options: timeoutOptions options: timeoutOptions
@@ -116,7 +115,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Turn off monitors after" text: "Turn off monitors after"
options: timeoutOptions options: timeoutOptions
@@ -153,7 +151,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Suspend system after" text: "Suspend system after"
options: timeoutOptions options: timeoutOptions
@@ -190,7 +187,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Hibernate system after" text: "Hibernate system after"
options: timeoutOptions options: timeoutOptions
visible: SessionService.hibernateSupported visible: SessionService.hibernateSupported

View File

@@ -271,18 +271,17 @@ DankPopout {
spacing: Theme.spacingM spacing: Theme.spacingM
visible: searchField.text.length === 0 visible: searchField.text.length === 0
leftPadding: Theme.spacingS leftPadding: Theme.spacingS
topPadding: Theme.spacingXS
Item { Rectangle {
width: 200 width: 180
height: 36 height: 40
radius: Theme.cornerRadius
color: "transparent"
DankDropdown { DankDropdown {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
text: "" text: ""
dropdownWidth: 180
currentValue: appLauncher.selectedCategory currentValue: appLauncher.selectedCategory
options: appLauncher.categories options: appLauncher.categories
optionIcons: appLauncher.categoryIcons optionIcons: appLauncher.categoryIcons
@@ -293,7 +292,7 @@ DankPopout {
} }
Item { Item {
width: parent.width - 310 width: parent.width - 290
height: 1 height: 1
} }

View File

@@ -397,12 +397,10 @@ Item {
// Inject PluginService for plugin widgets // Inject PluginService for plugin widgets
if (item.pluginService !== undefined) { if (item.pluginService !== undefined) {
console.log("CenterSection: Injecting PluginService into plugin widget:", model.widgetId) if (item.pluginId !== undefined) {
item.pluginService = PluginService item.pluginId = model.widgetId
if (item.loadTimezones) {
console.log("CenterSection: Calling loadTimezones for widget:", model.widgetId)
item.loadTimezones()
} }
item.pluginService = PluginService
} }
layoutTimer.restart() layoutTimer.restart()

View File

@@ -99,17 +99,11 @@ Item {
target: PluginService target: PluginService
function onPluginLoaded(pluginId) { function onPluginLoaded(pluginId) {
console.log("DankBar: Plugin loaded:", pluginId) console.log("DankBar: Plugin loaded:", pluginId)
// Force componentMap to update by triggering property change SettingsData.widgetDataChanged()
if (topBarContent) {
topBarContent.updateComponentMap()
}
} }
function onPluginUnloaded(pluginId) { function onPluginUnloaded(pluginId) {
console.log("DankBar: Plugin unloaded:", pluginId) console.log("DankBar: Plugin unloaded:", pluginId)
// Force componentMap to update by triggering property change SettingsData.widgetDataChanged()
if (topBarContent) {
topBarContent.updateComponentMap()
}
} }
} }
@@ -333,9 +327,10 @@ Item {
bottom: barWindow.isVertical ? parent.bottom : undefined bottom: barWindow.isVertical ? parent.bottom : undefined
} }
// Only enable mouse handling while hidden (for reveal-on-edge logic). // Only enable mouse handling while hidden (for reveal-on-edge logic).
hoverEnabled: SettingsData.dankBarAutoHide && !topBarCore.reveal readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview
hoverEnabled: SettingsData.dankBarAutoHide && !topBarCore.reveal && !inOverview
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
enabled: SettingsData.dankBarAutoHide && !topBarCore.reveal enabled: SettingsData.dankBarAutoHide && !topBarCore.reveal && !inOverview
Item { Item {
id: topBarContainer id: topBarContainer

View File

@@ -273,7 +273,7 @@ DankPopout {
height: 32 height: 32
radius: 16 radius: 16
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent" color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent

View File

@@ -79,18 +79,15 @@ Loader {
} }
if (item.pluginService !== undefined) { if (item.pluginService !== undefined) {
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId) if (item.pluginId !== undefined) {
item.pluginService = PluginService item.pluginId = widgetId
if (item.loadTimezones) {
console.log("WidgetHost: Calling loadTimezones for widget:", widgetId)
item.loadTimezones()
} }
item.pluginService = PluginService
} }
} }
} }
function getWidgetComponent(widgetId, components) { function getWidgetComponent(widgetId, components) {
// Build component map for built-in widgets
const componentMap = { const componentMap = {
"launcherButton": components.launcherButtonComponent, "launcherButton": components.launcherButtonComponent,
"workspaceSwitcher": components.workspaceSwitcherComponent, "workspaceSwitcher": components.workspaceSwitcherComponent,
@@ -121,12 +118,10 @@ Loader {
"systemUpdate": components.systemUpdateComponent "systemUpdate": components.systemUpdateComponent
} }
// Check for built-in component first
if (componentMap[widgetId]) { if (componentMap[widgetId]) {
return componentMap[widgetId] return componentMap[widgetId]
} }
// Check for plugin component
let pluginMap = PluginService.getWidgetComponents() let pluginMap = PluginService.getWidgetComponents()
return pluginMap[widgetId] || null return pluginMap[widgetId] || null
} }

View File

@@ -19,6 +19,9 @@ Item {
property string screenName: "" property string screenName: ""
property string randomFact: "" property string randomFact: ""
property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0
property bool isPrimaryScreen: { property bool isPrimaryScreen: {
if (!Qt.application.screens || Qt.application.screens.length === 0) if (!Qt.application.screens || Qt.application.screens.length === 0)
return true return true
@@ -61,6 +64,11 @@ Item {
sessionListProc.running = true sessionListProc.running = true
applyLastSuccessfulUser() applyLastSuccessfulUser()
} }
if (CompositorService.isHyprland) {
updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
@@ -75,6 +83,56 @@ Item {
Component.onDestruction: { Component.onDestruction: {
WeatherService.removeRef() WeatherService.removeRef()
if (CompositorService.isHyprland) {
hyprlandLayoutUpdateTimer.stop()
}
}
function updateHyprlandLayout() {
if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
}
}
Process {
id: hyprlandLayoutProcess
running: false
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard && mainKeyboard.active_keymap) {
const parts = mainKeyboard.active_keymap.split(" ")
if (parts.length > 0) {
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase()
} else {
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase()
}
} else {
hyprlandCurrentLayout = ""
}
if (mainKeyboard && mainKeyboard.layout_names) {
hyprlandLayoutCount = mainKeyboard.layout_names.length
} else {
hyprlandLayoutCount = 0
}
} catch (e) {
hyprlandCurrentLayout = ""
hyprlandLayoutCount = 0
}
}
}
}
Timer {
id: hyprlandLayoutUpdateTimer
interval: 1000
running: false
repeat: true
onTriggered: updateHyprlandLayout()
} }
// ! This was for development and testing, just leaving so people can see how I did it. // ! This was for development and testing, just leaving so people can see how I did it.
@@ -438,6 +496,8 @@ Item {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 20 Layout.preferredHeight: 20
Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS
text: { text: {
if (GreeterState.pamState === "error") if (GreeterState.pamState === "error")
return "Authentication error - try again" return "Authentication error - try again"
@@ -448,7 +508,6 @@ Item {
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
visible: GreeterState.pamState !== ""
opacity: GreeterState.pamState !== "" ? 1 : 0 opacity: GreeterState.pamState !== "" ? 1 : 0
Behavior on opacity { Behavior on opacity {
@@ -461,7 +520,7 @@ Item {
Rectangle { Rectangle {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Theme.spacingS Layout.topMargin: 0
Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2 Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2
Layout.preferredHeight: 40 Layout.preferredHeight: 40
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -516,6 +575,91 @@ Item {
anchors.margins: Theme.spacingXL anchors.margins: Theme.spacingXL
spacing: Theme.spacingL spacing: Theme.spacingL
Item {
width: keyboardLayoutRow.width
height: keyboardLayoutRow.height
anchors.verticalCenter: parent.verticalCenter
visible: {
if (CompositorService.isNiri) {
return NiriService.keyboardLayoutNames.length > 1
} else if (CompositorService.isHyprland) {
return hyprlandLayoutCount > 1
}
return false
}
Row {
id: keyboardLayoutRow
spacing: 4
Item {
width: Theme.iconSize
height: Theme.iconSize
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: "white"
anchors.centerIn: parent
}
}
Item {
width: childrenRect.width
height: Theme.iconSize
StyledText {
text: {
if (CompositorService.isNiri) {
const layout = NiriService.getCurrentKeyboardLayoutName()
if (!layout) return ""
const parts = layout.split(" ")
if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase()
}
return layout.substring(0, 2).toUpperCase()
} else if (CompositorService.isHyprland) {
return hyprlandCurrentLayout
}
return ""
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Light
color: "white"
anchors.verticalCenter: parent.verticalCenter
}
}
}
MouseArea {
id: keyboardLayoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout()
} else if (CompositorService.isHyprland) {
Quickshell.execDetached([
"hyprctl",
"switchxkblayout",
hyprlandKeyboard,
"next"
])
updateHyprlandLayout()
}
}
}
}
Rectangle {
width: 1
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: MprisController.activePlayer
}
Row { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: MprisController.activePlayer visible: MprisController.activePlayer
@@ -970,7 +1114,7 @@ Item {
if (idx >= 0) { if (idx >= 0) {
GreeterState.currentSessionIndex = idx GreeterState.currentSessionIndex = idx
GreeterState.selectedSession = GreeterState.sessionExecs[idx] GreeterState.selectedSession = GreeterState.sessionExecs[idx]
GreetdMemory.setLastSessionId(GreeterState.sessionExecs[idx].split(" ")[0]) GreetdMemory.setLastSessionId(GreeterState.sessionPaths[idx])
} }
} }
} }
@@ -987,6 +1131,7 @@ Item {
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || "" property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
property int pendingParsers: 0 property int pendingParsers: 0
function finalizeSessionSelection() { function finalizeSessionSelection() {
if (GreeterState.sessionList.length === 0) { if (GreeterState.sessionList.length === 0) {
return return
@@ -997,8 +1142,8 @@ Item {
const savedSession = GreetdMemory.lastSessionId const savedSession = GreetdMemory.lastSessionId
let foundSaved = false let foundSaved = false
if (savedSession) { if (savedSession) {
for (var i = 0; i < GreeterState.sessionExecs.length; i++) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionExecs[i].toLowerCase().includes(savedSession.toLowerCase()) || GreeterState.sessionList[i].toLowerCase().includes(savedSession.toLowerCase())) { if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i GreeterState.currentSessionIndex = i
foundSaved = true foundSaved = true
break break
@@ -1059,10 +1204,13 @@ Item {
if (!GreeterState.sessionList.includes(name)) { if (!GreeterState.sessionList.includes(name)) {
let newList = GreeterState.sessionList.slice() let newList = GreeterState.sessionList.slice()
let newExecs = GreeterState.sessionExecs.slice() let newExecs = GreeterState.sessionExecs.slice()
let newPaths = GreeterState.sessionPaths.slice()
newList.push(name) newList.push(name)
newExecs.push(exec) newExecs.push(exec)
newPaths.push(desktopPath)
GreeterState.sessionList = newList GreeterState.sessionList = newList
GreeterState.sessionExecs = newExecs GreeterState.sessionExecs = newExecs
GreeterState.sessionPaths = newPaths
root.sessionCount = GreeterState.sessionList.length root.sessionCount = GreeterState.sessionList.length
} }
} }
@@ -1098,17 +1246,16 @@ Item {
GreeterState.unlocking = true GreeterState.unlocking = true
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex] const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]
if (sessionCmd) { if (sessionCmd) {
GreetdMemory.setLastSessionId(sessionCmd.split(" ")[0]) GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex])
GreetdMemory.setLastSuccessfulUser(GreeterState.username) GreetdMemory.setLastSuccessfulUser(GreeterState.username)
Greetd.launch(sessionCmd.split(" "), [], true) Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"], true)
} }
} }
function onAuthFailure(message) { function onAuthFailure(message) {
GreeterState.pamState = "fail" GreeterState.pamState = "fail"
GreeterState.reset() GreeterState.passwordBuffer = ""
inputField.text = "" inputField.text = ""
PortalService.profileImage = ""
placeholderDelay.restart() placeholderDelay.restart()
} }

View File

@@ -16,6 +16,7 @@ Singleton {
property var sessionList: [] property var sessionList: []
property var sessionExecs: [] property var sessionExecs: []
property var sessionPaths: []
property int currentSessionIndex: 0 property int currentSessionIndex: 0
function reset() { function reset() {

View File

@@ -18,7 +18,7 @@ Manual installation:
1. Install `greetd` (in most distro's standard repositories) 1. Install `greetd` (in most distro's standard repositories)
2. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd` 2. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd`
- niri if you want to run the greeter under niri, hypr if you want to run the greeter under Hyprland - niri if you want to run the greeter under niri, hypr if you want to run the greeter under Hyprland
3. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/etc/greetd/start-dms.sh` 3. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
4. Edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` and replace `_DMS_PATH_` with the absolute path to dms, e.g. `/home/joecool/.config/quickshell/dms` 4. Edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` and replace `_DMS_PATH_` with the absolute path to dms, e.g. `/home/joecool/.config/quickshell/dms`
5. Edit or create `/etc/greetd/config.toml` 5. Edit or create `/etc/greetd/config.toml`
```toml ```toml
@@ -38,7 +38,7 @@ vt = 1
# in the `video` group. # in the `video` group.
user = "greeter" user = "greeter"
command = "/etc/greetd/start-dms.sh"% command = "/usr/local/bin/start-dms-greetd.sh"
``` ```
Enable the greeter with `sudo systemctl enable greetd` Enable the greeter with `sudo systemctl enable greetd`

View File

@@ -1,6 +1,3 @@
env = DMS_RUN_GREETER,1 env = DMS_RUN_GREETER,1
env = QT_QPA_PLATFORM,wayland
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
env = EGL_PLATFORM,gbm
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit" exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"

View File

@@ -4,8 +4,6 @@ hotkey-overlay {
environment { environment {
DMS_RUN_GREETER "1" DMS_RUN_GREETER "1"
QT_QPA_PLATFORM "wayland"
QT_WAYLAND_DISABLE_WINDOWDECORATION "1"
} }
spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation" spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation"

View File

@@ -1,3 +1,8 @@
#!/bin/sh #!/bin/sh
EGL_PLATFORM=gbm Hyprland -c /etc/greetd/dms-hypr.conf export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
exec Hyprland -c /etc/greetd/dms-hypr.conf

View File

@@ -1,3 +1,8 @@
#!/bin/sh #!/bin/sh
EGL_PLATFORM=gbm niri -c /etc/greetd/dms-niri.kdl export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
exec niri -c /etc/greetd/dms-niri.kdl

View File

@@ -20,6 +20,9 @@ Item {
property bool unlocking: false property bool unlocking: false
property string pamState: "" property string pamState: ""
property string randomFact: "" property string randomFact: ""
property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0
signal unlockRequested signal unlockRequested
@@ -55,6 +58,11 @@ Item {
WeatherService.addRef() WeatherService.addRef()
UserInfoService.refreshUserInfo() UserInfoService.refreshUserInfo()
if (CompositorService.isHyprland) {
updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
} }
onDemoModeChanged: { onDemoModeChanged: {
if (demoMode) { if (demoMode) {
@@ -63,6 +71,56 @@ Item {
} }
Component.onDestruction: { Component.onDestruction: {
WeatherService.removeRef() WeatherService.removeRef()
if (CompositorService.isHyprland) {
hyprlandLayoutUpdateTimer.stop()
}
}
function updateHyprlandLayout() {
if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
}
}
Process {
id: hyprlandLayoutProcess
running: false
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard && mainKeyboard.active_keymap) {
const parts = mainKeyboard.active_keymap.split(" ")
if (parts.length > 0) {
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase()
} else {
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase()
}
} else {
hyprlandCurrentLayout = ""
}
if (mainKeyboard && mainKeyboard.layout_names) {
hyprlandLayoutCount = mainKeyboard.layout_names.length
} else {
hyprlandLayoutCount = 0
}
} catch (e) {
hyprlandCurrentLayout = ""
hyprlandLayoutCount = 0
}
}
}
}
Timer {
id: hyprlandLayoutUpdateTimer
interval: 1000
running: false
repeat: true
onTriggered: updateHyprlandLayout()
} }
Loader { Loader {
@@ -520,7 +578,7 @@ Item {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: root.pamState ? 20 : 0 Layout.preferredHeight: 20
text: { text: {
if (root.pamState === "error") { if (root.pamState === "error") {
return "Authentication error - try again" return "Authentication error - try again"
@@ -536,7 +594,6 @@ Item {
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
visible: root.pamState !== ""
opacity: root.pamState !== "" ? 1 : 0 opacity: root.pamState !== "" ? 1 : 0
Behavior on opacity { Behavior on opacity {
@@ -545,13 +602,6 @@ Item {
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
} }
@@ -572,6 +622,92 @@ Item {
anchors.margins: Theme.spacingXL anchors.margins: Theme.spacingXL
spacing: Theme.spacingL spacing: Theme.spacingL
Item {
width: keyboardLayoutRow.width
height: keyboardLayoutRow.height
anchors.verticalCenter: parent.verticalCenter
visible: {
if (CompositorService.isNiri) {
return NiriService.keyboardLayoutNames.length > 1
} else if (CompositorService.isHyprland) {
return hyprlandLayoutCount > 1
}
return false
}
Row {
id: keyboardLayoutRow
spacing: 4
Item {
width: Theme.iconSize
height: Theme.iconSize
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: "white"
anchors.centerIn: parent
}
}
Item {
width: childrenRect.width
height: Theme.iconSize
StyledText {
text: {
if (CompositorService.isNiri) {
const layout = NiriService.getCurrentKeyboardLayoutName()
if (!layout) return ""
const parts = layout.split(" ")
if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase()
}
return layout.substring(0, 2).toUpperCase()
} else if (CompositorService.isHyprland) {
return hyprlandCurrentLayout
}
return ""
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Light
color: "white"
anchors.verticalCenter: parent.verticalCenter
}
}
}
MouseArea {
id: keyboardLayoutArea
anchors.fill: parent
enabled: !demoMode
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout()
} else if (CompositorService.isHyprland) {
Quickshell.execDetached([
"hyprctl",
"switchxkblayout",
hyprlandKeyboard,
"next"
])
updateHyprlandLayout()
}
}
}
}
Rectangle {
width: 1
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: MprisController.activePlayer
}
Row { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: MprisController.activePlayer visible: MprisController.activePlayer
@@ -1036,6 +1172,8 @@ Item {
return return
} }
console.log("Authentication failed:", res) console.log("Authentication failed:", res)
passwordField.text = ""
root.passwordBuffer = ""
if (res === PamResult.Error) if (res === PamResult.Error)
root.pamState = "error" root.pamState = "error"
else if (res === PamResult.MaxTries) else if (res === PamResult.MaxTries)

View File

@@ -157,7 +157,6 @@ Rectangle {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Low Priority" text: "Low Priority"
description: "Timeout for low priority notifications" description: "Timeout for low priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow) currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
@@ -173,7 +172,6 @@ Rectangle {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Normal Priority" text: "Normal Priority"
description: "Timeout for normal priority notifications" description: "Timeout for normal priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal) currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
@@ -189,7 +187,6 @@ Rectangle {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Critical Priority" text: "Critical Priority"
description: "Timeout for critical priority notifications" description: "Timeout for critical priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical) currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)

View File

@@ -8,7 +8,8 @@ Column {
required property string settingKey required property string settingKey
required property string label required property string label
property string description: "" property string description: ""
property var items: [] property var defaultValue: []
property var items: defaultValue
property Component delegate: null property Component delegate: null
width: parent.width width: parent.width
@@ -17,7 +18,7 @@ Column {
Component.onCompleted: { Component.onCompleted: {
const settings = findSettings() const settings = findSettings()
if (settings) { if (settings) {
items = settings.loadValue(settingKey, []) items = settings.loadValue(settingKey, defaultValue)
} }
} }

View File

@@ -9,7 +9,8 @@ Column {
required property string label required property string label
property string description: "" property string description: ""
property var fields: [] property var fields: []
property var items: [] property var defaultValue: []
property var items: defaultValue
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -17,7 +18,7 @@ Column {
Component.onCompleted: { Component.onCompleted: {
const settings = findSettings() const settings = findSettings()
if (settings) { if (settings) {
items = settings.loadValue(settingKey, []) items = settings.loadValue(settingKey, defaultValue)
} }
} }

View File

@@ -11,6 +11,8 @@ Item {
property var parentScreen: null property var parentScreen: null
property real widgetThickness: 30 property real widgetThickness: 30
property real barThickness: 48 property real barThickness: 48
property string pluginId: ""
property var pluginService: null
property Component horizontalBarPill: null property Component horizontalBarPill: null
property Component verticalBarPill: null property Component verticalBarPill: null
@@ -18,11 +20,42 @@ Item {
property real popoutWidth: 400 property real popoutWidth: 400
property real popoutHeight: 400 property real popoutHeight: 400
property var pluginData: ({})
readonly property bool isVertical: axis?.isVertical ?? false readonly property bool isVertical: axis?.isVertical ?? false
readonly property bool hasHorizontalPill: horizontalBarPill !== null readonly property bool hasHorizontalPill: horizontalBarPill !== null
readonly property bool hasVerticalPill: verticalBarPill !== null readonly property bool hasVerticalPill: verticalBarPill !== null
readonly property bool hasPopout: popoutContent !== null readonly property bool hasPopout: popoutContent !== null
Component.onCompleted: {
loadPluginData()
}
onPluginServiceChanged: {
loadPluginData()
}
onPluginIdChanged: {
loadPluginData()
}
Connections {
target: pluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === pluginId) {
loadPluginData()
}
}
}
function loadPluginData() {
if (!pluginService || !pluginId) {
pluginData = {}
return
}
pluginData = SettingsData.getPluginSettingsForPlugin(pluginId)
}
width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0) width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0)
height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0) height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0)
@@ -60,6 +93,12 @@ Item {
} }
} }
function closePopout() {
if (pluginPopout) {
pluginPopout.close()
}
}
PluginPopout { PluginPopout {
id: pluginPopout id: pluginPopout
contentWidth: root.popoutWidth contentWidth: root.popoutWidth

View File

@@ -62,53 +62,24 @@ DankPopout {
Column { Column {
id: popoutColumn id: popoutColumn
width: parent.width - Theme.spacingL * 2 width: parent.width - Theme.spacingS * 2
anchors.left: parent.left height: parent.height - Theme.spacingS * 2
anchors.top: parent.top x: Theme.spacingS
anchors.margins: Theme.spacingL y: Theme.spacingS
spacing: Theme.spacingL spacing: Theme.spacingS
Row {
width: parent.width
height: 32
visible: closeButton.visible
Item {
width: parent.width - 32
height: 32
}
Rectangle {
id: closeButton
width: 32
height: 32
radius: 16
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
visible: true
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.close()
}
}
}
}
Loader { Loader {
id: popoutContent id: popoutContent
width: parent.width width: parent.width
sourceComponent: root.pluginContent sourceComponent: root.pluginContent
onLoaded: {
if (item && "closePopout" in item) {
item.closePopout = function() {
root.close()
}
}
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -9,12 +10,35 @@ Item {
property var pluginService: null property var pluginService: null
default property alias content: settingsColumn.children default property alias content: settingsColumn.children
implicitHeight: settingsColumn.implicitHeight signal settingChanged()
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
height: implicitHeight height: implicitHeight
readonly property bool hasPermission: pluginService && pluginService.hasPermission ? pluginService.hasPermission(pluginId, "settings_write") : true
onPluginServiceChanged: {
if (pluginService) {
for (let i = 0; i < settingsColumn.children.length; i++) {
const child = settingsColumn.children[i]
if (child.loadValue) {
child.loadValue()
}
}
}
}
function saveValue(key, value) { function saveValue(key, value) {
if (pluginService && pluginService.savePluginData) { if (!pluginService) {
return
}
if (!hasPermission) {
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission")
return
}
if (pluginService.savePluginData) {
pluginService.savePluginData(pluginId, key, value) pluginService.savePluginData(pluginId, key, value)
settingChanged()
} }
} }
@@ -25,8 +49,21 @@ Item {
return defaultValue return defaultValue
} }
StyledText {
id: errorText
visible: pluginService && !root.hasPermission
anchors.fill: parent
text: "This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json"
color: Theme.error
font.pixelSize: Theme.fontSizeMedium
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Column { Column {
id: settingsColumn id: settingsColumn
visible: root.hasPermission
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
} }

View File

@@ -0,0 +1,76 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string headerText: ""
property string detailsText: ""
property bool showCloseButton: false
property var closePopout: null
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
readonly property int detailsHeight: popoutDetails.visible ? popoutDetails.implicitHeight : 0
spacing: 0
Item {
id: popoutHeader
width: parent.width
height: 40
visible: headerText.length > 0
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.headerText
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
Rectangle {
id: closeButton
width: 32
height: 32
radius: 16
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
visible: root.showCloseButton
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (root.closePopout) {
root.closePopout()
}
}
}
}
}
StyledText {
id: popoutDetails
width: parent.width
leftPadding: Theme.spacingS
bottomPadding: Theme.spacingS
text: root.detailsText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: detailsText.length > 0
wrapMode: Text.WordWrap
}
}

View File

@@ -15,6 +15,17 @@ Column {
width: parent.width width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
function loadValue() {
const settings = findSettings()
if (settings && settings.pluginService) {
value = settings.loadValue(settingKey, defaultValue)
}
}
Component.onCompleted: {
loadValue()
}
readonly property var optionLabels: { readonly property var optionLabels: {
const labels = [] const labels = []
for (let i = 0; i < options.length; i++) { for (let i = 0; i < options.length; i++) {
@@ -49,13 +60,6 @@ Column {
return map return map
} }
Component.onCompleted: {
const settings = findSettings()
if (settings) {
value = settings.loadValue(settingKey, defaultValue)
}
}
onValueChanged: { onValueChanged: {
const settings = findSettings() const settings = findSettings()
if (settings) { if (settings) {
@@ -74,40 +78,14 @@ Column {
return null return null
} }
Row { DankDropdown {
width: parent.width width: parent.width
spacing: Theme.spacingM text: root.label
description: root.description
Column { currentValue: root.valueToLabel[root.value] || root.value
width: parent.width * 0.4 options: root.optionLabels
spacing: Theme.spacingXS onValueChanged: newValue => {
anchors.verticalCenter: parent.verticalCenter root.value = root.labelToValue[newValue] || newValue
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
}
DankDropdown {
width: parent.width * 0.6 - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
currentValue: root.valueToLabel[root.value] || root.value
options: root.optionLabels
onValueChanged: newValue => {
root.value = root.labelToValue[newValue] || newValue
}
} }
} }
} }

View File

@@ -0,0 +1,81 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
required property string settingKey
required property string label
property string description: ""
property int defaultValue: 0
property int value: defaultValue
property int minimum: 0
property int maximum: 100
property string leftIcon: ""
property string rightIcon: ""
property string unit: ""
width: parent.width
spacing: Theme.spacingS
function loadValue() {
const settings = findSettings()
if (settings && settings.pluginService) {
value = settings.loadValue(settingKey, defaultValue)
}
}
Component.onCompleted: {
loadValue()
}
onValueChanged: {
const settings = findSettings()
if (settings) {
settings.saveValue(settingKey, value)
}
}
function findSettings() {
let item = parent
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined) {
return item
}
item = item.parent
}
return null
}
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
DankSlider {
width: parent.width
value: root.value
minimum: root.minimum
maximum: root.maximum
leftIcon: root.leftIcon
rightIcon: root.rightIcon
unit: root.unit
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHighest
onSliderValueChanged: newValue => {
root.value = newValue
}
}
}

View File

@@ -8,6 +8,15 @@ import qs.Widgets
Item { Item {
id: dankBarTab id: dankBarTab
function getWidgetsForPopup() {
return baseWidgetDefinitions.filter(widget => {
if (widget.warning && widget.warning.includes("Plugin is disabled")) {
return false
}
return true
})
}
property var baseWidgetDefinitions: { property var baseWidgetDefinitions: {
var coreWidgets = [{ var coreWidgets = [{
"id": "launcherButton", "id": "launcherButton",
@@ -179,16 +188,18 @@ Item {
"enabled": SystemUpdateService.distributionSupported "enabled": SystemUpdateService.distributionSupported
}] }]
// Add plugin widgets dynamically // Add all available plugins (loaded and unloaded)
var loadedPlugins = PluginService.getLoadedPlugins() var allPlugins = PluginService.getAvailablePlugins()
for (var i = 0; i < loadedPlugins.length; i++) { for (var i = 0; i < allPlugins.length; i++) {
var plugin = loadedPlugins[i] var plugin = allPlugins[i]
var isLoaded = PluginService.isPluginLoaded(plugin.id)
coreWidgets.push({ coreWidgets.push({
"id": plugin.id, "id": plugin.id,
"text": plugin.name, "text": plugin.name,
"description": plugin.description || "Plugin widget", "description": plugin.description || "Plugin widget",
"icon": plugin.icon || "extension", "icon": plugin.icon || "extension",
"enabled": true "enabled": isLoaded,
"warning": !isLoaded ? "Plugin is disabled - enable in Plugins settings to use" : undefined
}) })
} }
@@ -1146,7 +1157,7 @@ Item {
} }
onAddWidget: sectionId => { onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets widgetSelectionPopup.allWidgets
= dankBarTab.baseWidgetDefinitions = dankBarTab.getWidgetsForPopup()
widgetSelectionPopup.targetSection = sectionId widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen() widgetSelectionPopup.safeOpen()
} }
@@ -1218,7 +1229,7 @@ Item {
} }
onAddWidget: sectionId => { onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets widgetSelectionPopup.allWidgets
= dankBarTab.baseWidgetDefinitions = dankBarTab.getWidgetsForPopup()
widgetSelectionPopup.targetSection = sectionId widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen() widgetSelectionPopup.safeOpen()
} }
@@ -1290,7 +1301,7 @@ Item {
} }
onAddWidget: sectionId => { onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets widgetSelectionPopup.allWidgets
= dankBarTab.baseWidgetDefinitions = dankBarTab.getWidgetsForPopup()
widgetSelectionPopup.targetSection = sectionId widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen() widgetSelectionPopup.safeOpen()
} }

View File

@@ -520,7 +520,6 @@ Item {
DankDropdown { DankDropdown {
id: monitorDropdown id: monitorDropdown
width: parent.width - parent.leftPadding
text: "Monitor" text: "Monitor"
description: "Select monitor to configure wallpaper" description: "Select monitor to configure wallpaper"
currentValue: selectedMonitorName || "No monitors" currentValue: selectedMonitorName || "No monitors"
@@ -678,7 +677,6 @@ Item {
property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"] property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"]
property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200] property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200]
width: parent.width - parent.leftPadding
visible: { visible: {
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval" return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval"
@@ -833,7 +831,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Transition Effect" text: "Transition Effect"
description: "Visual effect used when wallpaper changes" description: "Visual effect used when wallpaper changes"
currentValue: { currentValue: {
@@ -851,8 +848,6 @@ Item {
width: parent.width width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
visible: SessionData.wallpaperTransition === "random" visible: SessionData.wallpaperTransition === "random"
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
StyledText { StyledText {
text: "Include Transitions" text: "Include Transitions"
@@ -866,12 +861,12 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width - parent.leftPadding - parent.rightPadding width: parent.width
} }
DankButtonGroup { DankButtonGroup {
id: transitionGroup id: transitionGroup
width: parent.width - parent.leftPadding - parent.rightPadding width: parent.width
selectionMode: "multi" selectionMode: "multi"
model: SessionData.availableWallpaperTransitions.filter(t => t !== "none") model: SessionData.availableWallpaperTransitions.filter(t => t !== "none")
initialSelection: SessionData.includedTransitions initialSelection: SessionData.includedTransitions
@@ -959,7 +954,6 @@ Item {
DankDropdown { DankDropdown {
id: personalizationMatugenPaletteDropdown id: personalizationMatugenPaletteDropdown
width: parent.width
text: "Matugen Palette" text: "Matugen Palette"
description: "Select the palette algorithm used for wallpaper-based colors" description: "Select the palette algorithm used for wallpaper-based colors"
options: Theme.availableMatugenSchemes.map(function (option) { return option.label }) options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
@@ -1075,7 +1069,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Temperature" text: "Temperature"
description: "Color temperature for night mode" description: "Color temperature for night mode"
currentValue: SessionData.nightModeTemperature + "K" currentValue: SessionData.nightModeTemperature + "K"
@@ -1424,7 +1417,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Popup Position" text: "Popup Position"
description: "Choose where notification popups appear on screen" description: "Choose where notification popups appear on screen"
currentValue: { currentValue: {
@@ -1506,7 +1498,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Font Family" text: "Font Family"
description: "Select system font family" description: "Select system font family"
currentValue: { currentValue: {
@@ -1528,7 +1519,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Font Weight" text: "Font Weight"
description: "Select font weight" description: "Select font weight"
currentValue: { currentValue: {
@@ -1595,7 +1585,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
text: "Monospace Font" text: "Monospace Font"
description: "Select monospace font for process list and technical displays" description: "Select monospace font for process list and technical displays"
currentValue: { currentValue: {

View File

@@ -10,14 +10,6 @@ Item {
property string expandedPluginId: "" property string expandedPluginId: ""
Component.onCompleted: {
console.log("PluginsTab: Component completed")
console.log("PluginsTab: PluginService available:", typeof PluginService !== "undefined")
if (typeof PluginService !== "undefined") {
console.log("PluginsTab: Available plugins:", Object.keys(PluginService.availablePlugins).length)
console.log("PluginsTab: Plugin directory:", PluginService.pluginDirectory)
}
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
@@ -186,9 +178,6 @@ Item {
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== "" property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
property bool isExpanded: pluginsTab.expandedPluginId === pluginId property bool isExpanded: pluginsTab.expandedPluginId === pluginId
onIsExpandedChanged: {
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
}
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh) color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh)
border.width: 0 border.width: 0
@@ -200,15 +189,8 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: { onClicked: {
console.log("Plugin clicked:", pluginDelegate.pluginId, "hasSettings:", pluginDelegate.hasSettings, "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
if (pluginDelegate.hasSettings) { if (pluginDelegate.hasSettings) {
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) { pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
console.log("Collapsing plugin:", pluginDelegate.pluginId)
pluginsTab.expandedPluginId = ""
} else {
console.log("Expanding plugin:", pluginDelegate.pluginId)
pluginsTab.expandedPluginId = pluginDelegate.pluginId
}
} }
} }
} }
@@ -234,7 +216,7 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - pluginToggle.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM - toggleRow.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -267,27 +249,69 @@ Item {
} }
} }
DankToggle { Row {
id: pluginToggle id: toggleRow
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId) spacing: Theme.spacingXS
onToggled: (isChecked) => {
if (isChecked) { Rectangle {
if (PluginService.enablePlugin(pluginDelegate.pluginId)) { width: 28
ToastService.showInfo("Plugin enabled: " + pluginDelegate.pluginName) height: 28
} else { radius: 14
ToastService.showError("Failed to enable plugin: " + pluginDelegate.pluginName) color: reloadArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
checked = false visible: PluginService.isPluginLoaded(pluginDelegate.pluginId)
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 16
color: reloadArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: reloadArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const currentPluginId = pluginDelegate.pluginId
const currentPluginName = pluginDelegate.pluginName
pluginsTab.isReloading = true
if (PluginService.reloadPlugin(currentPluginId)) {
ToastService.showInfo("Plugin reloaded: " + currentPluginName)
} else {
ToastService.showError("Failed to reload plugin: " + currentPluginName)
pluginsTab.isReloading = false
}
} }
} else { }
if (PluginService.disablePlugin(pluginDelegate.pluginId)) { }
ToastService.showInfo("Plugin disabled: " + pluginDelegate.pluginName)
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) { DankToggle {
pluginsTab.expandedPluginId = "" id: pluginToggle
anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
onToggled: isChecked => {
const currentPluginId = pluginDelegate.pluginId
const currentPluginName = pluginDelegate.pluginName
if (isChecked) {
if (PluginService.enablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin enabled: " + currentPluginName)
} else {
ToastService.showError("Failed to enable plugin: " + currentPluginName)
checked = false
} }
} else { } else {
ToastService.showError("Failed to disable plugin: " + pluginDelegate.pluginName) if (PluginService.disablePlugin(currentPluginId)) {
checked = true ToastService.showInfo("Plugin disabled: " + currentPluginName)
if (pluginDelegate.isExpanded) {
pluginsTab.expandedPluginId = ""
}
} else {
ToastService.showError("Failed to disable plugin: " + currentPluginName)
checked = true
}
} }
} }
} }
@@ -355,15 +379,8 @@ Item {
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId) active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
asynchronous: false asynchronous: false
onActiveChanged: {
console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId,
"isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings,
"isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
}
source: { source: {
if (active && pluginDelegate.pluginSettingsPath) { if (active && pluginDelegate.pluginSettingsPath) {
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
var path = pluginDelegate.pluginSettingsPath var path = pluginDelegate.pluginSettingsPath
if (!path.startsWith("file://")) { if (!path.startsWith("file://")) {
path = "file://" + path path = "file://" + path
@@ -373,37 +390,9 @@ Item {
return "" return ""
} }
onStatusChanged: {
console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId)
if (status === Loader.Error) {
console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath)
} else if (status === Loader.Ready) {
console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId)
}
}
onLoaded: { onLoaded: {
if (item) { if (item && typeof PluginService !== "undefined") {
console.log("Plugin settings loaded for:", pluginDelegate.pluginId) item.pluginService = PluginService
if (typeof PluginService !== "undefined") {
console.log("Making PluginService available to plugin settings")
console.log("PluginService functions available:",
"savePluginData" in PluginService,
"loadPluginData" in PluginService)
item.pluginService = PluginService
console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null)
} else {
console.error("PluginService not available in PluginsTab context")
}
if (item.loadTimezones) {
console.log("Calling loadTimezones for WorldClock plugin")
item.loadTimezones()
}
if (item.initializeSettings) {
item.initializeSettings()
}
} }
} }
} }
@@ -437,14 +426,19 @@ Item {
} }
} }
property bool isReloading: false
Connections { Connections {
target: PluginService target: PluginService
function onPluginLoaded() { function onPluginLoaded() {
pluginRepeater.model = PluginService.getAvailablePlugins() pluginRepeater.model = PluginService.getAvailablePlugins()
if (isReloading) {
isReloading = false
}
} }
function onPluginUnloaded() { function onPluginUnloaded() {
pluginRepeater.model = PluginService.getAvailablePlugins() pluginRepeater.model = PluginService.getAvailablePlugins()
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) { if (!isReloading && pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
pluginsTab.expandedPluginId = "" pluginsTab.expandedPluginId = ""
} }
} }

View File

@@ -651,7 +651,6 @@ Item {
DankDropdown { DankDropdown {
id: matugenPaletteDropdown id: matugenPaletteDropdown
width: parent.width
text: "Matugen Palette" text: "Matugen Palette"
description: "Select the palette algorithm used for wallpaper-based colors" description: "Select the palette algorithm used for wallpaper-based colors"
options: Theme.availableMatugenSchemes.map(function (option) { return option.label }) options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
@@ -993,7 +992,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width - Theme.iconSize - Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Icon Theme" text: "Icon Theme"
description: "DankShell & System Icons\n(requires restart)" description: "DankShell & System Icons\n(requires restart)"

View File

@@ -121,7 +121,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
height: 50 height: 50
text: "Top Bar Format" text: "Top Bar Format"
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d")) description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
@@ -185,7 +184,6 @@ Item {
} }
DankDropdown { DankDropdown {
width: parent.width
height: 50 height: 50
text: "Lock Screen Format" text: "Lock Screen Format"
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat)) description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))

View File

@@ -142,13 +142,14 @@ Column {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Item { Item {
width: 120 width: 60
height: 32 height: 32
visible: modelData.id === "gpuTemp" visible: modelData.id === "gpuTemp"
DankDropdown { DankDropdown {
id: gpuDropdown id: gpuDropdown
anchors.fill: parent anchors.fill: parent
popupWidth: -1
currentValue: { currentValue: {
var selectedIndex = modelData.selectedGpuIndex var selectedIndex = modelData.selectedGpuIndex
!== undefined ? modelData.selectedGpuIndex : 0 !== undefined ? modelData.selectedGpuIndex : 0
@@ -223,12 +224,7 @@ Column {
Item { Item {
width: 32 width: 32
height: 32 height: 32
visible: (modelData.warning !== undefined visible: modelData.warning !== undefined && modelData.warning !== ""
&& modelData.warning !== "")
&& (modelData.id === "cpuUsage"
|| modelData.id === "memUsage"
|| modelData.id === "cpuTemp"
|| modelData.id === "gpuTemp")
DankIcon { DankIcon {
name: "warning" name: "warning"

View File

@@ -0,0 +1,97 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "exampleEmojiPlugin"
// Header section to explain what this plugin does
StyledText {
width: parent.width
text: "Emoji Cycler Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Configure which emojis appear in your bar, how quickly they cycle, and how many show at once."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
// Dropdown to select which emoji set to use
SelectionSetting {
settingKey: "emojiSet"
label: "Emoji Set"
description: "Choose which collection of emojis to cycle through"
options: [
{label: "Happy & Sad", value: "happySad"},
{label: "Hearts", value: "hearts"},
{label: "Hand Gestures", value: "hands"},
{label: "All Mixed", value: "mixed"}
]
defaultValue: "happySad"
// Update the actual emoji array when selection changes
onValueChanged: {
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
}
const newEmojis = sets[value] || sets["happySad"]
root.saveValue("emojis", newEmojis)
}
Component.onCompleted: {
// Initialize the emojis array on first load
const currentSet = value || defaultValue
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
}
const emojis = sets[currentSet] || sets["happySad"]
root.saveValue("emojis", emojis)
}
}
// Slider to control how fast emojis cycle (in milliseconds)
SliderSetting {
settingKey: "cycleInterval"
label: "Cycle Speed"
description: "How quickly emojis rotate (in seconds)"
defaultValue: 3000
minimum: 500
maximum: 10000
unit: "ms"
leftIcon: "schedule"
}
// Slider to control max emojis shown in the bar
SliderSetting {
settingKey: "maxBarEmojis"
label: "Max Bar Emojis"
description: "Maximum number of emojis to display in the bar at once"
defaultValue: 3
minimum: 1
maximum: 8
unit: ""
rightIcon: "emoji_emotions"
}
StyledText {
width: parent.width
text: "💡 Tip: Click the emoji widget in your bar to open the emoji picker and copy any emoji to your clipboard!"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}

View File

@@ -0,0 +1,149 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
property int cycleInterval: pluginData.cycleInterval || 3000
property int maxBarEmojis: pluginData.maxBarEmojis || 3
property int currentIndex: 0
property var displayedEmojis: []
Timer {
interval: root.cycleInterval
running: true
repeat: true
onTriggered: {
if (root.enabledEmojis.length > 0) {
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length
root.updateDisplayedEmojis()
}
}
}
function updateDisplayedEmojis() {
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length)
let emojis = []
for (let i = 0; i < maxToShow; i++) {
const idx = (root.currentIndex + i) % root.enabledEmojis.length
emojis.push(root.enabledEmojis[idx])
}
root.displayedEmojis = emojis
}
Component.onCompleted: {
updateDisplayedEmojis()
}
onEnabledEmojisChanged: updateDisplayedEmojis()
onMaxBarEmojisChanged: updateDisplayedEmojis()
horizontalBarPill: Component {
Row {
id: emojiRow
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
verticalBarPill: Component {
Column {
id: emojiColumn
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
popoutContent: Component {
PopoutComponent {
id: popoutColumn
headerText: "Emoji Picker"
detailsText: "Click an emoji to copy it to clipboard"
showCloseButton: true
property var allEmojis: [
"😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
"😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
"😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔",
"🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥",
"😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮",
"🤧", "🥵", "🥶", "😶‍🌫️", "😵", "😵‍💫", "🤯", "🤠", "🥳", "😎",
"🤓", "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳",
"🥺", "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖",
"😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬",
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔",
"❤️‍🔥", "❤️‍🩹", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟",
"👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘",
"👌", "🤌", "🤏", "👈", "👉", "👆", "👇", "☝️", "✋", "🤚"
]
Item {
width: parent.width
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
DankGridView {
id: emojiGrid
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(parent.width / 50) * 50
height: parent.height
clip: true
cellWidth: 50
cellHeight: 50
model: popoutColumn.allEmojis
delegate: StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
border.width: 0
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
MouseArea {
id: emojiMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
ToastService.showInfo("Copied " + modelData + " to clipboard")
popoutColumn.closePopout()
}
}
}
}
}
}
}
popoutWidth: 400
popoutHeight: 500
}

View File

@@ -0,0 +1,56 @@
# Emoji Cycler Plugin
An example dms plugin that displays cycling emojis in your bar with an emoji picker popout.
## Features
- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar
- **Emoji Picker**: Click the widget to open a grid of 120+ emojis
- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard (uses `wl-copy`)
- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown
## Installation
1. Copy this directory to `~/.config/DankMaterialShell/plugins/ExampleEmojiPlugin`
2. Open DMS Settings → Plugins
3. Click "Scan for Plugins"
4. Enable "Emoji Cycler"
5. Add `exampleEmojiPlugin` to your DankBar widget list
## Settings
### Emoji Set
Choose from different emoji collections:
- **Happy & Sad**: Mix of emotional faces
- **Hearts**: Various colored hearts
- **Hand Gestures**: Thumbs up, peace signs, etc.
- **All Mixed**: A bit of everything
### Cycle Speed
Control how fast emojis rotate (500ms - 10000ms)
### Max Bar Emojis
How many emojis to display at once (1-8)
## Usage
**In the bar**: Watch emojis cycle through automatically
**Click the widget**: Opens emoji picker with 120+ emojis
**Click any emoji**: Copies it to clipboard and shows toast
## Requirements
- `wl-copy` (for clipboard support on Wayland)
## Example Code Highlights
This plugin demonstrates:
- Using `PluginComponent` for bar integration
- `SelectionSetting`, `SliderSetting` for configuration
- Timer-based animation
- Popout content with grid layout
- External command execution (`Quickshell.execDetached`)
- Toast notifications (`ToastService.show`)
- Dynamic settings loading/saving
Perfect template for creating your own DMS plugins!

View File

@@ -0,0 +1,14 @@
{
"id": "exampleEmojiPlugin",
"name": "Emoji Cycler",
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
"version": "1.0.0",
"author": "AvengeMedia",
"icon": "mood",
"component": "./EmojiWidget.qml",
"settings": "./EmojiSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -57,18 +57,6 @@ The manifest file defines plugin metadata and configuration:
"icon": "material_icon_name", "icon": "material_icon_name",
"component": "./YourWidget.qml", "component": "./YourWidget.qml",
"settings": "./YourSettings.qml", "settings": "./YourSettings.qml",
"dependencies": {
"libraryName": {
"url": "https://cdn.example.com/library.js",
"optional": true
}
},
"settings_schema": {
"settingKey": {
"type": "string|number|boolean|array|object",
"default": "defaultValue"
}
},
"permissions": [ "permissions": [
"settings_read", "settings_read",
"settings_write" "settings_write"
@@ -82,14 +70,20 @@ The manifest file defines plugin metadata and configuration:
- `component`: Relative path to widget QML file - `component`: Relative path to widget QML file
**Optional Fields:** **Optional Fields:**
- `description`: Short description of plugin functionality - `description`: Short description of plugin functionality (displayed in UI)
- `version`: Semantic version string - `version`: Semantic version string (displayed in UI)
- `author`: Plugin creator name - `author`: Plugin creator name (displayed in UI)
- `icon`: Material Design icon name - `icon`: Material Design icon name (displayed in UI)
- `settings`: Path to settings component - `settings`: Path to settings component (enables settings UI)
- `dependencies`: External JS libraries - `permissions`: Required capabilities (enforced by PluginSettings component)
- `settings_schema`: Configuration schema
- `permissions`: Required capabilities **Permissions:**
The plugin system enforces permissions when settings are accessed:
- `settings_read`: Required to read plugin settings (currently not enforced)
- `settings_write`: **Required** to use PluginSettings component and save settings
If your plugin includes a settings component but doesn't declare `settings_write` permission, users will see an error message instead of the settings UI.
### Widget Component ### Widget Component
@@ -127,15 +121,21 @@ PluginComponent {
// Define popout content (optional) // Define popout content (optional)
popoutContent: Component { popoutContent: Component {
Column { PopoutComponent {
width: parent.width headerText: "My Plugin"
spacing: Theme.spacingM detailsText: "Optional description text goes here"
padding: Theme.spacingM showCloseButton: true
StyledText { // Your popout content goes here
text: "Popout Content" Column {
font.pixelSize: Theme.fontSizeLarge width: parent.width
color: Theme.surfaceText spacing: Theme.spacingM
StyledText {
text: "Popout Content"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
}
} }
} }
} }
@@ -166,6 +166,42 @@ The PluginComponent automatically handles:
- Proper positioning and anchoring - Proper positioning and anchoring
- Theme integration - Theme integration
### PopoutComponent
PopoutComponent provides a consistent header/content layout for plugin popouts:
```qml
import qs.Modules.Plugins
PopoutComponent {
headerText: "Header Title" // Main header text (bold, large)
detailsText: "Description text" // Optional description (smaller, gray)
showCloseButton: true // Show X button in top-right
// Access to exposed properties for dynamic sizing
readonly property int headerHeight // Height of header area
readonly property int detailsHeight // Height of description area
// Your content here - use parent.width for full width
// Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing
DankGridView {
width: parent.width
height: parent.height
// ...
}
}
```
**PopoutComponent Properties:**
- `headerText`: Main header text (optional, hidden if empty)
- `detailsText`: Description text below header (optional, hidden if empty)
- `showCloseButton`: Show close button in header (default: false)
- `closePopout`: Function to close popout (auto-injected by PluginPopout)
- `headerHeight`: Readonly height of header (0 if not visible)
- `detailsHeight`: Readonly height of description (0 if not visible)
The component automatically handles spacing and layout. Content children are rendered below the description with proper padding.
### Settings Component ### Settings Component
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components: Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:
@@ -261,6 +297,27 @@ PluginSettings {
All settings automatically save on change and load on component creation. No manual `pluginService.savePluginData()` calls needed! All settings automatically save on change and load on component creation. No manual `pluginService.savePluginData()` calls needed!
**How Default Values Work:**
Each setting component has a `defaultValue` property that is used when no saved value exists. Define sensible defaults in your settings UI:
```qml
StringSetting {
settingKey: "apiKey"
defaultValue: "" // Empty string if no key saved
}
ToggleSetting {
settingKey: "enabled"
defaultValue: true // Enabled by default
}
ListSettingWithInput {
settingKey: "locations"
defaultValue: [] // Empty array if no locations saved
}
```
1. **PluginSettings** - Root wrapper for all plugin settings 1. **PluginSettings** - Root wrapper for all plugin settings
- `pluginId`: Your plugin ID (required) - `pluginId`: Your plugin ID (required)
- Auto-handles storage and provides saveValue/loadValue to children - Auto-handles storage and provides saveValue/loadValue to children
@@ -271,14 +328,14 @@ All settings automatically save on change and load on component creation. No man
- `label`: Display label (required) - `label`: Display label (required)
- `description`: Help text (optional) - `description`: Help text (optional)
- `placeholder`: Input placeholder (optional) - `placeholder`: Input placeholder (optional)
- `defaultValue`: Default value (optional) - `defaultValue`: Default value (optional, default: `""`)
- Layout: Vertical stack (label, description, input field) - Layout: Vertical stack (label, description, input field)
3. **ToggleSetting** - Boolean toggle switch 3. **ToggleSetting** - Boolean toggle switch
- `settingKey`: Storage key (required) - `settingKey`: Storage key (required)
- `label`: Display label (required) - `label`: Display label (required)
- `description`: Help text (optional) - `description`: Help text (optional)
- `defaultValue`: Default boolean (optional) - `defaultValue`: Default boolean (optional, default: `false`)
- Layout: Horizontal (label/description left, toggle right) - Layout: Horizontal (label/description left, toggle right)
4. **SelectionSetting** - Dropdown menu 4. **SelectionSetting** - Dropdown menu
@@ -286,7 +343,7 @@ All settings automatically save on change and load on component creation. No man
- `label`: Display label (required) - `label`: Display label (required)
- `description`: Help text (optional) - `description`: Help text (optional)
- `options`: Array of `{label, value}` objects or simple strings (required) - `options`: Array of `{label, value}` objects or simple strings (required)
- `defaultValue`: Default value (optional) - `defaultValue`: Default value (optional, default: `""`)
- Layout: Horizontal (label/description left, dropdown right) - Layout: Horizontal (label/description left, dropdown right)
- Stores the `value` field, displays the `label` field - Stores the `value` field, displays the `label` field
@@ -294,6 +351,7 @@ All settings automatically save on change and load on component creation. No man
- `settingKey`: Storage key (required) - `settingKey`: Storage key (required)
- `label`: Display label (required) - `label`: Display label (required)
- `description`: Help text (optional) - `description`: Help text (optional)
- `defaultValue`: Default array (optional, default: `[]`)
- `delegate`: Custom item delegate Component (optional) - `delegate`: Custom item delegate Component (optional)
- `addItem(item)`: Add item to list - `addItem(item)`: Add item to list
- `removeItem(index)`: Remove item from list - `removeItem(index)`: Remove item from list
@@ -303,6 +361,7 @@ All settings automatically save on change and load on component creation. No man
- `settingKey`: Storage key (required) - `settingKey`: Storage key (required)
- `label`: Display label (required) - `label`: Display label (required)
- `description`: Help text (optional) - `description`: Help text (optional)
- `defaultValue`: Default array (optional, default: `[]`)
- `fields`: Array of field definitions (required) - `fields`: Array of field definitions (required)
- `id`: Field ID in saved object (required) - `id`: Field ID in saved object (required)
- `label`: Column header text (required) - `label`: Column header text (required)
@@ -329,7 +388,6 @@ import qs.Modules.Plugins
PluginSettings { PluginSettings {
pluginId: "myPlugin" pluginId: "myPlugin"
// Section header (optional)
StyledText { StyledText {
width: parent.width width: parent.width
text: "General Settings" text: "General Settings"
@@ -338,7 +396,6 @@ PluginSettings {
color: Theme.surfaceText color: Theme.surfaceText
} }
// Text input
StringSetting { StringSetting {
settingKey: "apiKey" settingKey: "apiKey"
label: "API Key" label: "API Key"
@@ -347,7 +404,6 @@ PluginSettings {
defaultValue: "" defaultValue: ""
} }
// Toggle switches
ToggleSetting { ToggleSetting {
settingKey: "enabled" settingKey: "enabled"
label: "Enable Feature" label: "Enable Feature"
@@ -355,7 +411,6 @@ PluginSettings {
defaultValue: true defaultValue: true
} }
// Dropdown selection
SelectionSetting { SelectionSetting {
settingKey: "theme" settingKey: "theme"
label: "Theme" label: "Theme"
@@ -368,11 +423,11 @@ PluginSettings {
defaultValue: "dark" defaultValue: "dark"
} }
// Structured list with multi-field input
ListSettingWithInput { ListSettingWithInput {
settingKey: "locations" settingKey: "locations"
label: "Locations" label: "Locations"
description: "Track multiple locations" description: "Track multiple locations"
defaultValue: []
fields: [ fields: [
{id: "name", label: "Name", placeholder: "Home", width: 150, required: true}, {id: "name", label: "Name", placeholder: "Home", width: 150, required: true},
{id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true} {id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true}
@@ -636,12 +691,12 @@ Look for lines prefixed with:
Plugins run with full QML runtime access. Only install plugins from trusted sources. Plugins run with full QML runtime access. Only install plugins from trusted sources.
**Permissions System:** **Permissions System:**
- `settings_read`: Read plugin configuration - `settings_read`: Read plugin configuration (not currently enforced)
- `settings_write`: Write plugin configuration - `settings_write`: **Required** to use PluginSettings - write plugin configuration (enforced)
- `process`: Execute system commands - `process`: Execute system commands (not currently enforced)
- `network`: Network access - `network`: Network access (not currently enforced)
Future versions may enforce permission restrictions. Currently, only `settings_write` is enforced by the PluginSettings component.
## API Stability ## API Stability

View File

@@ -11,7 +11,7 @@
</div> </div>
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and designed for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors. Features Material 3 design principles with a heavy focus on functionality and customizability. A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and optimized for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors.
## Screenshots ## Screenshots
@@ -122,6 +122,8 @@ curl -fsSL https://install.danklinux.com | sh
- Configure bluetooth, wifi, and audio input+output devices. - Configure bluetooth, wifi, and audio input+output devices.
- A lock screen - A lock screen
- Idle monitoring - configure auto lock, screen off, suspend, and hibernate with different knobs for battery + AC power. - Idle monitoring - configure auto lock, screen off, suspend, and hibernate with different knobs for battery + AC power.
- A greeter
- A comprehensive plugin system for endless customization possibilities.
**TL;DR** *dms replaces your waybar, swaylock, swayidle, hypridle, hyprlock, fuzzels, walker, mako, and basically everything you use to stitch a desktop together* **TL;DR** *dms replaces your waybar, swaylock, swayidle, hypridle, hyprlock, fuzzels, walker, mako, and basically everything you use to stitch a desktop together*
@@ -313,7 +315,7 @@ sudo sh -c "curl -L https://github.com/AvengeMedia/dgop/releases/latest/download
A lot of options are subject to personal preference, but the below sets a good starting point for most features. A lot of options are subject to personal preference, but the below sets a good starting point for most features.
### Niri Integration ### niri Integration
Add to your niri config Add to your niri config
@@ -393,6 +395,17 @@ binds {
} }
``` ```
#### niri theming
If using a niri build newer than [3933903](https://github.com/YaLTeR/niri/commit/39339032cee3453faa54c361a38db6d83756f750), you can synchronize colors and gaps with the shell settings by adding the following to your niri config.
```bash
# For colors
echo -e 'include "dms/colors.kdl"' >> ~/.config/niri/config.kdl
# For gaps, border widths, certain window rules
echo -e 'include "dms/layout.kdl"' >> ~/.config/niri/config.kdl
```
### Hyprland Integration ### Hyprland Integration
Add to your Hyprland config (`~/.config/hypr/hyprland.conf`): Add to your Hyprland config (`~/.config/hypr/hyprland.conf`):
@@ -629,6 +642,14 @@ echo "app-notifications = no-clipboard-copy,no-config-reload" >> ~/.config/ghost
echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf
``` ```
## Plugins
dms features a plugin system - meaning you can create your own widgets and load other user widgets.
More comprehensive details available in the [PLUGINS](PLUGINS/README.md) - and example [Emoji Plugin](PLUGINS/ExampleEmojiPlugin) is available for reference.
The example plugin can be installed by `cp -R ./PLUGINS/ExampleEmojiPlugin ~/.config/DankMaterialShell/plugins` - then it will appear in dms settings.
### Calendar Setup ### Calendar Setup
Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration: Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:

View File

@@ -82,6 +82,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
detectCompositor() detectCompositor()
NiriService.generateNiriLayoutConfig()
} }
function filterCurrentWorkspace(toplevels, screen) { function filterCurrentWorkspace(toplevels, screen) {
@@ -192,6 +193,7 @@ Singleton {
root.isHyprland = false root.isHyprland = false
root.compositor = "niri" root.compositor = "niri"
console.log("CompositorService: Detected Niri with socket:", root.niriSocket) console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
NiriService.generateNiriBinds()
} else { } else {
root.isHyprland = false root.isHyprland = false
root.isNiri = true root.isNiri = true

View File

@@ -708,7 +708,6 @@ Singleton {
onExited: function (exitCode) { onExited: function (exitCode) {
geoclueAvailable = (exitCode === 0) geoclueAvailable = (exitCode === 0)
console.log("DisplayService: geoclue available:", geoclueAvailable)
} }
} }

View File

@@ -2,10 +2,12 @@ pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common
Singleton { Singleton {
id: root id: root
@@ -31,6 +33,7 @@ Singleton {
property bool suppressConfigToast: true property bool suppressConfigToast: true
property bool suppressNextConfigToast: false property bool suppressNextConfigToast: false
property bool matugenSuppression: false property bool matugenSuppression: false
property bool configGenerationPending: false
readonly property string socketPath: Quickshell.env("NIRI_SOCKET") readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
@@ -412,13 +415,13 @@ Singleton {
} }
function doScreenTransition() { function doScreenTransition() {
send({ return send({
"Action": { "Action": {
"DoScreenTransition": { "DoScreenTransition": {
"delay_ms": 0, "delay_ms": 0,
}
} }
} })
})
} }
function switchToWorkspace(workspaceIndex) { function switchToWorkspace(workspaceIndex) {
@@ -652,6 +655,7 @@ Singleton {
return result return result
} }
Timer { Timer {
id: suppressToastTimer id: suppressToastTimer
interval: 3000 interval: 3000
@@ -663,4 +667,101 @@ Singleton {
interval: 2000 interval: 2000
onTriggered: root.matugenSuppression = false onTriggered: root.matugenSuppression = false
} }
Timer {
id: configGenerationDebounce
interval: 100
onTriggered: root.doGenerateNiriLayoutConfig()
}
function generateNiriLayoutConfig() {
const niriSocket = Quickshell.env("NIRI_SOCKET")
if (!niriSocket || niriSocket.length === 0) {
return
}
if (configGenerationPending) {
return
}
configGenerationPending = true
configGenerationDebounce.restart()
}
function doGenerateNiriLayoutConfig() {
console.log("NiriService: Generating layout config...")
const cornerRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, SettingsData.dankBarSpacing) : 4
const configContent = `layout {
gaps ${gaps}
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
const niriDmsDir = configDir + "/niri/dms"
const configPath = niriDmsDir + "/layout.kdl"
writeConfigProcess.configContent = configContent
writeConfigProcess.configPath = configPath
writeConfigProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${configPath}" << 'EOF'\n${configContent}\nEOF`]
writeConfigProcess.running = true
configGenerationPending = false
}
function generateNiriBinds() {
console.log("NiriService: Generating binds config...")
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
const niriDmsDir = configDir + "/niri/dms"
const bindsPath = niriDmsDir + "/binds.kdl"
const sourceBindsPath = Paths.strip(Qt.resolvedUrl("niri-binds.kdl"))
writeBindsProcess.bindsPath = bindsPath
writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp "${sourceBindsPath}" "${bindsPath}"`]
writeBindsProcess.running = true
}
Process {
id: writeConfigProcess
property string configContent: ""
property string configPath: ""
onExited: exitCode => {
if (exitCode === 0) {
console.log("NiriService: Generated layout config at", configPath)
} else {
console.warn("NiriService: Failed to write layout config, exit code:", exitCode)
}
}
}
Process {
id: writeBindsProcess
property string bindsPath: ""
onExited: exitCode => {
if (exitCode === 0) {
console.log("NiriService: Generated binds config at", bindsPath)
} else {
console.warn("NiriService: Failed to write binds config, exit code:", exitCode)
}
}
}
} }

View File

@@ -28,6 +28,7 @@ Singleton {
signal pluginLoaded(string pluginId) signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId) signal pluginUnloaded(string pluginId)
signal pluginLoadFailed(string pluginId, string error) signal pluginLoadFailed(string pluginId, string error)
signal pluginDataChanged(string pluginId)
Component.onCompleted: { Component.onCompleted: {
Qt.callLater(initializePlugins) Qt.callLater(initializePlugins)
@@ -53,20 +54,14 @@ Singleton {
var dir = directories[i].trim() var dir = directories[i].trim()
if (dir) { if (dir) {
var manifestPath = currentDir + "/" + dir + "/plugin.json" var manifestPath = currentDir + "/" + dir + "/plugin.json"
console.log("PluginService: Found plugin directory:", dir, "checking manifest at:", manifestPath)
loadPluginManifest(manifestPath) loadPluginManifest(manifestPath)
} }
} }
} else {
console.log("PluginService: No directories found in:", currentDir)
} }
} }
} }
onExited: function(exitCode) { onExited: function(exitCode) {
if (exitCode !== 0) {
console.log("PluginService: Directory scan failed for:", pluginDirectories[currentScanIndex], "exit code:", exitCode)
}
currentScanIndex++ currentScanIndex++
if (currentScanIndex < pluginDirectories.length) { if (currentScanIndex < pluginDirectories.length) {
scanNextDirectory() scanNextDirectory()
@@ -83,7 +78,6 @@ Singleton {
function scanNextDirectory() { function scanNextDirectory() {
var dir = pluginDirectories[currentScanIndex] var dir = pluginDirectories[currentScanIndex]
console.log("PluginService: Scanning directory:", dir)
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"] lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
lsProcess.running = true lsProcess.running = true
} }
@@ -91,9 +85,6 @@ Singleton {
property var manifestReaders: ({}) property var manifestReaders: ({})
function loadPluginManifest(manifestPath) { function loadPluginManifest(manifestPath) {
console.log("PluginService: Loading manifest:", manifestPath)
// Create a unique key for this manifest reader
var readerId = "reader_" + Date.now() + "_" + Math.random() var readerId = "reader_" + Date.now() + "_" + Math.random()
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }") var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
@@ -102,9 +93,7 @@ Singleton {
process.command = ["cat", manifestPath] process.command = ["cat", manifestPath]
process.stdout.streamFinished.connect(function() { process.stdout.streamFinished.connect(function() {
try { try {
console.log("PluginService: DEBUGGING parsing manifest, text length:", process.stdout.text.length)
var manifest = JSON.parse(process.stdout.text.trim()) var manifest = JSON.parse(process.stdout.text.trim())
console.log("PluginService: Successfully parsed manifest for plugin:", manifest.id)
processManifest(manifest, manifestPath) processManifest(manifest, manifestPath)
} catch (e) { } catch (e) {
console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message) console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message)
@@ -137,7 +126,6 @@ Singleton {
} }
function registerPlugin(manifest, manifestPath) { function registerPlugin(manifest, manifestPath) {
console.log("PluginService: registerPlugin called with", manifest.id)
if (!manifest.id || !manifest.name || !manifest.component) { if (!manifest.id || !manifest.name || !manifest.component) {
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath) console.error("PluginService: Invalid manifest, missing required fields:", manifestPath)
return return
@@ -167,12 +155,18 @@ Singleton {
pluginInfo.loaded = false pluginInfo.loaded = false
availablePlugins[manifest.id] = pluginInfo availablePlugins[manifest.id] = pluginInfo
console.log("PluginService: Registered plugin:", manifest.id, "-", manifest.name) }
console.log("PluginService: Component path:", pluginInfo.componentPath)
function hasPermission(pluginId, permission) {
var plugin = availablePlugins[pluginId]
if (!plugin) {
return false
}
var permissions = plugin.permissions || []
return permissions.indexOf(permission) !== -1
} }
function loadPlugin(pluginId) { function loadPlugin(pluginId) {
console.log("PluginService: loadPlugin called for", pluginId)
var plugin = availablePlugins[pluginId] var plugin = availablePlugins[pluginId]
if (!plugin) { if (!plugin) {
console.error("PluginService: Plugin not found:", pluginId) console.error("PluginService: Plugin not found:", pluginId)
@@ -181,27 +175,45 @@ Singleton {
} }
if (plugin.loaded) { if (plugin.loaded) {
console.log("PluginService: Plugin already loaded:", pluginId)
return true return true
} }
try { if (pluginWidgetComponents[pluginId]) {
// Create the widget component var oldComponent = pluginWidgetComponents[pluginId]
var componentUrl = "file://" + plugin.componentPath if (oldComponent) {
console.log("PluginService: Loading component from:", componentUrl) oldComponent.destroy()
}
delete pluginWidgetComponents[pluginId]
}
try {
var componentUrl = "file://" + plugin.componentPath
var component = Qt.createComponent(componentUrl, Component.PreferSynchronous)
if (component.status === Component.Loading) {
component.statusChanged.connect(function() {
if (component.status === Component.Error) {
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
pluginLoadFailed(pluginId, component.errorString())
component.destroy()
}
})
}
var component = Qt.createComponent(componentUrl)
if (component.status === Component.Error) { if (component.status === Component.Error) {
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString()) console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
pluginLoadFailed(pluginId, component.errorString()) pluginLoadFailed(pluginId, component.errorString())
component.destroy()
return false return false
} }
pluginWidgetComponents[pluginId] = component var newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = component
pluginWidgetComponents = newComponents
plugin.loaded = true plugin.loaded = true
loadedPlugins[pluginId] = plugin loadedPlugins[pluginId] = plugin
console.log("PluginService: Successfully loaded plugin:", pluginId)
pluginLoaded(pluginId) pluginLoaded(pluginId)
return true return true
@@ -220,14 +232,19 @@ Singleton {
} }
try { try {
// Remove from component map if (pluginWidgetComponents[pluginId]) {
delete pluginWidgetComponents[pluginId] var component = pluginWidgetComponents[pluginId]
if (component) {
component.destroy()
}
}
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
// Mark as unloaded
plugin.loaded = false plugin.loaded = false
delete loadedPlugins[pluginId] delete loadedPlugins[pluginId]
console.log("PluginService: Successfully unloaded plugin:", pluginId)
pluginUnloaded(pluginId) pluginUnloaded(pluginId)
return true return true
@@ -262,13 +279,11 @@ Singleton {
} }
function enablePlugin(pluginId) { function enablePlugin(pluginId) {
console.log("PluginService: Enabling plugin:", pluginId)
SettingsData.setPluginSetting(pluginId, "enabled", true) SettingsData.setPluginSetting(pluginId, "enabled", true)
return loadPlugin(pluginId) return loadPlugin(pluginId)
} }
function disablePlugin(pluginId) { function disablePlugin(pluginId) {
console.log("PluginService: Disabling plugin:", pluginId)
SettingsData.setPluginSetting(pluginId, "enabled", false) SettingsData.setPluginSetting(pluginId, "enabled", false)
return unloadPlugin(pluginId) return unloadPlugin(pluginId)
} }
@@ -281,29 +296,22 @@ Singleton {
} }
function savePluginData(pluginId, key, value) { function savePluginData(pluginId, key, value) {
console.log("PluginService: Saving plugin data:", pluginId, key, JSON.stringify(value))
SettingsData.setPluginSetting(pluginId, key, value) SettingsData.setPluginSetting(pluginId, key, value)
console.log("PluginService: Data saved successfully") pluginDataChanged(pluginId)
return true return true
} }
function loadPluginData(pluginId, key, defaultValue) { function loadPluginData(pluginId, key, defaultValue) {
console.log("PluginService: Loading plugin data:", pluginId, key) return SettingsData.getPluginSetting(pluginId, key, defaultValue)
var value = SettingsData.getPluginSetting(pluginId, key, defaultValue)
console.log("PluginService: Loaded key:", key, "value:", JSON.stringify(value))
return value
} }
function createPluginDirectory() { function createPluginDirectory() {
console.log("PluginService: Creating plugin directory:", pluginDirectory)
var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }") var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
if (mkdirProcess.status === Component.Ready) { if (mkdirProcess.status === Component.Ready) {
var process = mkdirProcess.createObject(root) var process = mkdirProcess.createObject(root)
process.command = ["mkdir", "-p", pluginDirectory] process.command = ["mkdir", "-p", pluginDirectory]
process.exited.connect(function(exitCode) { process.exited.connect(function(exitCode) {
if (exitCode === 0) { if (exitCode !== 0) {
console.log("PluginService: Successfully created plugin directory")
} else {
console.error("PluginService: Failed to create plugin directory, exit code:", exitCode) console.error("PluginService: Failed to create plugin directory, exit code:", exitCode)
} }
process.destroy() process.destroy()

55
Services/niri-binds.kdl Normal file
View File

@@ -0,0 +1,55 @@
binds {
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "toggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "toggle";
}
Mod+N hotkey-overlay-title="Notification Center" {
spawn "dms" "ipc" "call" "notifications" "toggle";
}
Mod+Shift+N hotkey-overlay-title="Notepad" {
spawn "dms" "ipc" "call" "notepad" "toggle";
}
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "toggle";
}
// Audio
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
// BL
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
}

View File

@@ -1,10 +1,11 @@
import "../Common/fzf.js" as Fzf import "../Common/fzf.js" as Fzf
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
Rectangle { Item {
id: root id: root
property string text: "" property string text: ""
@@ -12,50 +13,33 @@ Rectangle {
property string currentValue: "" property string currentValue: ""
property var options: [] property var options: []
property var optionIcons: [] property var optionIcons: []
property bool forceRecreate: false
property bool enableFuzzySearch: false property bool enableFuzzySearch: false
property int popupWidthOffset: 0 property int popupWidthOffset: 0
property int maxPopupHeight: 400 property int maxPopupHeight: 400
property bool openUpwards: false property bool openUpwards: false
property int popupWidth: 0 property int popupWidth: 0
property bool alignPopupRight: false property bool alignPopupRight: false
property int dropdownWidth: 200
signal valueChanged(string value) signal valueChanged(string value)
width: parent.width width: parent.width
height: 60 implicitHeight: Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
radius: Theme.cornerRadius
color: "transparent"
Component.onCompleted: forceRecreateTimer.start()
Component.onDestruction: { Component.onDestruction: {
const popup = popupLoader.item const popup = dropdownMenu
if (popup && popup.visible) { if (popup && popup.visible) {
popup.close() popup.close()
} }
} }
onVisibleChanged: {
const popup = popupLoader.item
if (!visible && popup && popup.visible) {
popup.close()
} else if (visible) {
forceRecreateTimer.start()
}
}
Timer {
id: forceRecreateTimer
interval: 50
repeat: false
onTriggered: root.forceRecreate = !root.forceRecreate
}
Column { Column {
id: labelColumn
anchors.left: parent.left anchors.left: parent.left
anchors.right: dropdown.left anchors.right: dropdown.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
@@ -78,15 +62,14 @@ Rectangle {
Rectangle { Rectangle {
id: dropdown id: dropdown
width: root.width <= 60 ? root.width : 180 width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth)
height: 36 height: 40
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: dropdownArea.containsMouse ? Theme.primaryHover : Theme.contentBackground() color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : Theme.surfaceContainer
border.color: Theme.surfaceVariantAlpha border.color: dropdownMenu.visible ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1 border.width: dropdownMenu.visible ? 2 : 1
MouseArea { MouseArea {
id: dropdownArea id: dropdownArea
@@ -95,42 +78,39 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const popup = popupLoader.item if (dropdownMenu.visible) {
if (!popup) { dropdownMenu.close()
return return
} }
if (popup.visible) { dropdownMenu.searchQuery = ""
popup.close() dropdownMenu.updateFilteredOptions()
return
}
if (root.openUpwards || root.alignPopupRight) { dropdownMenu.open()
popup.open()
Qt.callLater(() => { const pos = dropdown.mapToItem(Overlay.overlay, 0, 0)
if (root.openUpwards) { const popupWidth = dropdownMenu.width
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0) const popupHeight = dropdownMenu.height
if (root.alignPopupRight) { const overlayHeight = Overlay.overlay.height
popup.x = pos.x + dropdown.width - popup.width
} else { if (root.openUpwards || pos.y + dropdown.height + popupHeight + 4 > overlayHeight) {
popup.x = pos.x - (root.popupWidthOffset / 2) if (root.alignPopupRight) {
} dropdownMenu.x = pos.x + dropdown.width - popupWidth
popup.y = pos.y - popup.height - 4 } else {
} else { dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4) }
if (root.alignPopupRight) { dropdownMenu.y = pos.y - popupHeight - 4
popup.x = pos.x + dropdown.width - popup.width
} else {
popup.x = pos.x - (root.popupWidthOffset / 2)
}
popup.y = pos.y
}
})
} else { } else {
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4) if (root.alignPopupRight) {
popup.x = pos.x - (root.popupWidthOffset / 2) dropdownMenu.x = pos.x + dropdown.width - popupWidth
popup.y = pos.y } else {
popup.open() dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
}
dropdownMenu.y = pos.y + dropdown.height + 4
}
if (root.enableFuzzySearch && searchField.visible) {
searchField.forceActiveFocus()
} }
} }
} }
@@ -139,8 +119,10 @@ Rectangle {
id: contentRow id: contentRow
anchors.left: parent.left anchors.left: parent.left
anchors.right: expandIcon.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
@@ -149,9 +131,9 @@ Rectangle {
return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : "" return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : ""
} }
size: 18 size: 18
color: Theme.surfaceVariantText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: name !== "" && root.width > 60 visible: name !== ""
} }
StyledText { StyledText {
@@ -159,229 +141,220 @@ Rectangle {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: root.width <= 60 ? dropdown.width - expandIcon.width - Theme.spacingS * 2 : dropdown.width - contentRow.x - expandIcon.width - Theme.spacingM - Theme.spacingS width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
elide: root.width <= 60 ? Text.ElideNone : Text.ElideRight elide: Text.ElideRight
horizontalAlignment: root.width <= 60 ? Text.AlignHCenter : Text.AlignLeft
} }
} }
DankIcon { DankIcon {
id: expandIcon id: expandIcon
name: "expand_more" name: dropdownMenu.visible ? "expand_less" : "expand_more"
size: 20 size: 20
color: Theme.surfaceVariantText color: Theme.surfaceText
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
} }
Loader { Popup {
id: popupLoader id: dropdownMenu
property bool recreateFlag: root.forceRecreate property string searchQuery: ""
property var filteredOptions: []
property int selectedIndex: -1
property var fzfFinder: new Fzf.Finder(root.options, {
"selector": option => option,
"limit": 50,
"casing": "case-insensitive"
})
active: true function updateFilteredOptions() {
onRecreateFlagChanged: { if (!root.enableFuzzySearch || searchQuery.length === 0) {
active = false filteredOptions = root.options
active = true selectedIndex = -1
return
}
const results = fzfFinder.find(searchQuery)
filteredOptions = results.map(result => result.item)
selectedIndex = -1
} }
sourceComponent: Component { function selectNext() {
Popup { if (filteredOptions.length === 0) {
id: dropdownMenu return
}
selectedIndex = (selectedIndex + 1) % filteredOptions.length
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
property string searchQuery: "" function selectPrevious() {
property var filteredOptions: [] if (filteredOptions.length === 0) {
property int selectedIndex: -1 return
property var fzfFinder: new Fzf.Finder(root.options, { }
"selector": option => option, selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
"limit": 50, listView.positionViewAtIndex(selectedIndex, ListView.Contain)
"casing": "case-insensitive" }
})
function updateFilteredOptions() { function selectCurrent() {
if (!root.enableFuzzySearch || searchQuery.length === 0) { if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
filteredOptions = root.options return
selectedIndex = -1 }
return root.currentValue = filteredOptions[selectedIndex]
} root.valueChanged(filteredOptions[selectedIndex])
close()
}
const results = fzfFinder.find(searchQuery) parent: Overlay.overlay
filteredOptions = results.map(result => result.item) width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
selectedIndex = -1 height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
} padding: 0
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
function selectNext() { background: Rectangle {
if (filteredOptions.length === 0) { color: "transparent"
return }
}
selectedIndex = (selectedIndex + 1) % filteredOptions.length
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
function selectPrevious() { contentItem: Rectangle {
if (filteredOptions.length === 0) { color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
return border.color: Theme.primary
} border.width: 2
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1 radius: Theme.cornerRadius
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
function selectCurrent() { layer.enabled: true
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) { layer.effect: MultiEffect {
return shadowEnabled: true
} shadowBlur: 0.4
root.currentValue = filteredOptions[selectedIndex] shadowColor: Theme.shadowStrong
root.valueChanged(filteredOptions[selectedIndex]) shadowVerticalOffset: 4
close() }
}
parent: Overlay.overlay Column {
width: root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset) anchors.fill: parent
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16) anchors.margins: Theme.spacingS
padding: 0
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: {
searchQuery = ""
updateFilteredOptions()
if (root.enableFuzzySearch && searchField.visible) {
searchField.forceActiveFocus()
}
}
background: Rectangle { Rectangle {
color: "transparent" id: searchContainer
}
contentItem: Rectangle { width: parent.width
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1) height: 42
border.color: Theme.primarySelected visible: root.enableFuzzySearch
border.width: 1
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
DankTextField {
id: searchField
Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: 1
placeholderText: "Search..."
text: dropdownMenu.searchQuery
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onTextChanged: {
dropdownMenu.searchQuery = text
dropdownMenu.updateFilteredOptions()
}
Keys.onDownPressed: dropdownMenu.selectNext()
Keys.onUpPressed: dropdownMenu.selectPrevious()
Keys.onReturnPressed: dropdownMenu.selectCurrent()
Keys.onEnterPressed: dropdownMenu.selectCurrent()
Keys.onPressed: event => {
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectPrevious()
event.accepted = true
}
}
}
}
Rectangle { Item {
id: searchContainer width: 1
height: Theme.spacingXS
visible: root.enableFuzzySearch
}
width: parent.width DankListView {
height: 42 id: listView
visible: root.enableFuzzySearch
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
DankTextField { width: parent.width
id: searchField height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
clip: true
model: dropdownMenu.filteredOptions
spacing: 2
anchors.fill: parent interactive: true
anchors.margins: 1 flickDeceleration: 1500
placeholderText: "Search..." maximumFlickVelocity: 2000
text: searchQuery boundsBehavior: Flickable.DragAndOvershootBounds
topPadding: Theme.spacingS boundsMovement: Flickable.FollowBoundsBehavior
bottomPadding: Theme.spacingS pressDelay: 0
onTextChanged: { flickableDirection: Flickable.VerticalFlick
searchQuery = text
updateFilteredOptions() delegate: Rectangle {
} property bool isSelected: dropdownMenu.selectedIndex === index
Keys.onDownPressed: selectNext() property bool isCurrentValue: root.currentValue === modelData
Keys.onUpPressed: selectPrevious() property int optionIndex: root.options.indexOf(modelData)
Keys.onReturnPressed: selectCurrent()
Keys.onEnterPressed: selectCurrent() width: ListView.view.width
Keys.onPressed: event => { height: 32
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { radius: Theme.cornerRadius
selectNext() color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { Row {
selectPrevious() anchors.left: parent.left
event.accepted = true anchors.leftMargin: Theme.spacingS
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) { anchors.verticalCenter: parent.verticalCenter
selectNext() spacing: Theme.spacingS
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) { DankIcon {
selectPrevious() name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
event.accepted = true size: 18
} color: isCurrentValue ? Theme.primary : Theme.surfaceText
} visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap
} }
} }
Item { MouseArea {
width: 1 id: optionArea
height: Theme.spacingXS
visible: root.enableFuzzySearch
}
DankListView { anchors.fill: parent
id: listView hoverEnabled: true
cursorShape: Qt.PointingHandCursor
property var popupRef: dropdownMenu onClicked: {
root.currentValue = modelData
width: parent.width root.valueChanged(modelData)
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) dropdownMenu.close()
clip: true
model: filteredOptions
spacing: 2
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: Rectangle {
property bool isSelected: selectedIndex === index
property bool isCurrentValue: root.currentValue === modelData
property int optionIndex: root.options.indexOf(modelData)
width: ListView.view.width
height: 32
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
size: 18
color: isCurrentValue ? Theme.primary : Theme.surfaceVariantText
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap
}
}
MouseArea {
id: optionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = modelData
root.valueChanged(modelData)
listView.popupRef.close()
}
}
} }
} }
} }

View File

@@ -44,7 +44,7 @@ Item {
DankIcon { DankIcon {
name: slider.leftIcon name: slider.leftIcon
size: Theme.iconSize size: Theme.iconSize
color: slider.enabled ? Theme.onSurface : Theme.onSurface_38 color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: slider.leftIcon.length > 0 visible: slider.leftIcon.length > 0
} }
@@ -265,7 +265,7 @@ Item {
DankIcon { DankIcon {
name: slider.rightIcon name: slider.rightIcon
size: Theme.iconSize size: Theme.iconSize
color: slider.enabled ? Theme.onSurface : Theme.onSurface_38 color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: slider.rightIcon.length > 0 visible: slider.rightIcon.length > 0
} }

View File

@@ -1,3 +1,3 @@
[templates.niri] [templates.niri]
input_path = './matugen/templates/niri-colors.kdl' input_path = './matugen/templates/niri-colors.kdl'
output_path = '~/.config/niri/dankshell-colors.kdl' output_path = '~/.config/niri/dms/colors.kdl'