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

Compare commits

...

28 Commits

Author SHA1 Message Date
purian23
04fdfa2a35 simplify Fedora Spec 2025-10-09 18:52:27 -04:00
bbedward
8f3085290d Update wrapper for consistency 2025-10-09 17:59:16 -04:00
bbedward
0839fe45f5 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-09 17:54:07 -04:00
bbedward
18f4795fda add dms-greeter wrapper 2025-10-09 17:53:56 -04:00
Parthiv Seetharaman
55d9fa622a Re-introduce default settings option and fix dms-cli build (#366)
* Reapply "Add default configuration option to home-manager (#356)"

This reverts commit bc23109f99.

* fix multiple xdg.configFile definitions error

* update dms-cli flake to fix build
2025-10-09 16:11:35 -04:00
bbedward
7dc723c764 Middle clock to close window on dock 2025-10-09 15:16:22 -04:00
bbedward
5a63205972 Fix light mode binding 2025-10-09 15:08:21 -04:00
bbedward
a4ceeafb1e Add ability to disable loginctl lock integration 2025-10-09 14:58:16 -04:00
bbedward
242e05cc0e Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-09 14:47:18 -04:00
bbedward
065dddbe6e Fix lock before suspend 2025-10-09 14:44:55 -04:00
purian23
fa6825252b prep Fedora Copr support 2025-10-09 14:42:09 -04:00
bbedward
b06e48a444 FolderListModel filters 2025-10-09 14:30:43 -04:00
bbedward
97dbd40f07 remove unused fileURL property 2025-10-09 14:17:29 -04:00
bbedward
bc23109f99 Revert "Add default configuration option to home-manager (#356)"
This reverts commit 67a4e3074e.
2025-10-09 13:53:27 -04:00
bbedward
ecb9675e9c Try more plugin loading things 2025-10-09 13:51:43 -04:00
bbedward
e1f9b9e7a4 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-09 13:37:46 -04:00
bbedward
067b485bb3 Bind perms directly to availablePlugins map 2025-10-09 13:37:34 -04:00
bokicoder
67a4e3074e Add default configuration option to home-manager (#356) 2025-10-09 13:23:44 -04:00
bokicoder
010bc4e8c3 fix systemd startup (#364) 2025-10-09 13:23:33 -04:00
bbedward
9de5e3253e Re-do plugin scanning 2025-10-09 13:22:33 -04:00
bbedward
e32622ac48 Some lockscreen restructure 2025-10-09 11:30:02 -04:00
bbedward
5e2371c2cb Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-09 09:01:39 -04:00
bbedward
a6ce26ee87 i18n: update loading + add zh_CN 2025-10-09 09:01:20 -04:00
Parthiv Seetharaman
2a72c126f1 Add nix home-manager option for adding plugins (#354)
* add plugins nix hm option

* add nix hm plugins option usage to readme
2025-10-09 08:34:05 -04:00
bbedward
36e1a5d379 Update greeter docs 2025-10-08 23:37:45 -04:00
bbedward
c12eafa1db Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-08 23:18:07 -04:00
bbedward
9e26d8755c Add os keyboard to greeter 2025-10-08 23:17:52 -04:00
Lukas Krejci
90bd30e351 add support for system updates on fedora (#353) 2025-10-08 22:19:40 -04:00
26 changed files with 2460 additions and 542 deletions

View File

@@ -1,4 +1,5 @@
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
pragma Singleton
@@ -7,60 +8,106 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
property string currentLocale: Qt.locale().name.substring(0, 2)
property var translations: ({})
property bool translationsLoaded: false
readonly property string _rawLocale: Qt.locale().name
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
readonly property var _candidates: {
const fullUnderscore = _rawLocale;
const fullHyphen = _rawLocale.replace("_", "-");
return [fullUnderscore, fullHyphen, _lang].filter(c => c && c !== "en");
}
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
property string currentLocale: "en"
property var translations: ({})
property bool translationsLoaded: false
property url _selectedPath: ""
FolderListModel {
id: dir
folder: root.translationsFolder
nameFilters: ["*.json"]
showDirs: false
showDotAndDotDot: false
onStatusChanged: if (status === FolderListModel.Ready) root._pickTranslation()
}
FileView {
id: translationLoader
path: root.currentLocale === "en" ? "" : Qt.resolvedUrl(`../translations/${root.currentLocale}.json`)
path: root._selectedPath
onLoaded: {
try {
root.translations = JSON.parse(text())
root.translationsLoaded = true
console.log(`I18n: Loaded translations for locale '${root.currentLocale}' (${Object.keys(root.translations).length} contexts)`)
console.log(`I18n: Loaded translations for '${root.currentLocale}' ` +
`(${Object.keys(root.translations).length} contexts)`)
} catch (e) {
console.warn(`I18n: Error parsing translations for locale '${root.currentLocale}':`, e, "- falling back to English")
root.translationsLoaded = false
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e,
"- falling back to English")
root._fallbackToEnglish()
}
}
onLoadFailed: (error) => {
console.warn(`I18n: Failed to load translations for locale '${root.currentLocale}' (${error}), falling back to English`)
root.translationsLoaded = false
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` +
"falling back to English")
root._fallbackToEnglish()
}
}
function tr(term, context) {
if (!translationsLoaded || !translations) {
return term
}
const actualContext = context || term
if (translations[actualContext] && translations[actualContext][term]) {
return translations[actualContext][term]
}
for (const ctx in translations) {
if (translations[ctx][term]) {
return translations[ctx][term]
function _pickTranslation() {
const present = new Set()
for (let i = 0; i < dir.count; i++) {
const name = dir.get(i, "fileName") // e.g. "zh_CN.json"
if (name && name.endsWith(".json")) {
present.add(name.slice(0, -5))
}
}
for (let i = 0; i < _candidates.length; i++) {
const cand = _candidates[i]
if (present.has(cand)) {
_useLocale(cand, dir.folder + "/" + cand + ".json")
return
}
}
_fallbackToEnglish()
}
function _useLocale(localeTag, fileUrl) {
currentLocale = localeTag
_selectedPath = fileUrl
translationsLoaded = false
translations = ({})
console.log(`I18n: Using locale '${localeTag}' from ${fileUrl}`)
}
function _fallbackToEnglish() {
currentLocale = "en"
_selectedPath = ""
translationsLoaded = false
translations = ({})
console.warn("I18n: Falling back to built-in English strings")
}
function tr(term, context) {
if (!translationsLoaded || !translations) return term
const ctx = context || term
if (translations[ctx] && translations[ctx][term]) return translations[ctx][term]
for (const c in translations) {
if (translations[c] && translations[c][term]) return translations[c][term]
}
return term
}
function trContext(context, term) {
if (!translationsLoaded || !translations) {
return term
}
if (translations[context] && translations[context][term]) {
return translations[context][term]
}
if (!translationsLoaded || !translations) return term
if (translations[context] && translations[context][term]) return translations[context][term]
return term
}
}

View File

@@ -38,6 +38,10 @@ Singleton {
return stringify(path).replace("file://", "")
}
function toFileUrl(path: string): string {
return path.startsWith("file://") ? path : "file://" + path
}
function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)])
}

View File

@@ -70,6 +70,7 @@ Singleton {
property int batteryHibernateTimeout: 0 // Never
property bool lockBeforeSuspend: false
property bool loginctlLockIntegration: true
property var recentColors: []
property bool showThirdPartyPlugins: false
@@ -152,6 +153,7 @@ Singleton {
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
loginctlLockIntegration = settings.loginctlLockIntegration !== undefined ? settings.loginctlLockIntegration : true
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
@@ -215,6 +217,7 @@ Singleton {
"batterySuspendTimeout": batterySuspendTimeout,
"batteryHibernateTimeout": batteryHibernateTimeout,
"lockBeforeSuspend": lockBeforeSuspend,
"loginctlLockIntegration": loginctlLockIntegration,
"recentColors": recentColors,
"showThirdPartyPlugins": showThirdPartyPlugins
}, null, 2))
@@ -643,6 +646,11 @@ Singleton {
saveSettings()
}
function setLoginctlLockIntegration(enabled) {
loginctlLockIntegration = enabled
saveSettings()
}
function setShowThirdPartyPlugins(enabled) {
showThirdPartyPlugins = enabled
saveSettings()

View File

@@ -74,7 +74,6 @@ Singleton {
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
property var workerRunning: false
property var matugenColors: ({})
property int colorUpdateTrigger: 0
property var customThemeData: null
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
@@ -84,7 +83,6 @@ Singleton {
matugenCheck.running = true
if (typeof SessionData !== "undefined") {
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
isLightMode = SessionData.isLightMode
}
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
@@ -100,7 +98,6 @@ Singleton {
}
function getMatugenColor(path, fallback) {
colorUpdateTrigger
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
for (const part of path.split(".")) {
@@ -578,10 +575,6 @@ Singleton {
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++
}
if (currentTheme === "custom" && customThemeFileView.path) {
customThemeFileView.reload()
}
@@ -905,7 +898,6 @@ Singleton {
const colorsText = dynamicColorsFileView.text()
if (colorsText) {
root.matugenColors = JSON.parse(colorsText)
root.colorUpdateTrigger++
if (typeof ToastService !== "undefined") {
ToastService.clearWallpaperError()
}

View File

@@ -621,7 +621,6 @@ DankModal {
required property bool fileIsDir
required property string filePath
required property string fileName
required property url fileURL
required property int index
width: weMode ? 245 : 140

View File

@@ -219,11 +219,20 @@ Item {
}
}
DankToggle {
width: parent.width
text: I18n.tr("Enable loginctl lock integration")
description: "Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen."
checked: SessionData.loginctlLockIntegration
onToggled: checked => SessionData.setLoginctlLockIntegration(checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Lock before suspend")
description: "Automatically lock the screen when the system prepares to suspend"
checked: SessionData.lockBeforeSuspend
visible: SessionData.loginctlLockIntegration
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}

View File

@@ -491,7 +491,6 @@ Item {
}
}
Behavior on color { ColorAnimation { duration: Anims.durShort } }
Behavior on border.color { ColorAnimation { duration: Anims.durShort } }
}
}
@@ -675,14 +674,6 @@ Item {
}
}
Behavior on color {
ColorAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Behavior on border.color {
ColorAnimation {
duration: Anims.durShort
@@ -858,14 +849,6 @@ Item {
}
}
}
Behavior on color {
ColorAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
@@ -1024,14 +1007,6 @@ Item {
}
}
}
Behavior on color {
ColorAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
}

View File

@@ -322,7 +322,21 @@ Item {
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
if (appData && appData.type === "window") {
const sortedToplevels = CompositorService.sortedToplevels
for (var i = 0; i < sortedToplevels.length; i++) {
const toplevel = sortedToplevels[i]
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
if (checkId === appData.uniqueId) {
toplevel.close()
break
}
}
} else if (appData && appData.type === "grouped") {
if (contextMenu) {
contextMenu.showForButton(root, appData, 40, false, cachedDesktopEntry)
}
} else if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
@@ -333,7 +347,7 @@ Item {
"comment": desktopEntry.comment || ""
})
}
SessionService.launchDesktopEntry(desktopEntry)
SessionService.launchDesktopEntry(desktopEntry)
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu && appData) {

View File

@@ -11,6 +11,7 @@ import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Lock
Item {
id: root
@@ -322,6 +323,8 @@ Item {
TextInput {
id: inputField
property bool syncingFromState: false
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.rightMargin: {
@@ -329,6 +332,9 @@ Item {
if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width
}
if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width
}
if (enterButton.visible) {
margin += enterButton.width + 2
}
@@ -338,6 +344,7 @@ Item {
focus: true
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
onTextChanged: {
if (syncingFromState) return
if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text
} else {
@@ -355,13 +362,17 @@ Item {
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(GreeterState.username)
GreeterState.passwordBuffer = ""
inputField.text = ""
syncingFromState = true
text = ""
syncingFromState = false
}
}
}
Component.onCompleted: {
syncingFromState = true
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput
syncingFromState = false
if (isPrimaryScreen)
forceActiveFocus()
}
@@ -371,12 +382,18 @@ Item {
}
}
KeyboardController {
id: keyboard_controller
target: inputField
rootObject: root
}
StyledText {
id: placeholder
anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : parent.right))
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter
text: {
@@ -413,7 +430,7 @@ Item {
StyledText {
anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : parent.right))
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter
text: {
@@ -441,8 +458,8 @@ Item {
DankActionButton {
id: revealButton
anchors.right: enterButton.visible ? enterButton.left : parent.right
anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32
@@ -450,6 +467,24 @@ Item {
enabled: visible
onClicked: parent.showPassword = !parent.showPassword
}
DankActionButton {
id: virtualKeyboardButton
anchors.right: enterButton.visible ? enterButton.left : parent.right
anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: 32
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: visible
onClicked: {
if (keyboard_controller.isKeyboardActive) {
keyboard_controller.hide()
} else {
keyboard_controller.show()
}
}
}
DankActionButton {
id: enterButton
@@ -1256,7 +1291,7 @@ Item {
if (sessionCmd) {
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex])
GreetdMemory.setLastSuccessfulUser(GreeterState.username)
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"], true)
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"])
}
}

View File

@@ -18,34 +18,43 @@ The easiest thing is to run `dms greeter install` or `dms` for interactive insta
### Manual
1. Install `greetd` (in most distro's standard repositories)
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
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`
5. Edit or create `/etc/greetd/config.toml`
1. Install `greetd` (in most distro's standard repositories) and `quickshell`
2. Clone the dms project to `/etc/xdg/quickshell/dms-greeter`
```bash
sudo git clone https://github.com/AvengeMedia/DankMaterialShell.git /etc/xdg/quickshell/dms-greeter
```
3. Copy `assets/dms-greeter` to `/usr/local/bin/dms-greeter`:
```bash
sudo cp assets/dms-greeter /usr/local/bin/dms-greeter
sudo chmod +x /usr/local/bin/dms-greeter
```
4. Create greeter cache directory with proper permissions:
```bash
sudo mkdir -p /var/cache/dms-greeter
sudo chown greeter:greeter /var/cache/dms-greeter
sudo chmod 770 /var/cache/dms-greeter
```
6. Edit or create `/etc/greetd/config.toml`:
```toml
[terminal]
# The VT to run the greeter on. Can be "next", "current" or a number
# designating the VT.
vt = 1
# The default session, also known as the greeter.
[default_session]
# `agreety` is the bundled agetty/login-lookalike. You can replace `/bin/sh`
# with whatever you want started, such as `sway`.
# The user to run the command as. The privileges this user must have depends
# on the greeter. A graphical greeter may for example require the user to be
# in the `video` group.
user = "greeter"
command = "/usr/local/bin/start-dms-greetd.sh"
# Change compositor to sway or hyprland if preferred
command = "/usr/local/bin/dms-greeter --command niri"
```
Enable the greeter with `sudo systemctl enable greetd`
#### Legacy installation (deprecated)
If you prefer the old method with separate shell scripts and config files:
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd`
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
### NixOS
To install the greeter on NixOS add the repo to your flake inputs as described in the readme. Then somewhere in your NixOS config add this to imports:
@@ -66,7 +75,30 @@ programs.dankMaterialShell.greeter = {
## Usage
To run dms in greeter mode you just need to set `DMS_RUN_GREETER=1` in the environment.
### Using dms-greeter wrapper (recommended)
The `dms-greeter` wrapper simplifies running the greeter with any compositor:
```bash
dms-greeter --command niri
dms-greeter --command hyprland
dms-greeter --command sway
dms-greeter --command niri -C /path/to/custom-niri.kdl
```
Configure greetd to use it in `/etc/greetd/config.toml`:
```toml
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/local/bin/dms-greeter --command niri"
```
### Manual usage
To run dms in greeter mode you can also manually set environment variables:
```bash
DMS_RUN_GREETER=1 qs -p /path/to/dms
@@ -86,15 +118,17 @@ Wallpapers and themes and weather and clock formats and things are a TODO on the
You can synchronize those configurations with a specific user if you want greeter settings to always mirror the shell.
The greeter uses the `dms-greeter` group for file access permissions, so ensure your user and the greeter user are both members of this group.
```bash
# For core settings (theme, clock formats, etc)
sudo ln -sf ~/.config/DankMaterialShell/settings.json /etc/greetd/.dms/settings.json
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
# For state (mainly you would configure wallpaper in this file)
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /etc/greetd/.dms/session.json
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
# For wallpaper based theming
sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /etc/greetd/.dms/dms-colors.json
sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /var/cache/dms-greeter/dms-colors.json
```
You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable, the default is `/etc/greetd/.dms`
You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`.
It should be writable by the greeter user.
The cache directory should be owned by `greeter:greeter` with `770` permissions.

174
Modules/Greetd/assets/dms-greeter Executable file
View File

@@ -0,0 +1,174 @@
#!/bin/bash
set -e
COMPOSITOR=""
COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter"
show_help() {
cat << EOF
dms-greeter - DankMaterialShell greeter launcher
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
Required:
--command COMPOSITOR Compositor to use (niri, hyprland, or sway)
Options:
-C, --config PATH Custom compositor config file
-p, --path PATH DMS path (config name or absolute path)
(default: dms-greeter)
--cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter)
-h, --help Show this help message
Examples:
dms-greeter --command niri
dms-greeter --command hyprland -C /etc/greetd/custom-hypr.conf
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
EOF
}
while [[ $# -gt 0 ]]; do
case $1 in
--command)
COMPOSITOR="$2"
shift 2
;;
-C|--config)
COMPOSITOR_CONFIG="$2"
shift 2
;;
-p|--path)
DMS_PATH="$2"
shift 2
;;
--cache-dir)
CACHE_DIR="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1" >&2
show_help
exit 1
;;
esac
done
if [[ -z "$COMPOSITOR" ]]; then
echo "Error: --command COMPOSITOR is required" >&2
show_help
exit 1
fi
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1
export DMS_GREET_CFG_DIR="$CACHE_DIR"
mkdir -p "$CACHE_DIR"
QS_CMD="qs"
if [[ "$DMS_PATH" == /* ]]; then
QS_CMD="qs -p $DMS_PATH"
else
QS_CMD="qs -c $DMS_PATH"
fi
case "$COMPOSITOR" in
niri)
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << NIRI_EOF
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
debug {
keep-max-bpc-unchanged
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec niri -c "$COMPOSITOR_CONFIG"
;;
hyprland)
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1
exec = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
exec = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec Hyprland -c "$COMPOSITOR_CONFIG"
;;
sway)
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SWAY_EOF
exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SWAY_EOF
exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec sway -c "$COMPOSITOR_CONFIG"
;;
*)
echo "Error: Unsupported compositor: $COMPOSITOR" >&2
echo "Supported compositors: niri, hyprland, sway" >&2
exit 1
;;
esac

View File

@@ -8,81 +8,54 @@ import qs.Services
Item {
id: root
function activate() {
loader.activeAsync = true
}
property string sharedPasswordBuffer: ""
property bool shouldLock: false
Component.onCompleted: {
if (SessionService.loginctlAvailable || SessionService.sessionPath) {
if (SessionService.locked || SessionService.lockedHint) {
console.log("Lock: Session locked on startup")
loader.activeAsync = true
}
}
IdleService.lockComponent = root
}
Connections {
target: IdleService
function onLockRequested() {
console.log("Lock: Received lock request from IdleService")
loader.activeAsync = true
}
function activate() {
shouldLock = true
}
Connections {
target: SessionService
function onSessionLocked() {
console.log("Lock: Lock signal received -> show lock")
loader.activeAsync = true
shouldLock = true
}
function onSessionUnlocked() {
console.log("Lock: Unlock signal received -> hide lock")
loader.active = false
}
function onLoginctlStateChanged() {
if (SessionService.lockedHint && !loader.active) {
console.log("Lock: LockedHint=true -> show lock")
loader.activeAsync = true
} else if (!SessionService.locked && !SessionService.lockedHint && loader.active) {
console.log("Lock: LockedHint=false -> hide lock")
loader.active = false
}
}
function onPrepareForSleep() {
if (SessionService.preparingForSleep && SessionData.lockBeforeSuspend) {
console.log("Lock: PrepareForSleep -> lock before suspend")
loader.activeAsync = true
}
shouldLock = false
}
}
LazyLoader {
id: loader
Connections {
target: IdleService
WlSessionLock {
id: sessionLock
function onLockRequested() {
shouldLock = true
}
}
property bool unlocked: false
property string sharedPasswordBuffer: ""
WlSessionLock {
id: sessionLock
locked: true
locked: root.shouldLock
onLockedChanged: {
if (!locked) {
loader.active = false
}
}
WlSessionLockSurface {
color: "transparent"
LockSurface {
id: lockSurface
anchors.fill: parent
lock: sessionLock
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
sharedPasswordBuffer: root.sharedPasswordBuffer
onUnlockRequested: {
root.shouldLock = false
}
onPasswordChanged: newPassword => {
sessionLock.sharedPasswordBuffer = newPassword
root.sharedPasswordBuffer = newPassword
}
}
}
@@ -96,17 +69,15 @@ Item {
target: "lock"
function lock() {
console.log("Lock screen requested via IPC")
loader.activeAsync = true
shouldLock = true
}
function demo() {
console.log("Lock screen DEMO mode requested via IPC")
demoWindow.showDemo()
}
function isLocked(): bool {
return SessionService.locked || loader.active
return sessionLock.locked
}
}
}

View File

@@ -4,33 +4,26 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
WlSessionLockSurface {
Rectangle {
id: root
required property WlSessionLock lock
required property string sharedPasswordBuffer
signal passwordChanged(string newPassword)
readonly property bool locked: lock && !lock.locked
function unlock(): void {
lock.locked = false
}
signal unlockRequested()
color: "transparent"
Loader {
LockScreenContent {
anchors.fill: parent
sourceComponent: LockScreenContent {
demoMode: false
passwordBuffer: root.sharedPasswordBuffer
screenName: root.screen?.name ?? ""
onUnlockRequested: root.unlock()
onPasswordBufferChanged: {
if (root.sharedPasswordBuffer !== passwordBuffer) {
root.passwordChanged(passwordBuffer)
}
demoMode: false
passwordBuffer: root.sharedPasswordBuffer
screenName: ""
onUnlockRequested: root.unlockRequested()
onPasswordBufferChanged: {
if (root.sharedPasswordBuffer !== passwordBuffer) {
root.passwordChanged(passwordBuffer)
}
}
}

View File

@@ -17,7 +17,14 @@ Item {
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
height: implicitHeight
readonly property bool hasPermission: pluginService && pluginService.hasPermission ? pluginService.hasPermission(pluginId, "settings_write") : true
readonly property bool hasPermission: {
if (!pluginService || !pluginId) return true
const allPlugins = pluginService.availablePlugins
const plugin = allPlugins[pluginId]
if (!plugin) return true
const permissions = Array.isArray(plugin.permissions) ? plugin.permissions : []
return permissions.indexOf("settings_write") !== -1
}
Component.onCompleted: {
loadVariants()

View File

@@ -600,6 +600,9 @@ FocusScope {
pluginsTab.expandedPluginId = ""
}
}
function onPluginListUpdated() {
refreshPluginList()
}
}
Connections {

View File

@@ -171,7 +171,7 @@ DankPopout {
return "Failed to check for updates:\n" + SystemUpdateService.errorMessage;
}
if (!SystemUpdateService.helperAvailable) {
return "No package manager found. Please install 'paru' or 'yay' to check for updates.";
return "No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.";
}
if (SystemUpdateService.isChecking) {
return "Checking for updates...";

View File

@@ -661,6 +661,16 @@ cp -R ./PLUGINS/ExampleEmojiPlugin ~/.config/DankMaterialShell/plugins
**Only install plugins from TRUSTED sources.** Plugins execute QML and javascript at runtime, plugins from third parties should be reviewed before enabling them in dms.
### nixOS - via home-manager
Add the following to your home-manager config to install a plugin:
```nix
programs.dankMaterialShell.plugins = {
ExampleEmojiPlugin.src = "${inputs.dankMaterialShell}/PLUGINS/ExampleEmojiPlugin";
};
```
### Calendar Setup
Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:

View File

@@ -136,6 +136,15 @@ Singleton {
}
}
Connections {
target: SessionService
function onPrepareForSleep() {
if (SessionData.lockBeforeSuspend) {
root.lockRequested()
}
}
}
Component.onCompleted: {
if (!idleMonitorAvailable) {
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.")

View File

@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Common
@@ -24,197 +25,202 @@ Singleton {
return configDirStr + "/DankMaterialShell/plugins"
}
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
property var pluginDirectories: [pluginDirectory, systemPluginDirectory]
property var knownManifests: ({})
property var pathToPluginId: ({})
property var pluginInstances: ({})
signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId)
signal pluginLoadFailed(string pluginId, string error)
signal pluginDataChanged(string pluginId)
signal pluginListUpdated()
Timer {
id: resyncDebounce
interval: 120
repeat: false
onTriggered: resyncAll()
}
Component.onCompleted: {
Qt.callLater(initializePlugins)
userWatcher.folder = Paths.toFileUrl(root.pluginDirectory)
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory)
Qt.callLater(resyncAll)
}
function initializePlugins() {
scanPlugins()
FolderListModel {
id: userWatcher
showDirs: true
showFiles: false
showDotAndDotDot: false
nameFilters: ["plugin.json"]
onCountChanged: resyncDebounce.restart()
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart()
}
property int currentScanIndex: 0
property var scanResults: []
property var foundPlugins: ({})
FolderListModel {
id: systemWatcher
showDirs: true
showFiles: false
showDotAndDotDot: false
nameFilters: ["plugin.json"]
property var lsProcess: Process {
id: dirScanner
onCountChanged: resyncDebounce.restart()
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart()
}
stdout: StdioCollector {
onStreamFinished: {
var output = text.trim()
var currentDir = pluginDirectories[currentScanIndex]
if (output) {
var directories = output.split('\n')
for (var i = 0; i < directories.length; i++) {
var dir = directories[i].trim()
if (dir) {
var manifestPath = currentDir + "/" + dir + "/plugin.json"
loadPluginManifest(manifestPath)
}
}
}
function snapshotModel(model, sourceTag) {
const out = []
const n = model.count
const baseDir = sourceTag === "user" ? pluginDirectory : systemPluginDirectory
for (let i = 0; i < n; i++) {
let dirPath = model.get(i, "filePath")
if (dirPath.startsWith("file://")) {
dirPath = dirPath.substring(7)
}
if (!dirPath.startsWith(baseDir)) {
continue
}
const manifestPath = dirPath + "/plugin.json"
out.push({ path: manifestPath, source: sourceTag })
}
return out
}
function resyncAll() {
const userList = snapshotModel(userWatcher, "user")
const sysList = snapshotModel(systemWatcher, "system")
const seenPaths = {}
function consider(entry) {
const key = entry.path
seenPaths[key] = true
const prev = knownManifests[key]
if (!prev) {
loadPluginManifestFile(entry.path, entry.source, Date.now())
}
}
for (let i=0;i<userList.length;i++) consider(userList[i])
for (let i=0;i<sysList.length;i++) consider(sysList[i])
onExited: function(exitCode) {
currentScanIndex++
if (currentScanIndex < pluginDirectories.length) {
scanNextDirectory()
} else {
currentScanIndex = 0
cleanupRemovedPlugins()
}
const removed = []
for (const path in knownManifests) {
if (!seenPaths[path]) removed.push(path)
}
}
function scanPlugins() {
currentScanIndex = 0
foundPlugins = {}
scanNextDirectory()
}
function scanNextDirectory() {
var dir = pluginDirectories[currentScanIndex]
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
lsProcess.running = true
}
property var manifestReaders: ({})
function loadPluginManifest(manifestPath) {
var readerId = "reader_" + Date.now() + "_" + Math.random()
var checkProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
if (checkProcess.status === Component.Ready) {
var checker = checkProcess.createObject(root)
checker.command = ["test", "-f", manifestPath]
checker.exited.connect(function(exitCode) {
if (exitCode !== 0) {
checker.destroy()
delete manifestReaders[readerId]
return
if (removed.length) {
removed.forEach(function(path) {
const pid = pathToPluginId[path]
if (pid) {
unregisterPluginByPath(path, pid)
}
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
if (catProcess.status === Component.Ready) {
var process = catProcess.createObject(root)
process.command = ["cat", manifestPath]
process.stdout.streamFinished.connect(function() {
try {
var manifest = JSON.parse(process.stdout.text.trim())
processManifest(manifest, manifestPath)
} catch (e) {
console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message)
}
process.destroy()
delete manifestReaders[readerId]
})
process.exited.connect(function(exitCode) {
if (exitCode !== 0) {
console.error("PluginService: Failed to read manifest file:", manifestPath, "exit code:", exitCode)
process.destroy()
delete manifestReaders[readerId]
}
})
manifestReaders[readerId] = process
process.running = true
} else {
console.error("PluginService: Failed to create manifest reader process")
}
checker.destroy()
delete knownManifests[path]
delete pathToPluginId[path]
})
manifestReaders[readerId] = checker
checker.running = true
} else {
console.error("PluginService: Failed to create file check process")
pluginListUpdated()
}
}
function processManifest(manifest, manifestPath) {
registerPlugin(manifest, manifestPath)
var enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false)
if (enabled) {
loadPlugin(manifest.id)
}
function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) {
const manifestId = "m_" + Math.random().toString(36).slice(2)
const qml = `
import QtQuick
import Quickshell.Io
FileView {
id: fv
property string absPath: ""
onLoaded: {
try {
let raw = text()
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1)
const manifest = JSON.parse(raw)
root._onManifestParsed(absPath, manifest, "${sourceTag}", ${mtimeEpochMs})
} catch (e) {
console.error("PluginService: bad manifest", absPath, e.message)
knownManifests[absPath] = { mtime: ${mtimeEpochMs}, source: "${sourceTag}", bad: true }
}
fv.destroy()
}
onLoadFailed: (err) => {
console.warn("PluginService: manifest load failed", absPath, err)
fv.destroy()
}
}
`
const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId)
loader.absPath = manifestPathNoScheme
loader.path = manifestPathNoScheme
}
function registerPlugin(manifest, manifestPath) {
if (!manifest.id || !manifest.name || !manifest.component) {
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath)
function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
if (!manifest || !manifest.id || !manifest.name || !manifest.component) {
console.error("PluginService: invalid manifest fields:", absPath)
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, bad: true }
return
}
var pluginDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'))
const dir = absPath.substring(0, absPath.lastIndexOf('/'))
let comp = manifest.component
if (comp.startsWith("./")) comp = comp.slice(2)
let settings = manifest.settings
if (settings && settings.startsWith("./")) settings = settings.slice(2)
// Clean up relative paths by removing './' prefix
var componentFile = manifest.component
if (componentFile.startsWith('./')) {
componentFile = componentFile.substring(2)
const info = {}
for (const k in manifest) info[k] = manifest[k]
let perms = manifest.permissions
if (typeof perms === "string") {
perms = perms.split(/\s*,\s*/)
}
var settingsFile = manifest.settings
if (settingsFile && settingsFile.startsWith('./')) {
settingsFile = settingsFile.substring(2)
if (!Array.isArray(perms)) {
perms = []
}
info.permissions = perms.map(p => String(p).trim())
var pluginInfo = {}
for (var key in manifest) {
pluginInfo[key] = manifest[key]
info.manifestPath = absPath
info.pluginDirectory = dir
info.componentPath = dir + "/" + comp
info.settingsPath = settings ? (dir + "/" + settings) : null
info.loaded = isPluginLoaded(manifest.id)
info.type = manifest.type || "widget"
info.source = sourceTag
const existing = availablePlugins[manifest.id]
const shouldReplace =
(!existing) ||
(existing && existing.source === "system" && sourceTag === "user")
if (shouldReplace) {
if (existing && existing.loaded && existing.source !== sourceTag) {
unloadPlugin(manifest.id)
}
const newMap = Object.assign({}, availablePlugins)
newMap[manifest.id] = info
availablePlugins = newMap
pathToPluginId[absPath] = manifest.id
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag }
pluginListUpdated()
const enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false)
if (enabled && !info.loaded) loadPlugin(manifest.id)
} else {
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, shadowedBy: existing.source }
pathToPluginId[absPath] = manifest.id
}
pluginInfo.manifestPath = manifestPath
pluginInfo.pluginDirectory = pluginDir
pluginInfo.componentPath = pluginDir + '/' + componentFile
pluginInfo.settingsPath = settingsFile ? pluginDir + '/' + settingsFile : null
pluginInfo.loaded = false
pluginInfo.type = manifest.type || "widget"
var newPlugins = Object.assign({}, availablePlugins)
newPlugins[manifest.id] = pluginInfo
availablePlugins = newPlugins
foundPlugins[manifest.id] = true
}
function hasPermission(pluginId, permission) {
var plugin = availablePlugins[pluginId]
if (!plugin) {
return false
}
var permissions = plugin.permissions || []
return permissions.indexOf(permission) !== -1
}
function cleanupRemovedPlugins() {
var pluginsToRemove = []
for (var pluginId in availablePlugins) {
if (!foundPlugins[pluginId]) {
pluginsToRemove.push(pluginId)
}
}
if (pluginsToRemove.length > 0) {
var newPlugins = Object.assign({}, availablePlugins)
for (var i = 0; i < pluginsToRemove.length; i++) {
var pluginId = pluginsToRemove[i]
if (isPluginLoaded(pluginId)) {
unloadPlugin(pluginId)
}
delete newPlugins[pluginId]
}
availablePlugins = newPlugins
function unregisterPluginByPath(absPath, pluginId) {
const current = availablePlugins[pluginId]
if (current && current.manifestPath === absPath) {
if (current.loaded) unloadPlugin(pluginId)
const newMap = Object.assign({}, availablePlugins)
delete newMap[pluginId]
availablePlugins = newMap
}
}
function loadPlugin(pluginId) {
var plugin = availablePlugins[pluginId]
const plugin = availablePlugins[pluginId]
if (!plugin) {
console.error("PluginService: Plugin not found:", pluginId)
pluginLoadFailed(pluginId, "Plugin not found")
@@ -225,48 +231,43 @@ Singleton {
return true
}
var isDaemon = plugin.type === "daemon"
var componentMap = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
const isDaemon = plugin.type === "daemon"
const map = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
if (componentMap[pluginId]) {
componentMap[pluginId]?.destroy()
if (isDaemon) {
var newDaemons = Object.assign({}, pluginDaemonComponents)
delete newDaemons[pluginId]
pluginDaemonComponents = newDaemons
} else {
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
}
const prevInstance = pluginInstances[pluginId]
if (prevInstance) {
prevInstance.destroy()
const newInstances = Object.assign({}, pluginInstances)
delete newInstances[pluginId]
pluginInstances = newInstances
}
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())
}
})
}
if (component.status === Component.Error) {
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
pluginLoadFailed(pluginId, component.errorString())
const url = "file://" + plugin.componentPath
const comp = Qt.createComponent(url, Component.PreferSynchronous)
if (comp.status === Component.Error) {
console.error("PluginService: component error", pluginId, comp.errorString())
pluginLoadFailed(pluginId, comp.errorString())
return false
}
if (isDaemon) {
var newDaemons = Object.assign({}, pluginDaemonComponents)
newDaemons[pluginId] = component
const instance = comp.createObject(root, { "pluginId": pluginId })
if (!instance) {
console.error("PluginService: failed to instantiate daemon:", pluginId, comp.errorString())
pluginLoadFailed(pluginId, comp.errorString())
return false
}
const newInstances = Object.assign({}, pluginInstances)
newInstances[pluginId] = instance
pluginInstances = newInstances
const newDaemons = Object.assign({}, pluginDaemonComponents)
newDaemons[pluginId] = comp
pluginDaemonComponents = newDaemons
} else {
var newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = component
const newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = comp
pluginWidgetComponents = newComponents
}
@@ -276,31 +277,37 @@ Singleton {
pluginLoaded(pluginId)
return true
} catch (error) {
console.error("PluginService: Error loading plugin:", pluginId, "Error:", error.message)
pluginLoadFailed(pluginId, error.message)
} catch (e) {
console.error("PluginService: Error loading plugin:", pluginId, e.message)
pluginLoadFailed(pluginId, e.message)
return false
}
}
function unloadPlugin(pluginId) {
var plugin = loadedPlugins[pluginId]
const plugin = loadedPlugins[pluginId]
if (!plugin) {
console.warn("PluginService: Plugin not loaded:", pluginId)
return false
}
try {
var isDaemon = plugin.type === "daemon"
const isDaemon = plugin.type === "daemon"
const instance = pluginInstances[pluginId]
if (instance) {
instance.destroy()
const newInstances = Object.assign({}, pluginInstances)
delete newInstances[pluginId]
pluginInstances = newInstances
}
if (isDaemon && pluginDaemonComponents[pluginId]) {
pluginDaemonComponents[pluginId]?.destroy()
var newDaemons = Object.assign({}, pluginDaemonComponents)
const newDaemons = Object.assign({}, pluginDaemonComponents)
delete newDaemons[pluginId]
pluginDaemonComponents = newDaemons
} else if (pluginWidgetComponents[pluginId]) {
pluginWidgetComponents[pluginId]?.destroy()
var newComponents = Object.assign({}, pluginWidgetComponents)
const newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
}
@@ -326,30 +333,30 @@ Singleton {
}
function getAvailablePlugins() {
var result = []
for (var key in availablePlugins) {
const result = []
for (const key in availablePlugins) {
result.push(availablePlugins[key])
}
return result
}
function getPluginVariants(pluginId) {
var plugin = availablePlugins[pluginId]
const plugin = availablePlugins[pluginId]
if (!plugin) {
return []
}
var variants = SettingsData.getPluginSetting(pluginId, "variants", [])
const variants = SettingsData.getPluginSetting(pluginId, "variants", [])
return variants
}
function getAllPluginVariants() {
var result = []
for (var pluginId in availablePlugins) {
var plugin = availablePlugins[pluginId]
const result = []
for (const pluginId in availablePlugins) {
const plugin = availablePlugins[pluginId]
if (plugin.type !== "widget") {
continue
}
var variants = getPluginVariants(pluginId)
const variants = getPluginVariants(pluginId)
if (variants.length === 0) {
result.push({
pluginId: pluginId,
@@ -361,8 +368,8 @@ Singleton {
loaded: plugin.loaded
})
} else {
for (var i = 0; i < variants.length; i++) {
var variant = variants[i]
for (let i = 0; i < variants.length; i++) {
const variant = variants[i]
result.push({
pluginId: pluginId,
variantId: variant.id,
@@ -379,9 +386,9 @@ Singleton {
}
function createPluginVariant(pluginId, variantName, variantConfig) {
var variants = getPluginVariants(pluginId)
var variantId = "variant_" + Date.now()
var newVariant = Object.assign({}, variantConfig, {
const variants = getPluginVariants(pluginId)
const variantId = "variant_" + Date.now()
const newVariant = Object.assign({}, variantConfig, {
id: variantId,
name: variantName
})
@@ -392,15 +399,15 @@ Singleton {
}
function removePluginVariant(pluginId, variantId) {
var variants = getPluginVariants(pluginId)
var newVariants = variants.filter(function(v) { return v.id !== variantId })
const variants = getPluginVariants(pluginId)
const newVariants = variants.filter(function(v) { return v.id !== variantId })
SettingsData.setPluginSetting(pluginId, "variants", newVariants)
pluginDataChanged(pluginId)
}
function updatePluginVariant(pluginId, variantId, variantConfig) {
var variants = getPluginVariants(pluginId)
for (var i = 0; i < variants.length; i++) {
const variants = getPluginVariants(pluginId)
for (let i = 0; i < variants.length; i++) {
if (variants[i].id === variantId) {
variants[i] = Object.assign({}, variants[i], variantConfig)
break
@@ -411,8 +418,8 @@ Singleton {
}
function getPluginVariantData(pluginId, variantId) {
var variants = getPluginVariants(pluginId)
for (var i = 0; i < variants.length; i++) {
const variants = getPluginVariants(pluginId)
for (let i = 0; i < variants.length; i++) {
if (variants[i].id === variantId) {
return variants[i]
}
@@ -421,8 +428,8 @@ Singleton {
}
function getLoadedPlugins() {
var result = []
for (var key in loadedPlugins) {
const result = []
for (const key in loadedPlugins) {
result.push(loadedPlugins[key])
}
return result
@@ -463,10 +470,14 @@ Singleton {
SettingsData.savePluginSettings()
}
function scanPlugins() {
resyncDebounce.restart()
}
function createPluginDirectory() {
var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
if (mkdirProcess.status === Component.Ready) {
var process = mkdirProcess.createObject(root)
const process = mkdirProcess.createObject(root)
process.command = ["mkdir", "-p", pluginDirectory]
process.exited.connect(function(exitCode) {
if (exitCode !== 0) {

View File

@@ -61,6 +61,10 @@ Singleton {
detectHibernateProcess.running = true
detectPrimeRunProcess.running = true
console.log("SessionService: Native inhibitor available:", nativeInhibitorAvailable)
if (!SessionData.loginctlLockIntegration) {
console.log("SessionService: loginctl lock integration disabled by user")
return
}
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities()
} else {
@@ -291,10 +295,29 @@ Singleton {
}
}
Connections {
target: SessionData
function onLoginctlLockIntegrationChanged() {
if (SessionData.loginctlLockIntegration) {
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities()
} else {
initFallbackLoginctl()
}
} else {
subscriptionSocket.connected = false
lockStateMonitorFallback.running = false
loginctlAvailable = false
stateInitialized = false
}
}
}
DankSocket {
id: subscriptionSocket
path: root.socketPath
connected: loginctlAvailable
connected: loginctlAvailable && SessionData.loginctlLockIntegration
onConnectionStateChanged: {
root.subscriptionConnected = connected
@@ -342,6 +365,10 @@ Singleton {
return
}
if (!SessionData.loginctlLockIntegration) {
return
}
if (DMSService.capabilities.includes("loginctl")) {
loginctlAvailable = true
if (!stateInitialized) {
@@ -366,6 +393,8 @@ Singleton {
}
function updateLoginctlState(state) {
const wasLocked = locked
sessionId = state.sessionId || ""
sessionPath = state.sessionPath || ""
locked = state.locked || false
@@ -384,6 +413,12 @@ Singleton {
prepareForSleep()
}
if (locked && !wasLocked) {
sessionLocked()
} else if (!locked && wasLocked) {
sessionUnlocked()
}
loginctlStateChanged()
}
@@ -483,10 +518,4 @@ Singleton {
}
}
Process {
id: lockSessionFallback
command: ["loginctl", "lock-session"]
running: false
}
}

View File

@@ -19,7 +19,48 @@ Singleton {
property bool distributionSupported: false
property string shellVersion: ""
readonly property list<string> supportedDistributions: ["arch", "cachyos", "manjaro", "endeavouros"]
readonly property var archBasedSettings: {
"listUpdatesParams": ["-Qu"],
"upgradeSettings": {
"params": ["-Syu"],
"requiresSudo": false
},
"parserSettings": {
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": match[2],
"newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}`
}
}
}
}
readonly property var packageManagerParams: {
"yay": archBasedSettings,
"paru": archBasedSettings,
"dnf": {
"listUpdatesParams": ["list", "--upgrades", "--quiet", "--color=never"],
"upgradeSettings": {
"params": ["upgrade"],
"requiresSudo": true
},
"parserSettings": {
"lineRegex": /^([^\s]+)\s+([^\s]+)\s+.*$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": "",
"newVersion": match[2],
"description": `${match[1]} ${match[2]}`
}
}
}
}
}
readonly property list<string> supportedDistributions: ["arch", "cachyos", "manjaro", "endeavouros", "fedora"]
readonly property int updateCount: availableUpdates.length
readonly property bool helperAvailable: pkgManager !== "" && distributionSupported
@@ -63,7 +104,7 @@ Singleton {
Process {
id: helperDetection
command: ["sh", "-c", "which paru || which yay"]
command: ["sh", "-c", "which paru || which yay || which dnf"]
onExited: (exitCode) => {
if (exitCode === 0) {
@@ -110,7 +151,7 @@ Singleton {
isChecking = true
hasError = false
updateChecker.command = [pkgManager, "-Qu"]
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesParams)
updateChecker.running = true
}
@@ -118,15 +159,13 @@ Singleton {
const lines = output.trim().split('\n').filter(line => line.trim())
const updates = []
const regex = packageManagerParams[pkgManager].parserSettings.lineRegex
const entryProducer = packageManagerParams[pkgManager].parserSettings.entryProducer
for (const line of lines) {
const match = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
const match = line.match(regex)
if (match) {
updates.push({
name: match[1],
currentVersion: match[2],
newVersion: match[3],
description: `${match[1]} ${match[2]} ${match[3]}`
})
updates.push(entryProducer(match))
}
}
@@ -137,7 +176,9 @@ Singleton {
if (!distributionSupported || !pkgManager || updateCount === 0) return
const terminal = Quickshell.env("TERMINAL") || "xterm"
const updateCommand = `${pkgManager} -Syu && echo "Updates complete! Press Enter to close..." && read`
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ")
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : ""
const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`
updater.command = [terminal, "-e", "sh", "-c", updateCommand]
updater.running = true
@@ -149,4 +190,4 @@ Singleton {
running: distributionSupported && pkgManager
onTriggered: checkForUpdates()
}
}
}

105
dms.spec Normal file
View File

@@ -0,0 +1,105 @@
# Spec for DMS - uses rpkg macros for both stable and git builds
%global debug_package %{nil}
%global version {{{ git_dir_version }}}
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: 1%{?dist}
Summary: %{pkg_summary}
License: GPL-3.0-only
URL: https://github.com/AvengeMedia/DankMaterialShell
VCS: {{{ git_dir_vcs }}}
Source0: {{{ git_dir_pack }}}
# dms CLI tool sources - compiled from danklinux
Source1: https://github.com/AvengeMedia/danklinux/archive/refs/heads/master.tar.gz#/danklinux-master.tar.gz
BuildRequires: git-core
BuildRequires: golang >= 1.21
BuildRequires: rpkg
# Core requirements - Shell and fonts
Requires: (quickshell or quickshell-git)
Recommends: quickshell-git
Requires: fira-code-fonts
Requires: rsms-inter-fonts
# Core utilities (REQUIRED for DMS functionality)
Requires: dgop
Requires: cava
Requires: wl-clipboard
Requires: brightnessctl
Requires: matugen
Requires: cliphist
Requires: material-symbols-fonts
# Recommended system packages
Recommends: NetworkManager
Recommends: gammastep
Recommends: qt6ct
# Auto-require the CLI sub-package
Requires: dms-cli = %{version}-%{release}
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and Hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: GPL-3.0-only
URL: https://github.com/AvengeMedia/danklinux
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%prep
{{{ git_dir_setup_macro }}}
# Extract danklinux for building dms CLI
tar -xzf %{SOURCE1} -C %{_builddir}
%build
# Compile dms CLI from danklinux source
pushd %{_builddir}/danklinux-master
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
export GOFLAGS="-buildmode=pie -trimpath -ldflags=-linkmode=external -mod=readonly -modcacherw"
go build -o dms ./cmd/dms
popd
%install
# Install dms-cli binary
install -Dm755 %{_builddir}/danklinux-master/dms %{buildroot}%{_bindir}/dms-cli
# Install shell files to XDG config location
install -dm755 %{buildroot}%{_sysconfdir}/xdg/quickshell/dms
cp -r ./* %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/
# Remove git-related files
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.git*
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.github
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_sysconfdir}/xdg/quickshell/dms/
%files -n dms-cli
%{_bindir}/dms-cli
%changelog
{{{ git_dir_changelog }}}

6
flake.lock generated
View File

@@ -27,11 +27,11 @@
]
},
"locked": {
"lastModified": 1759946376,
"narHash": "sha256-/kQpJPH1y+U6V7N3bbGzvNRGfk9VuxdZev9Os4bS5ZQ=",
"lastModified": 1759982027,
"narHash": "sha256-4deRT98VwfZWZ685wIGevyYl3CzpuZJPjdjfulABH00=",
"owner": "AvengeMedia",
"repo": "danklinux",
"rev": "98db89ffba290265bc4a886d13b8a27a53fdaca1",
"rev": "5cdfeeae2e14089079dcb0d6b61f014ce754021f",
"type": "github"
},
"original": {

View File

@@ -6,6 +6,7 @@
...
}: let
cfg = config.programs.dankMaterialShell;
jsonFormat = pkgs.formats.json { };
in {
options.programs.dankMaterialShell = with lib.types; {
enable = lib.mkEnableOption "DankMaterialShell";
@@ -54,6 +55,37 @@ in {
quickshell = {
package = lib.mkPackageOption pkgs "quickshell" {};
};
default = {
settings = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "The default settings are only read if the settings.json file don't exist";
};
session = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "The default session are only read if the session.json file don't exist";
};
};
plugins = lib.mkOption {
type = attrsOf (types.submodule ({ config, ... }: {
options = {
enable = lib.mkOption {
type = types.bool;
default = true;
description = "Whether to link this plugin";
};
src = lib.mkOption {
type = types.path;
description = "Source to link to DMS plugins directory";
};
};
}));
default = {};
description = "DMS Plugins to install";
};
};
config = lib.mkIf cfg.enable
@@ -65,14 +97,39 @@ in {
configs.dms = "${
dmsPkgs.dankMaterialShell
}/etc/xdg/quickshell/DankMaterialShell";
activeConfig = lib.mkIf cfg.enableSystemd "dms";
systemd = lib.mkIf cfg.enableSystemd {
enable = true;
target = "graphical-session.target";
};
};
systemd.user.services.dms = lib.mkIf cfg.enableSystemd {
Unit = {
Description = "DankMaterialShell";
PartOf = [ config.wayland.systemd.target ];
After = [ config.wayland.systemd.target ];
};
Service = {
ExecStart = lib.getExe dmsPkgs.dmsCli + " run";
Restart = "on-failure";
};
Install.WantedBy = [ config.wayland.systemd.target ];
};
xdg.stateFile."DankMaterialShell/default-session.json" = lib.mkIf (cfg.default.session != { }) {
source = jsonFormat.generate "default-session.json" cfg.default.session;
};
xdg.configFile = lib.mkMerge [
(lib.mapAttrs' (name: plugin: {
name = "DankMaterialShell/plugins/${name}";
value.source = plugin.src;
}) (lib.filterAttrs (n: v: v.enable) cfg.plugins))
{
"DankMaterialShell/default-settings.json" = lib.mkIf (cfg.default.settings != { }) {
source = jsonFormat.generate "default-settings.json" cfg.default.settings;
};
}
];
home.packages =
[
pkgs.material-symbols

View File

@@ -30,16 +30,16 @@
"A file with this name already exists. Do you want to overwrite it?": "この名前のファイルは既に存在します。上書きしてもよろしいですか?"
},
"About": {
"About": ""
"About": "詳細"
},
"Access clipboard history": {
"Access clipboard history": ""
"Access clipboard history": "クリップボードの履歴へのアクセス"
},
"Access to notifications and do not disturb": {
"Access to notifications and do not disturb": ""
"Access to notifications and do not disturb": "通知へのアクセスおよびサイレントモード"
},
"Access to system controls and settings": {
"Access to system controls and settings": ""
"Access to system controls and settings": "システム制御へのアクセスおよび設定"
},
"Actions": {
"Actions": "アクション"
@@ -63,7 +63,7 @@
"All displays": "すべてのディスプレイ"
},
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": {
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": ""
"Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close": "Alt+←/Backspace: 戻る • F1/I: ファイル情報 • F10: ヘルプ • Esc: 閉じる"
},
"Always Show OSD Percentage": {
"Always Show OSD Percentage": "常に OSD パーセンテージを表示"
@@ -75,10 +75,10 @@
"Animations": "アニメーション"
},
"App Launcher": {
"App Launcher": ""
"App Launcher": "アプリランチャー"
},
"Applications": {
"Applications": ""
"Applications": "アプリ"
},
"Apply": {
"Apply": "適用"
@@ -93,7 +93,7 @@
"Apps Icon": "アプリアイコン"
},
"Apps are ordered by usage frequency, then last used, then alphabetically.": {
"Apps are ordered by usage frequency, then last used, then alphabetically.": ""
"Apps are ordered by usage frequency, then last used, then alphabetically.": "アプリは使用頻度、最終使用日、アルファベット順の優先順位で並べられています。"
},
"Are you sure you want to hibernate the system?": {
"Are you sure you want to hibernate the system?": "システムを休止状態にしますか?"
@@ -171,13 +171,13 @@
"Available Screens (": "利用可能なスクリーン("
},
"Back": {
"Back": ""
"Back": "戻る"
},
"Battery": {
"Battery": ""
"Battery": "バッテリー"
},
"Battery level and power management": {
"Battery level and power management": ""
"Battery level and power management": "バッテリーレベルおよび電源管理"
},
"Battery not detected - only AC power settings available": {
"Battery not detected - only AC power settings available": "バッテリーが検出されません - AC電源設定のみ利用可能です"
@@ -192,7 +192,7 @@
"Border": "ボーダー"
},
"Brightness": {
"Brightness": ""
"Brightness": "明るさ"
},
"Browse": {
"Browse": "ブラウズ"
@@ -204,16 +204,16 @@
"CPU": "CPU"
},
"CPU Temperature": {
"CPU Temperature": ""
"CPU Temperature": "CPU温度"
},
"CPU Usage": {
"CPU Usage": ""
"CPU Usage": "CPU使用率"
},
"CPU temperature display": {
"CPU temperature display": ""
"CPU temperature display": "CPU温度表示"
},
"CPU usage indicator": {
"CPU usage indicator": ""
"CPU usage indicator": "CPU使用率インジケーター"
},
"Cancel": {
"Cancel": "キャンセル"
@@ -225,37 +225,37 @@
"Center Section": "センターセクション"
},
"Check for system updates": {
"Check for system updates": ""
"Check for system updates": "システムアップデートを検査"
},
"Choose Launcher Logo Color": {
"Choose Launcher Logo Color": ""
"Choose Launcher Logo Color": "ランチャーロゴの色を選ぶ"
},
"Choose icon": {
"Choose icon": "アイコンを選"
"Choose icon": "アイコンを選"
},
"Choose the logo displayed on the launcher button in DankBar": {
"Choose the logo displayed on the launcher button in DankBar": "Dank Barのランチャーボタンに表示されるロゴを選"
"Choose the logo displayed on the launcher button in DankBar": "Dank Barのランチャーボタンに表示されるロゴを選"
},
"Choose where notification popups appear on screen": {
"Choose where notification popups appear on screen": ""
"Choose where notification popups appear on screen": "通知ポップアップが画面に表示される場所を選ぶ"
},
"Clear": {
"Clear": "クリア"
},
"Clear All": {
"Clear All": ""
"Clear All": "すべてクリア"
},
"Clear All History?": {
"Clear All History?": ""
"Clear All History?": "すべての履歴をクリアしますか?"
},
"Clipboard History": {
"Clipboard History": ""
"Clipboard History": "クリップボード履歴"
},
"Clipboard Manager": {
"Clipboard Manager": ""
"Clipboard Manager": "クリップボード管理"
},
"Clock": {
"Clock": ""
"Clock": "時計"
},
"Close": {
"Close": "閉じる"
@@ -264,10 +264,10 @@
"Color Override": "色のオーバーライド"
},
"Color Picker": {
"Color Picker": ""
"Color Picker": "カラーピッカー"
},
"Color temperature for night mode": {
"Color temperature for night mode": ""
"Color temperature for night mode": "ナイトモードの色温度"
},
"Communication": {
"Communication": "コミュニケーション"
@@ -294,19 +294,19 @@
"Connected Displays": "接続されたディスプレイ"
},
"Contrast": {
"Contrast": ""
"Contrast": "コントラスト"
},
"Control Center": {
"Control Center": ""
"Control Center": "コントロールセンター"
},
"Control currently playing media": {
"Control currently playing media": ""
"Control currently playing media": "現在再生中のメディアを制御"
},
"Control the speed of animations throughout the interface": {
"Control the speed of animations throughout the interface": "インターフェース全体のアニメーションの速度を制御する"
"Control the speed of animations throughout the interface": "インターフェース全体のアニメーションの速度を制御"
},
"Copied to clipboard": {
"Copied to clipboard": ""
"Copied to clipboard": "クリップボードにコピーしました"
},
"Copied!": {
"Copied!": "コピーしました!"
@@ -321,7 +321,7 @@
"Copy Process Name": "プロセス名をコピー"
},
"Corner Radius (0 = square corners)": {
"Corner Radius (0 = square corners)": ""
"Corner Radius (0 = square corners)": "コーナー半径0 = 角丸なし)"
},
"Create Dir": {
"Create Dir": "ディレクトリを作成"
@@ -333,10 +333,10 @@
"Current Items": "現在のアイテム"
},
"Current time and date display": {
"Current time and date display": ""
"Current time and date display": "現在の日時を表示"
},
"Current weather conditions and temperature": {
"Current weather conditions and temperature": ""
"Current weather conditions and temperature": "現在の天気状況と気温"
},
"Custom": {
"Custom": "カスタム"
@@ -351,7 +351,7 @@
"Custom: ": "カスタム: "
},
"Customizable empty space": {
"Customizable empty space": ""
"Customizable empty space": "カスタマイズ可能な空きスペース"
},
"DEMO MODE - Click anywhere to exit": {
"DEMO MODE - Click anywhere to exit": "デモモード -任意の場所をクリックして 終了"
@@ -360,13 +360,13 @@
"DMS Plugin Manager Unavailable": "DMS プラグイン マネージャーが利用できません"
},
"DMS_SOCKET not available": {
"DMS_SOCKET not available": ""
"DMS_SOCKET not available": "DMS_SOCKETが利用できません"
},
"Daily at:": {
"Daily at:": "毎日:"
},
"Dank Bar": {
"Dank Bar": ""
"Dank Bar": "Dank Bar"
},
"Dank Bar Transparency": {
"Dank Bar Transparency": "Dank Barの透明性"
@@ -384,7 +384,7 @@
"Date Format": "日付形式"
},
"Default": {
"Default": ""
"Default": "デフォルト"
},
"Defaults": {
"Defaults": "デフォルト"
@@ -405,7 +405,7 @@
"Disk": "ディスク"
},
"Disk Usage": {
"Disk Usage": ""
"Disk Usage": "ディスク使用率"
},
"Display Settings": {
"Display Settings": "表示設定"
@@ -417,22 +417,22 @@
"Display all priorities over fullscreen apps": "フルスクリーンアプリよりもすべての優先度を表示する"
},
"Display currently focused application title": {
"Display currently focused application title": ""
"Display currently focused application title": "現在フォーカスされているアプリケーションのタイトルを表示"
},
"Display volume and brightness percentage values by default in OSD popups": {
"Display volume and brightness percentage values by default in OSD popups": ""
"Display volume and brightness percentage values by default in OSD popups": "OSDポップアップに音量と輝度のパーセンテージ値をデフォルトで表示"
},
"Displays": {
"Displays": ""
"Displays": "表示"
},
"Displays the active keyboard layout and allows switching": {
"Displays the active keyboard layout and allows switching": ""
"Displays the active keyboard layout and allows switching": "アクティブなキーボードレイアウトを表示、切り替えを可能に"
},
"Do Not Disturb": {
"Do Not Disturb": "サイレントモード"
},
"Dock": {
"Dock": ""
"Dock": "ドック"
},
"Dock Position": {
"Dock Position": "ドック位置"
@@ -441,13 +441,13 @@
"Dock Transparency": "ドックの透明度"
},
"Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely.": {
"Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely.": ""
"Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely.": "ウィジェットをドラッグしてセクション内で順序を変更できます。目のアイコンでウィジェットを表示/非表示にスペースは維持、Xで完全に削除できます。"
},
"Dynamic Theming": {
"Dynamic Theming": "ダイナミックテーマ"
},
"Edge Spacing (0 = edge-to-edge)": {
"Edge Spacing (0 = edge-to-edge)": ""
"Edge Spacing (0 = edge-to-edge)": "エッジ間隔0 = エッジ・トゥ・エッジ)"
},
"Education": {
"Education": "教育"
@@ -480,7 +480,7 @@
"Exclusive Zone Offset": "排他ゾーンオフセット"
},
"F1/I: Toggle • F10: Help": {
"F1/I: Toggle • F10: Help": ""
"F1/I: Toggle • F10: Help": "F1/I: 切り替え • F10: ヘルプ"
},
"Feels Like": {
"Feels Like": "どうやら"
@@ -498,7 +498,7 @@
"Find in note...": "メモで検索..."
},
"Focused Window": {
"Focused Window": ""
"Focused Window": "フォーカスされたウィンドウ"
},
"Font Family": {
"Font Family": "フォントファミリー"
@@ -534,13 +534,13 @@
"Fun": "娯楽"
},
"GPU": {
"GPU": "グラフィックプロセッサ"
"GPU": "GPU"
},
"GPU Temperature": {
"GPU Temperature": ""
"GPU Temperature": "GPU温度"
},
"GPU temperature display": {
"GPU temperature display": ""
"GPU temperature display": "GPU温度表示"
},
"Games": {
"Games": "ゲーム"
@@ -585,7 +585,7 @@
"Hour": "時間"
},
"How often to change wallpaper": {
"How often to change wallpaper": ""
"How often to change wallpaper": "壁紙を切り替える間隔"
},
"Humidity": {
"Humidity": "湿度"
@@ -597,7 +597,7 @@
"Icon Theme": "アイコンテーマ"
},
"Idle Inhibitor": {
"Idle Inhibitor": ""
"Idle Inhibitor": "アイドルインヒビター"
},
"Idle Settings": {
"Idle Settings": "アイドル設定"
@@ -606,7 +606,7 @@
"Idle monitoring not supported - requires newer Quickshell version": "アイドル監視はサポートされていません - 新しい Quickshell バージョンが必要です"
},
"Image": {
"Image": ""
"Image": "画像"
},
"Include Transitions": {
"Include Transitions": "トランジションを含める"
@@ -624,10 +624,10 @@
"Interval": "間隔"
},
"Invert on mode change": {
"Invert on mode change": ""
"Invert on mode change": "モード変更時に反転"
},
"Keyboard Layout Name": {
"Keyboard Layout Name": ""
"Keyboard Layout Name": "キーボードレイアウト名"
},
"Kill Process": {
"Kill Process": "プロセスを強制終了"
@@ -642,13 +642,13 @@
"Launch": "起動"
},
"Launch Prefix": {
"Launch Prefix": ""
"Launch Prefix": "起動プリフィックス"
},
"Launch on dGPU": {
"Launch on dGPU": "dGPUで起動"
},
"Launcher": {
"Launcher": ""
"Launcher": "ランチャー"
},
"Launcher Button Logo": {
"Launcher Button Logo": "ランチャーのボタンロゴ"
@@ -678,7 +678,7 @@
"Log Out": "ログアウト"
},
"Long Text": {
"Long Text": ""
"Long Text": "長文"
},
"Longitude": {
"Longitude": "経度"
@@ -693,7 +693,7 @@
"Manual Coordinates": "手動座標"
},
"Manual Show/Hide": {
"Manual Show/Hide": ""
"Manual Show/Hide": "手動で表示/非表示"
},
"Material Colors": {
"Material Colors": "Material Colors"
@@ -708,7 +708,7 @@
"Media": "メディア"
},
"Media Controls": {
"Media Controls": ""
"Media Controls": "メディアコントロール"
},
"Media Player Settings": {
"Media Player Settings": "メディアプレーヤーの設定"
@@ -720,10 +720,10 @@
"Memory": "メモリ"
},
"Memory Usage": {
"Memory Usage": ""
"Memory Usage": "メモリ使用率"
},
"Memory usage indicator": {
"Memory usage indicator": ""
"Memory usage indicator": "メモリ使用率インジケーター"
},
"Minute": {
"Minute": "分"
@@ -744,7 +744,7 @@
"Mount": "マウント"
},
"NM not supported": {
"NM not supported": ""
"NM not supported": "NMが利用できません"
},
"Named Workspace Icons": {
"Named Workspace Icons": "名前付きワークスペースアイコン"
@@ -768,10 +768,10 @@
"Network Settings": "ネットワーク設定"
},
"Network Speed Monitor": {
"Network Speed Monitor": ""
"Network Speed Monitor": "ネットワーク速度モニター"
},
"Network download and upload speed display": {
"Network download and upload speed display": ""
"Network download and upload speed display": "ネットワークのダウンロードおよびアップロード速度を表示"
},
"New": {
"New": "新しい"
@@ -813,7 +813,7 @@
"No plugins found": "プラグインが見つかりませんでした"
},
"No plugins found.": {
"No plugins found.": ""
"No plugins found.": "プラグインが見つかりませんでした。"
},
"Normal Priority": {
"Normal Priority": "通常の優先度"
@@ -828,7 +828,7 @@
"Nothing to see here": "ここには何もありません"
},
"Notification Center": {
"Notification Center": ""
"Notification Center": "通知センター"
},
"Notification Overlay": {
"Notification Overlay": "通知オーバーレイ"
@@ -885,19 +885,19 @@
"Per-Monitor Workspaces": "モニターごとのワークスペース"
},
"Percentage": {
"Percentage": ""
"Percentage": "百分率"
},
"Personalization": {
"Personalization": ""
"Personalization": "パーソナライゼーション"
},
"Pin to Dock": {
"Pin to Dock": ""
"Pin to Dock": "ドックにピン留め"
},
"Place plugin directories here. Each plugin should have a plugin.json manifest file.": {
"Place plugin directories here. Each plugin should have a plugin.json manifest file.": "プラグインディレクトリをここに配置します。各プラグインには plugin.json マニフェストファイルが必要です。"
},
"Place plugins in": {
"Place plugins in": ""
"Place plugins in": "プラグインを配置する場所"
},
"Plugin Directory": {
"Plugin Directory": "プラグインディレクトリ"
@@ -906,10 +906,10 @@
"Plugin Management": "プラグイン管理"
},
"Plugin is disabled - enable in Plugins settings to use": {
"Plugin is disabled - enable in Plugins settings to use": ""
"Plugin is disabled - enable in Plugins settings to use": "プラグインは無効です - 使用するにはプラグイン設定で有効にしてください"
},
"Plugins": {
"Plugins": ""
"Plugins": "プラグイン"
},
"Popup Position": {
"Popup Position": "ポップアップの位置"
@@ -921,7 +921,7 @@
"Position": "位置"
},
"Power": {
"Power": ""
"Power": "電源"
},
"Power Off": {
"Power Off": "電源オフ"
@@ -936,13 +936,13 @@
"Pressure": "プレッシャー"
},
"Prevent screen timeout": {
"Prevent screen timeout": ""
"Prevent screen timeout": "画面のタイムアウトを防止"
},
"Primary": {
"Primary": ""
"Primary": "プライマリー"
},
"Privacy Indicator": {
"Privacy Indicator": ""
"Privacy Indicator": "プライバシーインジケーター"
},
"Process": {
"Process": "プロセス"
@@ -951,13 +951,13 @@
"QML (Qt Modeling Language)": "QML (Qt モデリング言語)"
},
"Quick access to application launcher": {
"Quick access to application launcher": ""
"Quick access to application launcher": "アプリケーションランチャーへのクイックアクセス"
},
"Quick access to color picker": {
"Quick access to color picker": ""
"Quick access to color picker": "カラーピッカーへのクイックアクセス"
},
"Quick access to notepad": {
"Quick access to notepad": ""
"Quick access to notepad": "メモ帳へのクイックアクセス"
},
"Rain Chance": {
"Rain Chance": "降水確率"
@@ -969,7 +969,7 @@
"Recent Colors": "最近の色"
},
"Recently Used Apps": {
"Recently Used Apps": ""
"Recently Used Apps": "最近使用したアプリ"
},
"Refresh": {
"Refresh": "リフレッシュ"
@@ -981,7 +981,7 @@
"Reset": "リセット"
},
"Running Apps": {
"Running Apps": ""
"Running Apps": "実行中のアプリ"
},
"Running Apps Only In Current Workspace": {
"Running Apps Only In Current Workspace": "現在のワークスペースでのみアプリを実行する"
@@ -1020,34 +1020,34 @@
"Search...": "検索..."
},
"Select Launcher Logo": {
"Select Launcher Logo": "ランチャーロゴを選"
"Select Launcher Logo": "ランチャーロゴを選"
},
"Select a color from the palette or use custom sliders": {
"Select a color from the palette or use custom sliders": "パレットから色を選択するか、カスタムスライダーを使用します"
"Select a color from the palette or use custom sliders": "パレットから色を選か、カスタムスライダーを使用します"
},
"Select a widget to add to the ": {
"Select a widget to add to the ": "追加するウィジェットを選択してください "
"Select a widget to add to the ": "追加するウィジェットを選 "
},
"Select an image file...": {
"Select an image file...": "画像ファイルを選..."
"Select an image file...": "画像ファイルを選..."
},
"Select font weight": {
"Select font weight": ""
"Select font weight": "フォントの太さを選ぶ"
},
"Select monitor to configure wallpaper": {
"Select monitor to configure wallpaper": ""
"Select monitor to configure wallpaper": "壁紙を設定するモニターを選ぶ"
},
"Select monospace font for process list and technical displays": {
"Select monospace font for process list and technical displays": ""
"Select monospace font for process list and technical displays": "プロセスリストと技術表示用の等幅フォントを選ぶ"
},
"Select system font family": {
"Select system font family": ""
"Select system font family": "システムフォントファミリーを選ぶ"
},
"Select which transitions to include in randomization": {
"Select which transitions to include in randomization": "含めたいトランジションをランダム化に選択"
},
"Separator": {
"Separator": ""
"Separator": "区切り"
},
"Set different wallpapers for each connected monitor": {
"Set different wallpapers for each connected monitor": "接続されているモニターごとに異なる壁紙を設定する"
@@ -1077,7 +1077,7 @@
"Show on Overview": "概要に表示"
},
"Show on all connected displays": {
"Show on all connected displays": ""
"Show on all connected displays": "すべての接続されたディスプレイに表示"
},
"Show on screens:": {
"Show on screens:": "画面に表示:"
@@ -1089,28 +1089,28 @@
"Show weather information in top bar and control center": "トップバーとコントロールセンターに天気情報を表示"
},
"Shows all running applications with focus indication": {
"Shows all running applications with focus indication": ""
"Shows all running applications with focus indication": "実行中のすべてのアプリケーションをフォーカス状態で表示"
},
"Shows current workspace and allows switching": {
"Shows current workspace and allows switching": ""
"Shows current workspace and allows switching": "現在のワークスペースを表示、切り替えを可能に"
},
"Shows when microphone, camera, or screen sharing is active": {
"Shows when microphone, camera, or screen sharing is active": ""
"Shows when microphone, camera, or screen sharing is active": "マイク、カメラ、または画面共有がアクティブなときに表示"
},
"Size": {
"Size": "サイズ"
},
"Size Offset": {
"Size Offset": ""
"Size Offset": "サイズオフセット"
},
"Spacer": {
"Spacer": ""
"Spacer": "間隔"
},
"Spacing": {
"Spacing": "間隔"
},
"Square Corners": {
"Square Corners": "コーナーを四角くする"
"Square Corners": "四角コーナー"
},
"Start": {
"Start": "始める"
@@ -1125,7 +1125,7 @@
"Storage & Disks": "ストレージとディスク"
},
"Surface": {
"Surface": ""
"Surface": "表面"
},
"Suspend": {
"Suspend": "一時停止"
@@ -1155,19 +1155,19 @@
"System Monitoring:": "システム監視:"
},
"System Tray": {
"System Tray": ""
"System Tray": "システムトレイ"
},
"System Update": {
"System Update": ""
"System Update": "システムアップデート"
},
"System Updates": {
"System Updates": "システムアップデート"
},
"System notification area icons": {
"System notification area icons": ""
"System notification area icons": "システム通知エリアアイコン"
},
"Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select": {
"Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select": ""
"Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select": "Tab/Shift+Tab: ナビゲーション • ←→↑↓: グリッドナビゲーション • Enter/Space: 選択"
},
"Technical Details": {
"Technical Details": "技術的な詳細"
@@ -1176,16 +1176,16 @@
"Temperature": "温度"
},
"Text": {
"Text": ""
"Text": "テキスト"
},
"The DMS_SOCKET environment variable is not set or the socket is unavailable. Automated plugin management requires the DMS_SOCKET.": {
"The DMS_SOCKET environment variable is not set or the socket is unavailable. Automated plugin management requires the DMS_SOCKET.": "DMS_SOCKET環境変数が設定されていないか、ソケットが利用できません。自動プラグイン管理にはDMS_SOCKETが必要です。"
},
"The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).": {
"The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).": ""
"The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).": "以下の設定は、GTKとQtの設定を変更します。現在の設定を維持したい場合は、バックアップを作成してくださいqt5ct.conf|qt6ct.conf および ~/.config/gtk-3.0|gtk-4.0)。"
},
"Theme & Colors": {
"Theme & Colors": ""
"Theme & Colors": "テーマおよびカラー"
},
"Theme Color": {
"Theme Color": "テーマカラー"
@@ -1194,16 +1194,16 @@
"Third-Party Plugin Warning": "サードパーティ製プラグインの警告"
},
"Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\\n\\nThese plugins may pose security and privacy risks - install at your own risk.": {
"Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\\n\\nThese plugins may pose security and privacy risks - install at your own risk.": ""
"Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\\n\\nThese plugins may pose security and privacy risks - install at your own risk.": "サードパーティプラグインはコミュニティによって作成されており、DankMaterialShellによる公式サポートはありません。\\n\\nこれらのプラグインはセキュリティやプライバシーのリスクをもたらす可能性があります - 自己責任でインストールしてください。"
},
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": {
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": ""
"This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.": "このウィジェットはGPUの省電力状態を防ぎ、ートパソコンのバッテリー寿命に大きな影響を与える可能性があります。ハイブリッドグラフィックス搭載のートパソコンでの使用は推奨されません。"
},
"This will permanently delete all clipboard history.": {
"This will permanently delete all clipboard history.": ""
"This will permanently delete all clipboard history.": "これはすべてのクリップボード履歴を完全に削除します。"
},
"Time & Date": {
"Time & Date": ""
"Time & Date": "時間と日付"
},
"Today": {
"Today": "今日"
@@ -1224,7 +1224,7 @@
"Turn off monitors after": "後にモニターの電源を切る"
},
"Unpin from Dock": {
"Unpin from Dock": ""
"Unpin from Dock": "ドックから固定を解除"
},
"Unsaved Changes": {
"Unsaved Changes": "保存されていない変更"
@@ -1242,7 +1242,7 @@
"Update All": "すべて更新"
},
"Use 24-hour time format instead of 12-hour AM/PM": {
"Use 24-hour time format instead of 12-hour AM/PM": ""
"Use 24-hour time format instead of 12-hour AM/PM": "12時間制のAM/PMではなく、24時間表記を使用"
},
"Use Fahrenheit": {
"Use Fahrenheit": "華氏を使用する"
@@ -1254,7 +1254,7 @@
"Use Monospace Font": "等幅フォントを使用"
},
"Use light theme instead of dark theme": {
"Use light theme instead of dark theme": ""
"Use light theme instead of dark theme": "ダークテーマではなく、ライトテーマを使用"
},
"Use%": {
"Use%": "使用%"
@@ -1263,28 +1263,28 @@
"Used": "使用済み"
},
"Uses sunrise/sunset times to automatically adjust night mode based on your location.": {
"Uses sunrise/sunset times to automatically adjust night mode based on your location.": ""
"Uses sunrise/sunset times to automatically adjust night mode based on your location.": "あなたの位置情報に基づいて、日の出と日の入りの時間を使ってナイトモードを自動調整します。"
},
"Utilities": {
"Utilities": "ユーティリティ"
},
"VPN": {
"VPN": ""
"VPN": "VPN"
},
"VPN Connections": {
"VPN Connections": "VPN接続"
},
"VPN status and quick connect": {
"VPN status and quick connect": ""
"VPN status and quick connect": "VPNステータスとクイック接続"
},
"Visibility": {
"Visibility": "可視性"
},
"Visual divider between widgets": {
"Visual divider between widgets": ""
"Visual divider between widgets": "ウィジェット間の視覚的分離"
},
"Visual effect used when wallpaper changes": {
"Visual effect used when wallpaper changes": ""
"Visual effect used when wallpaper changes": "壁紙が変更される時に使用されるビジュアルエフェクト"
},
"Wallpaper": {
"Wallpaper": "壁紙"
@@ -1293,10 +1293,10 @@
"Wave Progress Bars": "ウェーブプログレスバー"
},
"Weather": {
"Weather": ""
"Weather": "天気"
},
"Weather Widget": {
"Weather Widget": ""
"Weather Widget": "天気ウィジェット"
},
"WiFi is off": {
"WiFi is off": "Wi-Fiはオフ中"
@@ -1308,7 +1308,7 @@
"Widget Styling": "ウィジェットのスタイル"
},
"Widgets": {
"Widgets": ""
"Widgets": "ウィジェット"
},
"Wind": {
"Wind": "風"
@@ -1326,7 +1326,7 @@
"Workspace Settings": "ワークスペース設定"
},
"Workspace Switcher": {
"Workspace Switcher": ""
"Workspace Switcher": "ワークスペーススイッチャー"
},
"You have unsaved changes. Save before closing this tab?": {
"You have unsaved changes. Save before closing this tab?": "保存されていない変更があります。このタブを閉じる前に保存しますか?"
@@ -1347,7 +1347,7 @@
"official": "公式"
},
"update dms for NM integration.": {
"update dms for NM integration.": ""
"update dms for NM integration.": "NM統合のためにDMSを更新します。"
},
"• Install only from trusted sources": {
"• Install only from trusted sources": "• 信頼できるソースからのみインストールする"

File diff suppressed because it is too large Load Diff