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

Compare commits

...

86 Commits

Author SHA1 Message Date
bbedward
a3ada5b2bb Restructure lock screen 2025-10-13 17:44:09 -04:00
bbedward
3e167a2c52 Update localizations 2025-10-13 17:05:45 -04:00
bbedward
38b3ad2b31 niri: re-gen layout kdl on dbar spacing change 2025-10-13 17:03:08 -04:00
bbedward
56e5cd13b7 Add popup gaps override + math min 4 logic to match existing code 2025-10-13 16:57:18 -04:00
bbedward
d63c0fc6f0 Simplify font picking and elide contents 2025-10-13 14:58:24 -04:00
bbedward
6814b140fc bar: more repaint triggers + repaint debounce 2025-10-13 14:38:03 -04:00
max72bra
5f7e478118 Enhancement: custom system updater command (#414)
* enhancement: system updater custom command and custom additional terminal parameters

* removed console log

* minor: tabs
2025-10-13 14:25:25 -04:00
Bruno Cesar Rocha
7317024da5 feat: Launcher Plugin Component (#408)
Load launcher items from plugin.
2025-10-13 14:24:41 -04:00
Body
9b9fbabc3f tweak popout margins for consistency (#399) 2025-10-13 14:17:02 -04:00
bbedward
3c5a23799f Flip light/dark mode and icon themes properly 2025-10-13 14:16:36 -04:00
bbedward
3bfdc6163c Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-13 13:54:54 -04:00
purian23
cf2f74a38d Stock workflow release 2025-10-12 15:23:00 -04:00
bbedward
2a7f52c67e Update readme 2025-10-12 07:46:43 -04:00
Body
50fde1e308 Add niri overview toggle on launcher button rightclick. (#394) 2025-10-12 07:38:55 -04:00
max72bra
46fd0ae413 Hyprland: filter all spacial workspaces (#398)
Filters all workspaces with IDs less than 0, regardless of their name (which is user-defined)
2025-10-12 07:38:39 -04:00
bokicoder
65e32dc429 Fix keyboard focus issue in clipboard (#391) 2025-10-12 07:38:15 -04:00
Body
413675dfc1 Close ControlCenter popout on Settings open. (#396) 2025-10-12 07:34:06 -04:00
purian23
a17343f40e Enable Copr stable builds 2025-10-12 01:08:10 -04:00
bokicoder
5d023804c1 update flake.lock (#390) 2025-10-12 00:12:41 -04:00
bbedward
fa07a846b9 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-11 23:48:36 -04:00
bbedward
5df46b605e Call lockerReady to releasse inhibitor whenlock screen is drawn 2025-10-11 23:48:13 -04:00
bokicoder
77cf371a21 update flake.lock (#388) 2025-10-11 23:42:55 -04:00
bbedward
89802dd040 Presever user configs 2025-10-11 23:08:11 -04:00
bbedward
59b95e9dd6 Bundle distro package with dms releases 2025-10-11 22:21:55 -04:00
bbedward
b836db5252 Update copr-related build opts 2025-10-11 21:59:08 -04:00
purian23
4dc4b15925 Revert "Update spec to SRPM by default"
This reverts commit 5c3062e699.
2025-10-11 18:19:21 -04:00
purian23
5c3062e699 Update spec to SRPM by default 2025-10-11 17:52:26 -04:00
bbedward
3a7777c643 api: unify dms API clients
- Single subscribe socket
- Single req/callback socket
- Remove PrepareForSleep handling
- Dependency on dms API version enforcement
- Remove gdbus from portal and session
2025-10-11 14:37:18 -04:00
bbedward
71543c35d6 Re-organize settings 2025-10-11 13:04:26 -04:00
bbedward
f4cf66dc01 Update terms 2025-10-11 11:37:09 -04:00
bbedward
7870dff0fd matugen: opt to run user templates 2025-10-11 11:30:19 -04:00
bbedward
9fc9c1ed19 Update issue template 2025-10-11 10:39:34 -04:00
bbedward
4d0151350f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-11 10:30:19 -04:00
bbedward
dff10c8d13 Update issue templates 2025-10-11 10:30:09 -04:00
purian23
362bcb9294 More cli - better spec 2025-10-11 02:14:48 -04:00
purian23
351b4f8a94 Remove CLI ref 2025-10-11 01:59:59 -04:00
bbedward
90955eb0a1 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-10 20:07:19 -04:00
bbedward
62669747ad lock/greeter: show full power menu 2025-10-10 20:07:01 -04:00
bokicoder
466d00c666 Modify the greeter module to use the dms-greeter wrapper (#373) 2025-10-10 19:20:17 -04:00
bbedward
63845ff875 Update vid 2025-10-10 18:53:52 -04:00
bbedward
d013748a51 plugins: fix variant syncing 2025-10-10 18:45:43 -04:00
bbedward
474af3bc07 Remove braindead mouse area arbitrary rules 2025-10-10 18:21:35 -04:00
bbedward
8e09e155fa greeter: update readme 2025-10-10 17:47:02 -04:00
bbedward
7f9f4f96b9 Update greeter readme 2025-10-10 17:44:06 -04:00
bbedward
cd488a8623 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-10 15:01:10 -04:00
bbedward
080b7a28b1 Fix force focus 2025-10-10 15:00:57 -04:00
purian23
6949ed0ebd No inline package comments 2025-10-10 14:57:32 -04:00
purian23
8465fa45bb Adjust inline spec comments 2025-10-10 14:49:34 -04:00
purian23
40835ffc89 Update copr dms spec 2025-10-10 14:20:47 -04:00
bbedward
01a42ff330 Fix key forward targets 2025-10-10 13:51:17 -04:00
bbedward
ba49654a64 Fix launcher context menu sizing 2025-10-10 13:42:55 -04:00
bbedward
bc6577fe18 Fix icon theme overflow 2025-10-10 12:47:22 -04:00
bbedward
4ca3f0da67 Fix tab/backtab in launchers 2025-10-10 12:40:02 -04:00
bbedward
7f2086488b Fix power menu focus activation 2025-10-10 12:29:14 -04:00
bbedward
3014fd8095 Fractional scaling fixes + bar border settings 2025-10-10 12:25:00 -04:00
bbedward
27885c8ac3 Update readme and about page 2025-10-10 00:05:49 -04:00
bbedward
d6be0509ac Put release bin in bin/ folder 2025-10-09 23:32:20 -04:00
bbedward
1c85f5e857 Make release assets more sane and understandable 2025-10-09 23:28:10 -04:00
bbedward
abe5515aca Expose ensureVisible to PluginSettings 2025-10-09 23:07:42 -04:00
bbedward
6fba975490 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-09 20:38:26 -04:00
bbedward
2de6798f45 Clean up variants from bar whhen removed from plugin data 2025-10-09 20:37:14 -04:00
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
86 changed files with 8715 additions and 3445 deletions

View File

@@ -26,6 +26,14 @@ assignees: ""
- [ ] Hyprland
- [ ] Other (specify)
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the issue -->
@@ -45,6 +53,14 @@ assignees: ""
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
<!-- you can get a log file with the following steps:
dms kill
mkdir ~/dms_logs
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
-->
```
Paste error messages or logs here

View File

@@ -12,6 +12,14 @@ assignees: ""
- [ ] Hyprland
- [ ] other
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the support needed -->

View File

@@ -57,7 +57,28 @@ jobs:
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" "${PREVIOUS_TAG}..${TAG}")
fi
cat > CHANGELOG.md << EOF
cat > RELEASE_BODY.md << 'EOF'
## Assets
### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binary + QML source + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binary + QML source + installation guide)
### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-qml.tar.gz`** - QML source code only
### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity
**Installation:** Extract the `dms-full-*.tar.gz` package for your architecture and follow the `INSTALL.md` instructions inside.
---
EOF
cat >> RELEASE_BODY.md << EOF
## What's Changed
$CHANGELOG
@@ -66,7 +87,7 @@ jobs:
EOF
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create/Update DankMaterialShell Release
@@ -80,18 +101,113 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download DMS release assets
- name: Download and prepare release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euxo pipefail
# gh is preinstalled on ubuntu-24.04; auth via GH_TOKEN env
mkdir -p _release_assets
# Download DMS CLI binaries from the danklinux repo
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
- name: Attach DMS assets to DankMaterialShell release
# Rename CLI binaries to dms-cli-* format
for file in _dms_assets/dms-*.gz*; do
if [ -f "$file" ]; then
basename=$(basename "$file")
# dms-amd64.gz -> dms-cli-amd64.gz
# dms-amd64.gz.sha256 -> dms-cli-amd64.gz.sha256
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
cp "$file" "_release_assets/$newname"
fi
done
# Create QML source package (exclude .git, .github, build artifacts)
tar --exclude='.git' \
--exclude='.github' \
--exclude='_dms_assets' \
--exclude='_release_assets' \
--exclude='*.tar.gz' \
-czf _release_assets/dms-qml.tar.gz .
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each architecture
for arch in amd64 arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
# Extract QML source to temp directory
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
# Copy CLI binary if it exists
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
chmod +x _temp_full/bin/dms
fi
# Create INSTALL.md
cat > _temp_full/INSTALL.md << 'EOF'
# DankMaterialShell Installation
## Requirements
- Wayland compositor (niri or Hyprland recommended)
- Quickshell framework
- Qt6
## Installation Steps
1. **Install quickshell assets:**
```bash
mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
2. **Install the DMS CLI binary:**
```bash
sudo install -m 755 bin/dms /usr/local/bin/dms
# or install to a local directory:
mkdir -p ~/.local/bin
install -m 755 bin/dms ~/.local/bin/dms
```
3. **Start the shell:**
```bash
dms run
# or directly with quickshell (will lack some dbus integrations & plugin management):
quickshell -p ~/.config/quickshell/dms
```
## Configuration
- Settings are stored in `~/.config/DankMaterialShell/settings.json`
- Plugins go in `~/.config/DankMaterialShell/plugins/`
- See the documentation in the `dms/` directory for more details
## Troubleshooting
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
- Check logs in `~/.local/state/DankMaterialShell/`
- Ensure all dependencies are installed
EOF
# Create the full package
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
# Generate checksum
(cd _release_assets && sha256sum "dms-full-${arch}.tar.gz" > "dms-full-${arch}.tar.gz.sha256")
# Cleanup
rm -rf _temp_full
done
- name: Attach all assets to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
files: _dms_assets/**
files: _release_assets/**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

@@ -33,6 +33,7 @@ Singleton {
property string currentThemeName: "blue"
property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot"
property bool runUserMatugenTemplates: true
property real dankBarTransparency: 1.0
property real dankBarWidgetTransparency: 1.0
property real popupTransparency: 1.0
@@ -155,6 +156,16 @@ Singleton {
property bool dankBarNoBackground: false
property bool dankBarGothCornersEnabled: false
property bool dankBarBorderEnabled: false
property string dankBarBorderColor: "surfaceText"
property real dankBarBorderOpacity: 1.0
property real dankBarBorderThickness: 1
property bool popupGapsAuto: true
property int popupGapsManual: 4
onDankBarBorderColorChanged: saveSettings()
onDankBarBorderOpacityChanged: saveSettings()
onDankBarBorderThicknessChanged: saveSettings()
property int dankBarPosition: SettingsData.Position.Top
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
property bool lockScreenShowPowerActions: true
@@ -166,6 +177,9 @@ Singleton {
property int notificationTimeoutCritical: 0
property int notificationPopupPosition: SettingsData.Position.Top
property bool osdAlwaysShowValue: false
property bool updaterUseCustomCommand: false
property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: ""
property var screenPreferences: ({})
property int animationSpeed: SettingsData.AnimationSpeed.Short
readonly property string defaultFontFamily: "Inter Variable"
@@ -275,6 +289,7 @@ Singleton {
}
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
runUserMatugenTemplates = settings.runUserMatugenTemplates !== undefined ? settings.runUserMatugenTemplates : true
dankBarTransparency = settings.dankBarTransparency !== undefined ? (settings.dankBarTransparency > 1 ? settings.dankBarTransparency / 100 : settings.dankBarTransparency) : (settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 1.0)
dankBarWidgetTransparency = settings.dankBarWidgetTransparency !== undefined ? (settings.dankBarWidgetTransparency > 1 ? settings.dankBarWidgetTransparency / 100 : settings.dankBarWidgetTransparency) : (settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 1.0)
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 1.0
@@ -400,6 +415,9 @@ Singleton {
notificationTimeoutCritical = settings.notificationTimeoutCritical !== undefined ? settings.notificationTimeoutCritical : 0
notificationPopupPosition = settings.notificationPopupPosition !== undefined ? settings.notificationPopupPosition : SettingsData.Position.Top
osdAlwaysShowValue = settings.osdAlwaysShowValue !== undefined ? settings.osdAlwaysShowValue : false
updaterUseCustomCommand = settings.updaterUseCustomCommand !== undefined ? settings.updaterUseCustomCommand : false;
updaterCustomCommand = settings.updaterCustomCommand !== undefined ? settings.updaterCustomCommand : "";
updaterTerminalAdditionalParams = settings.updaterTerminalAdditionalParams !== undefined ? settings.updaterTerminalAdditionalParams : "";
dankBarSpacing = settings.dankBarSpacing !== undefined ? settings.dankBarSpacing : (settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4)
dankBarBottomGap = settings.dankBarBottomGap !== undefined ? settings.dankBarBottomGap : (settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0)
dankBarInnerPadding = settings.dankBarInnerPadding !== undefined ? settings.dankBarInnerPadding : (settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 4)
@@ -407,6 +425,11 @@ Singleton {
dankBarNoBackground = settings.dankBarNoBackground !== undefined ? settings.dankBarNoBackground : (settings.topBarNoBackground !== undefined ? settings.topBarNoBackground : false)
dankBarGothCornersEnabled = settings.dankBarGothCornersEnabled !== undefined ? settings.dankBarGothCornersEnabled : (settings.topBarGothCornersEnabled !== undefined ? settings.topBarGothCornersEnabled : false)
dankBarBorderEnabled = settings.dankBarBorderEnabled !== undefined ? settings.dankBarBorderEnabled : false
dankBarBorderColor = settings.dankBarBorderColor !== undefined ? settings.dankBarBorderColor : "surfaceText"
dankBarBorderOpacity = settings.dankBarBorderOpacity !== undefined ? settings.dankBarBorderOpacity : 1.0
dankBarBorderThickness = settings.dankBarBorderThickness !== undefined ? settings.dankBarBorderThickness : 1
popupGapsAuto = settings.popupGapsAuto !== undefined ? settings.popupGapsAuto : true
popupGapsManual = settings.popupGapsManual !== undefined ? settings.popupGapsManual : 4
dankBarPosition = settings.dankBarPosition !== undefined ? settings.dankBarPosition : (settings.dankBarAtBottom !== undefined ? (settings.dankBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : (settings.topBarAtBottom !== undefined ? (settings.topBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : SettingsData.Position.Top))
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
hideBrightnessSlider = settings.hideBrightnessSlider !== undefined ? settings.hideBrightnessSlider : false
@@ -441,6 +464,7 @@ Singleton {
"currentThemeName": currentThemeName,
"customThemeFile": customThemeFile,
"matugenScheme": matugenScheme,
"runUserMatugenTemplates": runUserMatugenTemplates,
"dankBarTransparency": dankBarTransparency,
"dankBarWidgetTransparency": dankBarWidgetTransparency,
"popupTransparency": popupTransparency,
@@ -533,6 +557,11 @@ Singleton {
"dankBarNoBackground": dankBarNoBackground,
"dankBarGothCornersEnabled": dankBarGothCornersEnabled,
"dankBarBorderEnabled": dankBarBorderEnabled,
"dankBarBorderColor": dankBarBorderColor,
"dankBarBorderOpacity": dankBarBorderOpacity,
"dankBarBorderThickness": dankBarBorderThickness,
"popupGapsAuto": popupGapsAuto,
"popupGapsManual": popupGapsManual,
"dankBarPosition": dankBarPosition,
"lockScreenShowPowerActions": lockScreenShowPowerActions,
"hideBrightnessSlider": hideBrightnessSlider,
@@ -543,6 +572,9 @@ Singleton {
"notificationTimeoutCritical": notificationTimeoutCritical,
"notificationPopupPosition": notificationPopupPosition,
"osdAlwaysShowValue": osdAlwaysShowValue,
"updaterUseCustomCommand": updaterUseCustomCommand,
"updaterCustomCommand": updaterCustomCommand,
"updaterTerminalAdditionalParams": updaterTerminalAdditionalParams,
"screenPreferences": screenPreferences,
"animationSpeed": animationSpeed
}, null, 2))
@@ -584,6 +616,21 @@ Singleton {
saveSettings()
}
function setUpdaterUseCustomCommandEnabled(enabled) {
updaterUseCustomCommand = enabled;
saveSettings();
}
function setUpdaterCustomCommand(command) {
updaterCustomCommand = command;
saveSettings();
}
function setUpdaterTerminalAdditionalParams(customArgs) {
updaterTerminalAdditionalParams = customArgs;
saveSettings();
}
function setWorkspaceNameIcon(workspaceName, iconData) {
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
iconMap[workspaceName] = iconData
@@ -697,6 +744,18 @@ Singleton {
}
}
function setRunUserMatugenTemplates(enabled) {
if (runUserMatugenTemplates === enabled)
return
runUserMatugenTemplates = enabled
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setDankBarTransparency(transparency) {
dankBarTransparency = transparency
saveSettings()
@@ -969,21 +1028,21 @@ Singleton {
updateQtIconTheme(themeName)
saveSettings()
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
Theme.generateSystemThemes()
Theme.generateSystemThemesFromCurrentTheme()
}
function updateGtkIconTheme(themeName) {
var gtkThemeName = (themeName === "System Default") ? systemDefaultIconTheme : themeName
if (gtkThemeName !== "System Default" && gtkThemeName !== "") {
var script = "if command -v gsettings >/dev/null 2>&1 && gsettings list-schemas | grep -q org.gnome.desktop.interface; then\n"
+ " gsettings set org.gnome.desktop.interface icon-theme '" + gtkThemeName + "'\n" + " echo 'Updated via gsettings'\n" + "elif command -v dconf >/dev/null 2>&1; then\n" + " dconf write /org/gnome/desktop/interface/icon-theme \\\"" + gtkThemeName + "\\\"\n"
+ " echo 'Updated via dconf'\n" + "fi\n" + "\n" + "# Ensure config directories exist\n" + "mkdir -p " + _configDir + "/gtk-3.0 " + _configDir
+ "/gtk-4.0\n" + "\n" + "# Update settings.ini files (keep existing gtk-theme-name)\n" + "for config_dir in " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0; do\n"
+ " settings_file=\"$config_dir/settings.ini\"\n" + " if [ -f \"$settings_file\" ]; then\n" + " # Update existing icon-theme-name line or add it\n" + " if grep -q '^gtk-icon-theme-name=' \"$settings_file\"; then\n" + " sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=" + gtkThemeName + "/' \"$settings_file\"\n" + " else\n"
+ " # Add icon theme setting to [Settings] section or create it\n" + " if grep -q '\\[Settings\\]' \"$settings_file\"; then\n" + " sed -i '/\\[Settings\\]/a gtk-icon-theme-name=" + gtkThemeName + "' \"$settings_file\"\n" + " else\n" + " echo -e '\\n[Settings]\\ngtk-icon-theme-name=" + gtkThemeName
+ "' >> \"$settings_file\"\n" + " fi\n" + " fi\n" + " else\n" + " # Create new settings.ini file\n" + " echo -e '[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' > \"$settings_file\"\n"
+ " fi\n" + " echo \"Updated $settings_file\"\n" + "done\n" + "\n" + "# Clear icon cache and force refresh\n" + "rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true\n" + "# Send SIGHUP to running GTK applications to reload themes (Fedora-specific)\n" + "pkill -HUP -f 'gtk' 2>/dev/null || true\n"
Quickshell.execDetached(["sh", "-lc", script])
if (DMSService.apiVersion >= 3) {
PortalService.setSystemIconTheme(gtkThemeName)
}
var configScript = "mkdir -p " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0\n" + "\n" + "for config_dir in " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0; do\n"
+ " settings_file=\"$config_dir/settings.ini\"\n" + " if [ -f \"$settings_file\" ]; then\n" + " if grep -q '^gtk-icon-theme-name=' \"$settings_file\"; then\n" + " sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=" + gtkThemeName + "/' \"$settings_file\"\n" + " else\n"
+ " if grep -q '\\[Settings\\]' \"$settings_file\"; then\n" + " sed -i '/\\[Settings\\]/a gtk-icon-theme-name=" + gtkThemeName + "' \"$settings_file\"\n" + " else\n" + " echo -e '\\n[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' >> \"$settings_file\"\n" + " fi\n"
+ " fi\n" + " else\n" + " echo -e '[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' > \"$settings_file\"\n" + " fi\n" + "done\n" + "\n" + "rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true\n" + "pkill -HUP -f 'gtk' 2>/dev/null || true\n"
Quickshell.execDetached(["sh", "-lc", configScript])
}
}
@@ -1224,6 +1283,9 @@ Singleton {
function setDankBarSpacing(spacing) {
dankBarSpacing = spacing
saveSettings()
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
NiriService.generateNiriLayoutConfig()
}
}
function setDankBarBottomGap(gap) {
@@ -1256,6 +1318,16 @@ Singleton {
saveSettings()
}
function setPopupGapsAuto(enabled) {
popupGapsAuto = enabled
saveSettings()
}
function setPopupGapsManual(value) {
popupGapsManual = value
saveSettings()
}
function setDankBarPosition(position) {
dankBarPosition = position
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {

View File

@@ -16,8 +16,10 @@ Singleton {
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
// ! TODO - Synchronize with niri/hyprland gaps?
readonly property real popupDistance: 2
readonly property real popupDistance: {
if (typeof SettingsData === "undefined") return 4
return SettingsData.popupGapsAuto ? Math.max(4, SettingsData.dankBarSpacing) : SettingsData.popupGapsManual
}
property string currentTheme: "blue"
property string currentThemeCategory: "generic"
@@ -74,7 +76,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 +85,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 +100,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 +577,6 @@ Singleton {
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++
}
if (currentTheme === "custom" && customThemeFileView.path) {
customThemeFileView.reload()
}
@@ -603,7 +598,8 @@ Singleton {
"mode": isLight ? "light" : "dark",
"iconTheme": iconTheme || "System Default",
"matugenType": matugenType || "scheme-tonal-spot",
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc",
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
}
const json = JSON.stringify(desired)
@@ -692,7 +688,17 @@ Singleton {
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); }
function snap(value, dpr) {
return Math.round(value * dpr) / dpr
const s = dpr || 1
return Math.round(value * s) / s
}
function px(value, dpr) {
const s = dpr || 1
return Math.round(value * s) / s
}
function hairline(dpr) {
return 1 / (dpr || 1)
}
function invertHex(hex) {
@@ -905,7 +911,6 @@ Singleton {
const colorsText = dynamicColorsFileView.text()
if (colorsText) {
root.matugenColors = JSON.parse(colorsText)
root.colorUpdateTrigger++
if (typeof ToastService !== "undefined") {
ToastService.clearWallpaperError()
}

View File

@@ -54,26 +54,8 @@ Item {
WallpaperBackground {}
LazyLoader {
id: lockLoader
active: false
Lock {
id: lock
anchors.fill: parent
Component.onCompleted: {
IdleService.lockComponent = lock
}
}
}
Timer {
id: lockInitTimer
interval: 100
running: true
repeat: false
onTriggered: lockLoader.active = true
Lock {
id: lock
}
Loader {
@@ -195,7 +177,7 @@ Item {
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
onLockRequested: {
lockLoader.item.activate()
lock.activate()
}
Component.onCompleted: {

View File

@@ -46,6 +46,7 @@ Item {
leftIconName: "search"
showClearButton: true
focus: true
ignoreTabKeys: true
keyForwardTargets: [modal.modalFocusScope]
onTextChanged: {
modal.searchText = text

View File

@@ -60,6 +60,7 @@ DankModal {
open()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
clipboardHistoryModal.shouldHaveFocus = true
refreshClipboard()
keyboardController.reset()

View File

@@ -68,9 +68,11 @@ DankModal {
}
}
onOpened: {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
Qt.callLater(function () {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
})
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) {

View File

@@ -1,7 +1,9 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
PanelWindow {
id: root
@@ -14,14 +16,16 @@ PanelWindow {
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
readonly property real dpr: (screen && screen.devicePixelRatio) || 1
function snap(v) {
return Math.round(v * dpr) / dpr
}
function px(v) {
return Math.round(v)
readonly property real dpr: {
if (CompositorService.isNiri && screen) {
const niriScale = NiriService.displayScales[screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return (screen?.devicePixelRatio) || 1
}
property bool showBackground: true
property real backgroundOpacity: 0.5
@@ -142,26 +146,26 @@ PanelWindow {
Rectangle {
id: contentContainer
width: px(root.width)
height: px(root.height)
width: Theme.px(root.width, dpr)
height: Theme.px(root.height, dpr)
anchors.centerIn: undefined
x: {
if (positioning === "center") {
return snap((root.screenWidth - width) / 2)
return Theme.snap((root.screenWidth - width) / 2, dpr)
} else if (positioning === "top-right") {
return px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL))
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
} else if (positioning === "custom") {
return snap(root.customPosition.x)
return Theme.snap(root.customPosition.x, dpr)
}
return 0
}
y: {
if (positioning === "center") {
return snap((root.screenHeight - height) / 2)
return Theme.snap((root.screenHeight - height) / 2, dpr)
} else if (positioning === "top-right") {
return px(Theme.barHeight + Theme.spacingXS)
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
} else if (positioning === "custom") {
return snap(root.customPosition.y)
return Theme.snap(root.customPosition.y, dpr)
}
return 0
}
@@ -170,6 +174,7 @@ PanelWindow {
border.color: root.borderColor
border.width: root.borderWidth
clip: false
layer.enabled: true
opacity: root.shouldBeVisible ? 1 : 0
transform: root.animationType === "slide" ? slideTransform : null
@@ -179,8 +184,8 @@ PanelWindow {
readonly property real rawX: root.shouldBeVisible ? 0 : 15
readonly property real rawY: root.shouldBeVisible ? 0 : -30
x: snap(rawX)
y: snap(rawY)
x: Theme.snap(rawX, root.dpr)
y: Theme.snap(rawY, root.dpr)
}
Behavior on opacity {

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

@@ -54,6 +54,9 @@ DankModal {
height: 700
visible: false
onBackgroundClicked: hide()
onOpened: () => {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
onShouldBeVisibleChanged: (shouldBeVisible) => {
if (!shouldBeVisible) {
notificationModalOpen = false

View File

@@ -78,7 +78,7 @@ DankModal {
}
onOpened: () => {
selectedIndex = 0;
modalFocusScope.forceActiveFocus();
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
modalFocusScope.Keys.onPressed: (event) => {
switch (event.key) {

View File

@@ -25,6 +25,81 @@ Item {
visible: !BatteryService.batteryAvailable
}
StyledRect {
width: parent.width
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: lockScreenSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "lock"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Lock Screen")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Power Actions")
description: "Show power, restart, and logout buttons on the lock screen"
checked: SettingsData.lockScreenShowPowerActions
onToggled: checked => SettingsData.setLockScreenShowPowerActions(checked)
}
StyledText {
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
visible: !SessionService.loginctlAvailable
width: parent.width
wrapMode: Text.Wrap
}
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: SessionService.loginctlAvailable && SessionData.loginctlLockIntegration
enabled: SessionService.loginctlAvailable
onToggled: checked => {
if (SessionService.loginctlAvailable) {
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: SessionService.loginctlAvailable && SessionData.loginctlLockIntegration
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}
}
}
StyledRect {
width: parent.width
height: timeoutSection.implicitHeight + Theme.spacingL * 2
@@ -219,14 +294,6 @@ Item {
}
}
DankToggle {
width: parent.width
text: I18n.tr("Lock before suspend")
description: "Automatically lock the screen when the system prepares to suspend"
checked: SessionData.lockBeforeSuspend
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}
StyledText {
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
font.pixelSize: Theme.fontSizeSmall

View File

@@ -35,27 +35,14 @@ FocusScope {
}
Loader {
id: timeLoader
id: timeWeatherLoader
anchors.fill: parent
active: root.currentIndex === 1
visible: active
asynchronous: true
sourceComponent: TimeTab {
}
}
Loader {
id: weatherLoader
anchors.fill: parent
active: root.currentIndex === 2
visible: active
asynchronous: true
sourceComponent: WeatherTab {
sourceComponent: TimeWeatherTab {
}
}
@@ -64,7 +51,7 @@ FocusScope {
id: topBarLoader
anchors.fill: parent
active: root.currentIndex === 3
active: root.currentIndex === 2
visible: active
asynchronous: true
@@ -78,7 +65,7 @@ FocusScope {
id: widgetsLoader
anchors.fill: parent
active: root.currentIndex === 4
active: root.currentIndex === 3
visible: active
asynchronous: true
@@ -91,7 +78,7 @@ FocusScope {
id: dockLoader
anchors.fill: parent
active: root.currentIndex === 5
active: root.currentIndex === 4
visible: active
asynchronous: true
@@ -107,7 +94,7 @@ FocusScope {
id: displaysLoader
anchors.fill: parent
active: root.currentIndex === 6
active: root.currentIndex === 5
visible: active
asynchronous: true
@@ -120,7 +107,7 @@ FocusScope {
id: launcherLoader
anchors.fill: parent
active: root.currentIndex === 7
active: root.currentIndex === 6
visible: active
asynchronous: true
@@ -133,7 +120,7 @@ FocusScope {
id: themeColorsLoader
anchors.fill: parent
active: root.currentIndex === 8
active: root.currentIndex === 7
visible: active
asynchronous: true
@@ -146,7 +133,7 @@ FocusScope {
id: powerLoader
anchors.fill: parent
active: root.currentIndex === 9
active: root.currentIndex === 8
visible: active
asynchronous: true
@@ -159,7 +146,7 @@ FocusScope {
id: pluginsLoader
anchors.fill: parent
active: root.currentIndex === 10
active: root.currentIndex === 9
visible: active
asynchronous: true
@@ -173,7 +160,7 @@ FocusScope {
id: aboutLoader
anchors.fill: parent
active: root.currentIndex === 11
active: root.currentIndex === 10
visible: active
asynchronous: true

View File

@@ -12,11 +12,8 @@ Rectangle {
"text": I18n.tr("Personalization"),
"icon": "person"
}, {
"text": I18n.tr("Time & Date"),
"text": I18n.tr("Time & Weather"),
"icon": "schedule"
}, {
"text": I18n.tr("Weather"),
"icon": "cloud"
}, {
"text": I18n.tr("Dank Bar"),
"icon": "toolbar"
@@ -36,8 +33,8 @@ Rectangle {
"text": I18n.tr("Theme & Colors"),
"icon": "palette"
}, {
"text": I18n.tr("Power"),
"icon": "power_settings_new"
"text": I18n.tr("Idle & Lock Screen"),
"icon": "lock"
}, {
"text": I18n.tr("Plugins"),
"icon": "extension"

View File

@@ -128,6 +128,7 @@ Item {
enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: ""
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery
onTextEdited: () => {

View File

@@ -77,12 +77,14 @@ Popup {
spacing: 1
Rectangle {
width: parent.width
implicitWidth: pinRow.implicitWidth + Theme.spacingS * 2
width: implicitWidth
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: pinRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
@@ -155,16 +157,16 @@ Popup {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
Rectangle {
width: parent.width
implicitWidth: actionRow.implicitWidth + Theme.spacingS * 2
width: implicitWidth
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
@@ -189,8 +191,6 @@ Popup {
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (modelData.icon && modelData.icon !== "" ? (Theme.iconSize - 2 + Theme.spacingS) : 0)
}
}
@@ -228,12 +228,14 @@ Popup {
}
Rectangle {
width: parent.width
implicitWidth: launchRow.implicitWidth + Theme.spacingS * 2
width: implicitWidth
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: launchRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
@@ -288,12 +290,14 @@ Popup {
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width
implicitWidth: primeRunRow.implicitWidth + Theme.spacingS * 2
width: implicitWidth
height: 32
radius: Theme.cornerRadius
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: primeRunRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter

View File

@@ -112,6 +112,8 @@ DankPopout {
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
mappings[Qt.Key_Tab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : appLauncher.selectNext()
mappings[Qt.Key_Backtab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : appLauncher.selectPrevious()
if (appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
@@ -217,6 +219,7 @@ DankPopout {
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text
@@ -241,7 +244,7 @@ DankPopout {
return
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right]
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]
const isNavigationKey = navigationKeys.includes(event.key)
const isEmptyEnter = isEnterKey && !hasText
@@ -797,12 +800,13 @@ DankPopout {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
Rectangle {
width: parent.width
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter

View File

@@ -18,7 +18,7 @@ Item {
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property bool suppressUpdatesWhileLaunching: false
readonly property var categories: {
property var categories: {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
const result = [I18n.tr("All")]
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
@@ -27,11 +27,30 @@ Item {
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
property var _uniqueApps: []
property bool _isTriggered: false
property string _triggeredCategory: ""
property bool _updatingFromTrigger: false
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function updateCategories() {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
const result = [I18n.tr("All")]
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
}
Connections {
target: PluginService
function onPluginLoaded() { updateCategories() }
function onPluginUnloaded() { updateCategories() }
function onPluginListUpdated() { updateCategories() }
}
function updateFilteredModel() {
if (suppressUpdatesWhileLaunching) {
suppressUpdatesWhileLaunching = false
@@ -41,21 +60,64 @@ Item {
selectedIndex = 0
keyboardNavigationActive = false
const triggerResult = checkPluginTriggers(searchQuery)
if (triggerResult.triggered) {
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId)
}
let apps = []
const allCategory = I18n.tr("All")
if (searchQuery.length === 0) {
apps = selectedCategory === allCategory ? AppSearchService.getAppsInCategory(allCategory) : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : []
if (triggerResult.triggered) {
_isTriggered = true
_triggeredCategory = triggerResult.pluginCategory
_updatingFromTrigger = true
selectedCategory = triggerResult.pluginCategory
_updatingFromTrigger = false
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query)
} else {
if (selectedCategory === allCategory) {
apps = AppSearchService.searchApplications(searchQuery)
} else {
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
if (categoryApps.length > 0) {
const allSearchResults = AppSearchService.searchApplications(searchQuery)
const categoryNames = new Set(categoryApps.map(app => app.name))
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
if (_isTriggered) {
_updatingFromTrigger = true
selectedCategory = allCategory
_updatingFromTrigger = false
_isTriggered = false
_triggeredCategory = ""
}
if (searchQuery.length === 0) {
if (selectedCategory === allCategory) {
let emptyTriggerItems = []
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId)
const pluginCategory = plugin.name || pluginId
const items = AppSearchService.getPluginItems(pluginCategory, "")
emptyTriggerItems = emptyTriggerItems.concat(items)
})
apps = AppSearchService.applications.concat(emptyTriggerItems)
} else {
apps = []
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
}
} else {
if (selectedCategory === allCategory) {
apps = AppSearchService.searchApplications(searchQuery)
let emptyTriggerItems = []
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId)
const pluginCategory = plugin.name || pluginId
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery)
emptyTriggerItems = emptyTriggerItems.concat(items)
})
apps = apps.concat(emptyTriggerItems)
} else {
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
if (categoryApps.length > 0) {
const allSearchResults = AppSearchService.searchApplications(searchQuery)
const categoryNames = new Set(categoryApps.map(app => app.name))
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
} else {
apps = []
}
}
}
}
@@ -73,18 +135,31 @@ Item {
})
}
const seenNames = new Set()
const uniqueApps = []
apps.forEach(app => {
if (app) {
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "")
if (seenNames.has(itemKey)) {
return
}
seenNames.add(itemKey)
uniqueApps.push(app)
const isPluginItem = app.action !== undefined
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"exec": app.execString || app.exec || app.action || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
"isPlugin": isPluginItem,
"appIndex": uniqueApps.length - 1
})
}
})
root._uniqueApps = uniqueApps
}
function selectNext() {
@@ -128,13 +203,25 @@ Item {
}
function launchApp(appData) {
if (!appData) {
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) {
return
}
suppressUpdatesWhileLaunching = true
SessionService.launchDesktopEntry(appData.desktopEntry)
appLaunched(appData)
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
const actualApp = _uniqueApps[appData.appIndex]
if (appData.isPlugin) {
const pluginId = getPluginIdForItem(actualApp)
if (pluginId) {
AppSearchService.executePluginItem(actualApp, pluginId)
appLaunched(appData)
return
}
} else {
SessionService.launchDesktopEntry(actualApp)
appLaunched(appData)
AppUsageHistoryData.addAppUsage(actualApp)
}
}
function setCategory(category) {
@@ -154,7 +241,12 @@ Item {
updateFilteredModel()
}
}
onSelectedCategoryChanged: updateFilteredModel()
onSelectedCategoryChanged: {
if (_updatingFromTrigger) {
return
}
updateFilteredModel()
}
onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: {
@@ -172,4 +264,63 @@ Item {
repeat: false
onTriggered: updateFilteredModel()
}
// Plugin trigger system functions
function checkPluginTriggers(query) {
if (!query || typeof PluginService === "undefined") {
return { triggered: false, pluginCategory: "", query: "" }
}
const triggers = PluginService.getAllPluginTriggers()
for (const trigger in triggers) {
if (query.startsWith(trigger)) {
const pluginId = triggers[trigger]
const plugin = PluginService.getLauncherPlugin(pluginId)
if (plugin) {
const remainingQuery = query.substring(trigger.length).trim()
const result = {
triggered: true,
pluginId: pluginId,
pluginCategory: plugin.name || pluginId,
query: remainingQuery,
trigger: trigger
}
return result
}
}
}
return { triggered: false, pluginCategory: "", query: "" }
}
function getPluginIdForItem(item) {
if (!item || !item.categories || typeof PluginService === "undefined") {
return null
}
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
const pluginCategory = plugin.name || pluginId
let hasCategory = false
if (Array.isArray(item.categories)) {
hasCategory = item.categories.includes(pluginCategory)
} else if (item.categories && typeof item.categories.count !== "undefined") {
for (let i = 0; i < item.categories.count; i++) {
if (item.categories.get(i) === pluginCategory) {
hasCategory = true
break
}
}
}
if (hasCategory) {
return pluginId
}
}
return null
}
}

View File

@@ -11,6 +11,7 @@ Rectangle {
signal powerButtonClicked()
signal lockRequested()
signal editModeToggled()
signal settingsButtonClicked()
implicitHeight: 70
radius: Theme.cornerRadius
@@ -96,6 +97,7 @@ Rectangle {
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: {
root.settingsButtonClicked()
settingsModal.show()
}
}

View File

@@ -140,6 +140,9 @@ DankPopout {
root.close()
root.lockRequested()
}
onSettingsButtonClicked: {
root.close()
}
}
DragDropGrid {

View File

@@ -16,6 +16,21 @@ Item {
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
function requestRepaint() {
debounceTimer.restart()
}
Timer {
id: debounceTimer
interval: 50
repeat: false
onTriggered: {
barShape.requestPaint()
barTint.requestPaint()
barBorder.requestPaint()
}
}
Canvas {
id: barShape
anchors.fill: parent
@@ -25,44 +40,41 @@ Item {
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight))
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: barWindow
function on_BgColorChanged() { barShape.requestPaint() }
function on_DprChanged() { barShape.requestPaint() }
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { barShape.requestPaint() }
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceContainerChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
ctx.scale(scale, scale)
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
@@ -89,7 +101,7 @@ Item {
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw))
ctx.save()
if (isBottom) {
@@ -120,46 +132,43 @@ Item {
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight))
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onAlphaTintChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onAlphaTintChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: barWindow
function on_BgColorChanged() { barTint.requestPaint() }
function on_DprChanged() { barTint.requestPaint() }
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { barTint.requestPaint() }
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
ctx.scale(scale, scale)
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
@@ -186,7 +195,7 @@ Item {
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw))
ctx.save()
if (isBottom) {
@@ -211,53 +220,53 @@ Item {
Canvas {
id: barBorder
anchors.fill: parent
antialiasing: true
antialiasing: false
visible: SettingsData.dankBarBorderEnabled
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight))
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property bool borderEnabled: SettingsData.dankBarBorderEnabled
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onBorderEnabledChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
Connections {
target: barWindow
function on_DprChanged() { barBorder.requestPaint() }
}
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onBorderEnabledChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: Theme
function onSecondaryChanged() { barBorder.requestPaint() }
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceTextChanged() { root.requestRepaint() }
function onPrimaryChanged() { root.requestRepaint() }
function onSecondaryChanged() { root.requestRepaint() }
function onOutlineChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarSpacingChanged() { barBorder.requestPaint() }
function onDankBarSquareCornersChanged() { barBorder.requestPaint() }
function onCornerRadiusChanged() { barBorder.requestPaint() }
function onDankBarBorderColorChanged() { root.requestRepaint() }
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
function onDankBarSpacingChanged() { root.requestRepaint() }
function onDankBarSquareCornersChanged() { root.requestRepaint() }
}
onPaint: {
if (!borderEnabled) return
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
@@ -267,8 +276,6 @@ Item {
const spacing = SettingsData.dankBarSpacing
const hasEdgeGap = spacing > 0 || RT > 0
ctx.scale(scale, scale)
function drawTopBorder() {
ctx.beginPath()
@@ -302,7 +309,7 @@ Item {
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw))
ctx.save()
if (isBottom) {
@@ -319,9 +326,17 @@ Item {
drawTopBorder()
ctx.restore()
ctx.lineWidth = 1
ctx.strokeStyle = Theme.secondary
const key = SettingsData.dankBarBorderColor || "surfaceText"
const base = (key === "surfaceText") ? Theme.surfaceText
: (key === "primary") ? Theme.primary
: Theme.secondary
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
const thickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
ctx.globalCompositeOperation = "source-over"
ctx.lineWidth = thickness
ctx.strokeStyle = color
ctx.stroke()
}
}
}
}

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Mpris
import Quickshell.Services.Notifications
@@ -61,8 +62,17 @@ Item {
property real wingtipsRadius: Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
readonly property color _bgColor: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, topBarCore?.backgroundTransparency ?? SettingsData.dankBarTransparency)
readonly property real _dpr: (barWindow.screen && barWindow.screen.devicePixelRatio) ? barWindow.screen.devicePixelRatio : 1
function px(v) { return Math.round(v * _dpr) / _dpr }
readonly property real _dpr: {
if (CompositorService.isNiri && barWindow.screen) {
const niriScale = NiriService.displayScales[barWindow.screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && barWindow.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return (barWindow.screen?.devicePixelRatio) || 1
}
property string screenName: modelData.name
readonly property int notificationCount: NotificationService.notifications.length
@@ -70,8 +80,8 @@ Item {
readonly property real widgetThickness: Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
screen: modelData
implicitHeight: !isVertical ? px(effectiveBarThickness + SettingsData.dankBarSpacing + (SettingsData.dankBarGothCornersEnabled ? _wingR : 0)) : 0
implicitWidth: isVertical ? px(effectiveBarThickness + SettingsData.dankBarSpacing + (SettingsData.dankBarGothCornersEnabled ? _wingR : 0)) : 0
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + SettingsData.dankBarSpacing + (SettingsData.dankBarGothCornersEnabled ? _wingR : 0), _dpr) : 0
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + SettingsData.dankBarSpacing + (SettingsData.dankBarGothCornersEnabled ? _wingR : 0), _dpr) : 0
color: "transparent"
property var nativeInhibitor: null
@@ -234,7 +244,7 @@ Item {
Item {
id: inputMask
readonly property int barThickness: px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing, barWindow._dpr)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview
readonly property bool effectiveVisible: SettingsData.dankBarVisible || inOverviewWithShow
@@ -367,8 +377,8 @@ Item {
id: topBarMouseArea
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
height: !barWindow.isVertical ? px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
width: barWindow.isVertical ? px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
height: !barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing, barWindow._dpr) : undefined
width: barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing, barWindow._dpr) : undefined
anchors {
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
@@ -387,8 +397,8 @@ Item {
transform: Translate {
id: topBarSlide
x: barWindow.isVertical ? px(topBarCore.reveal ? 0 : (SettingsData.dankBarPosition === SettingsData.Position.Right ? barWindow.implicitWidth : -barWindow.implicitWidth)) : 0
y: !barWindow.isVertical ? px(topBarCore.reveal ? 0 : (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? barWindow.implicitHeight : -barWindow.implicitHeight)) : 0
x: barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (SettingsData.dankBarPosition === SettingsData.Position.Right ? barWindow.implicitWidth : -barWindow.implicitWidth), barWindow._dpr) : 0
y: !barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? barWindow.implicitHeight : -barWindow.implicitHeight), barWindow._dpr) : 0
Behavior on x {
NumberAnimation {
@@ -408,10 +418,10 @@ Item {
Item {
id: barUnitInset
anchors.fill: parent
anchors.leftMargin: !barWindow.isVertical ? px(SettingsData.dankBarSpacing) : (axis.edge === "left" ? px(SettingsData.dankBarSpacing) : 0)
anchors.rightMargin: !barWindow.isVertical ? px(SettingsData.dankBarSpacing) : (axis.edge === "right" ? px(SettingsData.dankBarSpacing) : 0)
anchors.topMargin: barWindow.isVertical ? px(SettingsData.dankBarSpacing) : (axis.outerVisualEdge() === "bottom" ? 0 : px(SettingsData.dankBarSpacing))
anchors.bottomMargin: barWindow.isVertical ? px(SettingsData.dankBarSpacing) : (axis.outerVisualEdge() === "bottom" ? px(SettingsData.dankBarSpacing) : 0)
anchors.leftMargin: !barWindow.isVertical ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : (axis.edge === "left" ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : 0)
anchors.rightMargin: !barWindow.isVertical ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : (axis.edge === "right" ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : 0)
anchors.topMargin: barWindow.isVertical ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : (axis.outerVisualEdge() === "bottom" ? 0 : Theme.px(SettingsData.dankBarSpacing, barWindow._dpr))
anchors.bottomMargin: barWindow.isVertical ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : (axis.outerVisualEdge() === "bottom" ? Theme.px(SettingsData.dankBarSpacing, barWindow._dpr) : 0)
BarCanvas {
id: barBackground

View File

@@ -30,8 +30,15 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: function (mouse){
if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview()
}
return
}
root.clicked();
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0);

View File

@@ -22,7 +22,7 @@ Rectangle {
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
readonly property var sortedToplevels: {
if (SettingsData.runningAppsCurrentWorkspace) {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen.name);
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen?.name);
}
return CompositorService.sortedToplevels;
}

View File

@@ -30,9 +30,9 @@ Rectangle {
}
if (CompositorService.isHyprland) {
const baseList = getHyprlandWorkspaces()
// Filter out special:scratch_term
const filteredList = baseList.filter(ws => ws.name !== "special:scratch_term" && ws.id !== -98)
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
// Filter out special workspaces
const filteredList = baseList.filter(ws => ws.id > -1)
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
}
return [1]
}

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

@@ -60,14 +60,6 @@ Variants {
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool windowIsFullscreen: {
if (!ToplevelManager.activeToplevel) {
return false
}
const activeWindow = ToplevelManager.activeToplevel
const fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => activeWindow.appId && activeWindow.appId.toLowerCase().includes(app))
}
property bool revealSticky: false
Timer {
@@ -81,7 +73,7 @@ Variants {
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true
}
return (!autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky) && !windowIsFullscreen
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
}
onContextMenuOpenChanged: {

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

@@ -34,30 +34,10 @@ Item {
signal launchRequested
property bool powerDialogVisible: false
property string powerDialogTitle: ""
property string powerDialogMessage: ""
property string powerDialogConfirmText: ""
property color powerDialogConfirmColor: Theme.primary
property var powerDialogOnConfirm: function () {}
function pickRandomFact() {
randomFact = Facts.getRandomFact()
}
function showPowerDialog(title, message, confirmText, confirmColor, onConfirm) {
powerDialogTitle = title
powerDialogMessage = message
powerDialogConfirmText = confirmText
powerDialogConfirmColor = confirmColor
powerDialogOnConfirm = onConfirm
powerDialogVisible = true
}
function hidePowerDialog() {
powerDialogVisible = false
}
Component.onCompleted: {
pickRandomFact()
WeatherService.addRef()
@@ -373,11 +353,11 @@ Item {
syncingFromState = true
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput
syncingFromState = false
if (isPrimaryScreen)
if (isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
}
onVisibleChanged: {
if (visible && isPrimaryScreen)
if (visible && isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
}
}
@@ -1073,33 +1053,15 @@ Item {
visible: root.randomFact !== ""
}
Row {
DankActionButton {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Theme.spacingXL
spacing: Theme.spacingL
visible: GreetdSettings.lockScreenShowPowerActions
DankActionButton {
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
showPowerDialog("Power Off", "Power off this computer?", "Power Off", Theme.error, function () {
SessionService.poweroff()
})
}
}
DankActionButton {
iconName: "refresh"
buttonSize: 40
onClicked: {
showPowerDialog("Restart", "Restart this computer?", "Restart", Theme.primary, function () {
SessionService.reboot()
})
}
}
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: powerMenu.show()
}
Item {
@@ -1291,7 +1253,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"])
}
}
@@ -1314,90 +1276,12 @@ Item {
onTriggered: GreeterState.pamState = ""
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: powerDialogVisible
z: 1000
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "power_settings_new"
size: 32
color: powerDialogConfirmColor
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: powerDialogMessage
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: I18n.tr("Cancel")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: hidePowerDialog()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: powerDialogConfirmColor
StyledText {
anchors.centerIn: parent
text: powerDialogConfirmText
color: Theme.primaryText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
hidePowerDialog()
powerDialogOnConfirm()
}
}
}
}
LockPowerMenu {
id: powerMenu
showLogout: false
onClosed: {
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
Qt.callLater(() => inputField.forceActiveFocus())
}
}
}

View File

@@ -12,40 +12,89 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
## Installation
### Arch Linux
Arch linux users can install [greetd-dms-greeter-git](https://aur.archlinux.org/packages/greetd-dms-greeter-git) from the AUR.
```bash
paru -S greetd-dms-greeter-git
# Or with yay
yay -S greetd-dms-greeter-git
```
Then in your `/etc/greetd/config.toml` enable dms-greeter by replacing the greeter command with dms-greeter.
```bash
# hyprland and sway are also supported as compositors
command = "/usr/bin/dms-greeter --command niri"
```
See `dms-greeter --help` for full options including custom compositor configurations.
Once installed, you should disable any existing greeter (such as gdm, sddm, lightdm), and you can configure the greeter to run at boot with:
```bash
sudo systemctl enable greetd
```
#### Syncing themes
To sync wallpapers, colors, and other settings from the logged in user, you can add your user to the `greeter` group and symlink the shell configurations.
```bash
sudo usermod -aG greeter <username>
# LOGOUT and LOGIN after adding user to group
ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /var/cache/dms-greeter/colors.json
```
### Automatic
The easiest thing is to run `dms greeter install` or `dms` for interactive installation.
### 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 750 /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 +115,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 +158,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

@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
@@ -5,84 +7,55 @@ import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
function activate() {
loader.activeAsync = true
}
Scope {
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 = this
}
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: shouldLock
onLockedChanged: {
if (!locked) {
loader.active = false
}
}
WlSessionLockSurface {
color: "transparent"
LockSurface {
id: lockSurface
anchors.fill: parent
lock: sessionLock
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
sharedPasswordBuffer: sharedPasswordBuffer
onUnlockRequested: {
shouldLock = false
}
onPasswordChanged: newPassword => {
sessionLock.sharedPasswordBuffer = newPassword
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

@@ -0,0 +1,459 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVisible: false
property bool showLogout: true
property int selectedIndex: 0
property int optionCount: {
let count = 0
if (showLogout) count++
count++
if (SessionService.hibernateSupported) count++
count += 2
return count
}
signal closed()
function show() {
isVisible = true
selectedIndex = 0
Qt.callLater(() => {
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
powerMenuFocusScope.forceActiveFocus()
}
})
}
function hide() {
isVisible = false
closed()
}
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
visible: isVisible
z: 1000
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
FocusScope {
id: powerMenuFocusScope
anchors.fill: parent
focus: root.isVisible
onVisibleChanged: {
if (visible) {
Qt.callLater(() => forceActiveFocus())
}
}
Keys.onEscapePressed: {
root.hide()
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
break
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
const actions = []
if (showLogout) actions.push("logout")
actions.push("suspend")
if (SessionService.hibernateSupported) actions.push("hibernate")
actions.push("reboot", "poweroff")
if (selectedIndex < actions.length) {
const action = actions[selectedIndex]
hide()
switch (action) {
case "logout":
SessionService.logout()
break
case "suspend":
SessionService.suspend()
break
case "hibernate":
SessionService.hibernate()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
}
break
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
}
break
}
}
Rectangle {
anchors.centerIn: parent
width: 320
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
height: implicitHeight
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: I18n.tr("Power Options")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
visible: showLogout
color: {
if (selectedIndex === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (logoutArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
border.width: selectedIndex === 0 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Log Out")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.logout()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const suspendIdx = showLogout ? 1 : 0
if (selectedIndex === suspendIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (suspendArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Suspend")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.suspend()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const hibernateIdx = showLogout ? 2 : 1
if (selectedIndex === hibernateIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (hibernateArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
visible: SessionService.hibernateSupported
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "ac_unit"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Hibernate")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: hibernateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.hibernate()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
if (selectedIndex === rebootIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (rebootArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
}
border.width: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
return selectedIndex === rebootIdx ? 1 : 0
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Reboot")
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.reboot()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
if (selectedIndex === powerOffIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (powerOffArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
}
border.width: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
return selectedIndex === powerOffIdx ? 1 : 0
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Power Off")
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.poweroff()
}
}
}
}
Item {
height: Theme.spacingS
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
@@ -26,27 +27,6 @@ Item {
signal unlockRequested
// Internal power dialog state
property bool powerDialogVisible: false
property string powerDialogTitle: ""
property string powerDialogMessage: ""
property string powerDialogConfirmText: ""
property color powerDialogConfirmColor: Theme.primary
property var powerDialogOnConfirm: function () {}
function showPowerDialog(title, message, confirmText, confirmColor, onConfirm) {
powerDialogTitle = title
powerDialogMessage = message
powerDialogConfirmText = confirmText
powerDialogConfirmColor = confirmColor
powerDialogOnConfirm = onConfirm
powerDialogVisible = true
}
function hidePowerDialog() {
powerDialogVisible = false
}
function pickRandomFact() {
randomFact = Facts.getRandomFact()
}
@@ -63,6 +43,16 @@ Item {
updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
DMSService.sendRequest("loginctl.lockerReady", null, response => {
if (response.error) {
console.warn("LockScreenContent: Failed to signal locker ready:", response.error)
} else {
console.log("LockScreenContent: Locker ready signaled, inhibitor released")
}
})
}
}
onDemoModeChanged: {
if (demoMode) {
@@ -336,7 +326,7 @@ Item {
}
onActiveFocusChanged: {
if (!activeFocus && !demoMode && visible && passwordField) {
if (!activeFocus && !demoMode && visible && passwordField && !powerMenu.isVisible) {
Qt.callLater(() => {
if (passwordField && passwordField.forceActiveFocus) {
passwordField.forceActiveFocus()
@@ -346,7 +336,7 @@ Item {
}
onEnabledChanged: {
if (enabled && !demoMode && visible && passwordField) {
if (enabled && !demoMode && visible && passwordField && !powerMenu.isVisible) {
Qt.callLater(() => {
if (passwordField && passwordField.forceActiveFocus) {
passwordField.forceActiveFocus()
@@ -1070,53 +1060,19 @@ Item {
}
}
Row {
DankActionButton {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Theme.spacingXL
spacing: Theme.spacingL
visible: SettingsData.lockScreenShowPowerActions
DankActionButton {
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Power")
} else {
showPowerDialog("Power Off", "Power off this computer?", "Power Off", Theme.error, function () {
SessionService.poweroff()
})
}
}
}
DankActionButton {
iconName: "refresh"
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Reboot")
} else {
showPowerDialog("Restart", "Restart this computer?", "Restart", Theme.primary, function () {
SessionService.reboot()
})
}
}
}
DankActionButton {
iconName: "logout"
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Logout")
} else {
showPowerDialog("Log Out", "End this session?", "Log Out", Theme.primary, function () {
SessionService.logout()
})
}
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Power Menu")
} else {
powerMenu.show()
}
}
}
@@ -1197,91 +1153,12 @@ Item {
onClicked: root.unlockRequested()
}
// Internal power dialog
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: powerDialogVisible
z: 1000
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "power_settings_new"
size: 32
color: powerDialogConfirmColor
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: powerDialogMessage
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: I18n.tr("Cancel")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: hidePowerDialog()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: powerDialogConfirmColor
StyledText {
anchors.centerIn: parent
text: powerDialogConfirmText
color: Theme.primaryText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
hidePowerDialog()
powerDialogOnConfirm()
}
}
}
}
LockPowerMenu {
id: powerMenu
showLogout: true
onClosed: {
if (!demoMode && passwordField && passwordField.forceActiveFocus) {
Qt.callLater(() => passwordField.forceActiveFocus())
}
}
}

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland

View File

@@ -1,36 +1,30 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
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

@@ -8,16 +8,24 @@ Item {
required property string pluginId
property var pluginService: null
default property alias content: settingsColumn.children
default property list<QtObject> content
signal settingChanged()
property var variants: []
property alias variantsModel: variantsListModel
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()
@@ -26,8 +34,8 @@ Item {
onPluginServiceChanged: {
if (pluginService) {
loadVariants()
for (let i = 0; i < settingsColumn.children.length; i++) {
const child = settingsColumn.children[i]
for (let i = 0; i < content.length; i++) {
const child = content[i]
if (child.loadValue) {
child.loadValue()
}
@@ -35,6 +43,15 @@ Item {
}
}
onContentChanged: {
for (let i = 0; i < content.length; i++) {
const item = content[i]
if (item instanceof Item) {
item.parent = settingsColumn
}
}
}
Connections {
target: pluginService
function onPluginDataChanged(changedPluginId) {
@@ -50,6 +67,22 @@ Item {
return
}
variants = pluginService.getPluginVariants(pluginId)
syncVariantsToModel()
}
function syncVariantsToModel() {
variantsListModel.clear()
for (let i = 0; i < variants.length; i++) {
variantsListModel.append(variants[i])
}
}
onVariantsChanged: {
syncVariantsToModel()
}
ListModel {
id: variantsListModel
}
function createVariant(variantName, variantConfig) {
@@ -94,11 +127,47 @@ Item {
return defaultValue
}
function findFlickable(item) {
var current = item?.parent
while (current) {
if (current.contentY !== undefined && current.contentHeight !== undefined) {
return current
}
current = current.parent
}
return null
}
function ensureItemVisible(item) {
if (!item) return
var flickable = findFlickable(root)
if (!flickable) return
var itemGlobalY = item.mapToItem(null, 0, 0).y
var itemHeight = item.height
var flickableGlobalY = flickable.mapToItem(null, 0, 0).y
var viewportHeight = flickable.height
var itemRelativeY = itemGlobalY - flickableGlobalY
var viewportTop = 0
var viewportBottom = viewportHeight
if (itemRelativeY < viewportTop) {
flickable.contentY = Math.max(0, flickable.contentY - (viewportTop - itemRelativeY) - Theme.spacingL)
} else if (itemRelativeY + itemHeight > viewportBottom) {
flickable.contentY = Math.min(
flickable.contentHeight - viewportHeight,
flickable.contentY + (itemRelativeY + itemHeight - viewportBottom) + Theme.spacingL
)
}
}
StyledText {
id: errorText
visible: pluginService && !root.hasPermission
anchors.fill: parent
text: qsTr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json")
text: I18n.tr("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

View File

@@ -258,7 +258,7 @@ Item {
StyledText {
text: `dms is a highly customizable, modern desktop shell with a <a href="https://m3.material.io/" style="text-decoration:none; color:${Theme.primary};">material 3 inspired</a> design.
<br /><br/>It is built on top of <a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>, a QT6 framework for building desktop shells.
<br /><br/>It is built with <a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>, a QT6 framework for building desktop shells, and <a href="https://go.dev" style="text-decoration:none; color:${Theme.primary};">Go</a>, a statically typed, compiled programming language.
`
textFormat: Text.RichText
font.pixelSize: Theme.fontSizeMedium
@@ -352,7 +352,7 @@ Item {
}
StyledText {
text: I18n.tr("QML (Qt Modeling Language)")
text: I18n.tr("QML, JavaScript, Go")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}

View File

@@ -942,14 +942,51 @@ Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Edge Spacing (0 = edge-to-edge)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Edge Spacing (0 = edge-to-edge)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - edgeSpacingText.implicitWidth - resetEdgeSpacingBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: edgeSpacingText
visible: false
text: I18n.tr("Edge Spacing (0 = edge-to-edge)")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetEdgeSpacingBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setDankBarSpacing(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: edgeSpacingSlider
width: parent.width
height: 24
value: SettingsData.dankBarSpacing
@@ -963,6 +1000,13 @@ Item {
SettingsData.setDankBarSpacing(
newValue)
}
Binding {
target: edgeSpacingSlider
property: "value"
value: SettingsData.dankBarSpacing
restoreMode: Binding.RestoreBinding
}
}
}
@@ -970,19 +1014,56 @@ Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Exclusive Zone Offset")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Exclusive Zone Offset")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - exclusiveZoneText.implicitWidth - resetExclusiveZoneBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: exclusiveZoneText
visible: false
text: I18n.tr("Exclusive Zone Offset")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetExclusiveZoneBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setDankBarBottomGap(0)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: exclusiveZoneSlider
width: parent.width
height: 24
value: SettingsData.dankBarBottomGap
minimum: -100
maximum: 100
minimum: -50
maximum: 50
unit: ""
showValue: true
wheelEnabled: false
@@ -991,6 +1072,13 @@ Item {
SettingsData.setDankBarBottomGap(
newValue)
}
Binding {
target: exclusiveZoneSlider
property: "value"
value: SettingsData.dankBarBottomGap
restoreMode: Binding.RestoreBinding
}
}
}
@@ -998,14 +1086,51 @@ Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - sizeText.implicitWidth - resetSizeBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: sizeText
visible: false
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetSizeBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setDankBarInnerPadding(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: sizeSlider
width: parent.width
height: 24
value: SettingsData.dankBarInnerPadding
@@ -1019,9 +1144,115 @@ Item {
SettingsData.setDankBarInnerPadding(
newValue)
}
Binding {
target: sizeSlider
property: "value"
value: SettingsData.dankBarInnerPadding
restoreMode: Binding.RestoreBinding
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
DankToggle {
width: parent.width
text: I18n.tr("Auto Popup Gaps")
description: I18n.tr("Automatically calculate popup distance from bar edge.")
checked: SettingsData.popupGapsAuto
onToggled: checked => {
SettingsData.setPopupGapsAuto(checked)
}
}
Column {
width: parent.width
leftPadding: Theme.spacingM
spacing: Theme.spacingM
visible: !SettingsData.popupGapsAuto
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Manual Gap Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - manualGapSizeText.implicitWidth - resetManualGapSizeBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: manualGapSizeText
visible: false
text: I18n.tr("Manual Gap Size")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetManualGapSizeBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setPopupGapsManual(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: popupGapsManualSlider
width: parent.width
height: 24
value: SettingsData.popupGapsManual
minimum: 0
maximum: 50
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.setPopupGapsManual(newValue)
}
Binding {
target: popupGapsManualSlider
property: "value"
value: SettingsData.popupGapsManual
restoreMode: Binding.RestoreBinding
}
}
}
}
}
DankToggle {
width: parent.width
@@ -1056,14 +1287,227 @@ Item {
}
}
DankToggle {
Column {
width: parent.width
text: I18n.tr("Border")
description: "Add a 1px border to the bar. Smart edge detection only shows border on exposed sides."
checked: SettingsData.dankBarBorderEnabled
onToggled: checked => {
SettingsData.setDankBarBorderEnabled(checked)
}
spacing: Theme.spacingM
DankToggle {
width: parent.width
text: I18n.tr("Border")
description: "Add a 1px border to the bar. Smart edge detection only shows border on exposed sides."
checked: SettingsData.dankBarBorderEnabled
onToggled: checked => {
SettingsData.setDankBarBorderEnabled(checked)
}
}
Column {
width: parent.width
leftPadding: Theme.spacingM
spacing: Theme.spacingM
visible: SettingsData.dankBarBorderEnabled
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Row {
width: parent.width - parent.leftPadding
spacing: Theme.spacingM
Column {
width: parent.width - borderColorGroup.width - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Border Color")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Choose the border accent color")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankButtonGroup {
id: borderColorGroup
anchors.verticalCenter: parent.verticalCenter
model: ["Surface", "Secondary", "Primary"]
currentIndex: {
const colorOption = SettingsData.dankBarBorderColor || "surfaceText"
switch (colorOption) {
case "surfaceText": return 0
case "secondary": return 1
case "primary": return 2
default: return 0
}
}
onSelectionChanged: (index, selected) => {
if (selected) {
let newColor = "surfaceText"
switch (index) {
case 0: newColor = "surfaceText"; break
case 1: newColor = "secondary"; break
case 2: newColor = "primary"; break
}
if (SettingsData.dankBarBorderColor !== newColor) {
SettingsData.dankBarBorderColor = newColor
}
}
}
}
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Border Opacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - borderOpacityText.implicitWidth - resetBorderOpacityBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: borderOpacityText
visible: false
text: I18n.tr("Border Opacity")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetBorderOpacityBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.dankBarBorderOpacity = 1.0
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: borderOpacitySlider
width: parent.width
height: 24
value: (SettingsData.dankBarBorderOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.dankBarBorderOpacity = newValue / 100
}
Binding {
target: borderOpacitySlider
property: "value"
value: (SettingsData.dankBarBorderOpacity ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Border Thickness")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - borderThicknessText.implicitWidth - resetBorderThicknessBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: borderThicknessText
visible: false
text: I18n.tr("Border Thickness")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetBorderThicknessBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.dankBarBorderThickness = 1
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: borderThicknessSlider
width: parent.width
height: 24
value: SettingsData.dankBarBorderThickness ?? 1
minimum: 1
maximum: 10
unit: "px"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.dankBarBorderThickness = newValue
}
Binding {
target: borderThicknessSlider
property: "value"
value: SettingsData.dankBarBorderThickness ?? 1
restoreMode: Binding.RestoreBinding
}
}
}
}
}
Rectangle {

View File

@@ -76,6 +76,378 @@ Item {
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: gammaSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: gammaSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "brightness_6"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Gamma Control")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: nightModeToggle
width: parent.width
text: I18n.tr("Night Mode")
description: "Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates."
checked: DisplayService.nightModeEnabled
onToggled: checked => {
DisplayService.toggleNightMode()
}
Connections {
function onNightModeEnabledChanged() {
nightModeToggle.checked = DisplayService.nightModeEnabled
}
target: DisplayService
}
}
Column {
width: parent.width
spacing: 0
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
DankDropdown {
width: parent.width - parent.leftPadding - parent.rightPadding
text: I18n.tr("Temperature")
description: I18n.tr("Color temperature for night mode")
currentValue: SessionData.nightModeTemperature + "K"
options: {
var temps = []
for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K")
}
return temps
}
onValueChanged: value => {
var temp = parseInt(value.replace("K", ""))
SessionData.setNightModeTemperature(temp)
}
}
}
DankToggle {
id: automaticToggle
width: parent.width
text: I18n.tr("Automatic Control")
description: "Only adjust gamma based on time or location rules."
checked: SessionData.nightModeAutoEnabled
onToggled: checked => {
if (checked && !DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
} else if (!checked && DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
}
SessionData.setNightModeAutoEnabled(checked)
}
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticToggle.checked = SessionData.nightModeAutoEnabled
}
}
}
Column {
id: automaticSettings
width: parent.width
spacing: Theme.spacingS
visible: SessionData.nightModeAutoEnabled
leftPadding: Theme.spacingM
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticSettings.visible = SessionData.nightModeAutoEnabled
}
}
Item {
width: 200
height: 45 + Theme.spacingM
DankTabBar {
id: modeTabBarNight
width: 200
height: 45
model: [{
"text": "Time",
"icon": "access_time"
}, {
"text": "Location",
"icon": "place"
}]
Component.onCompleted: {
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(updateIndicator)
}
onTabClicked: index => {
console.log("Tab clicked:", index, "Setting mode to:", index === 1 ? "location" : "time")
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index
}
Connections {
target: SessionData
function onNightModeAutoModeChanged() {
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(modeTabBarNight.updateIndicator)
}
}
}
}
Column {
property bool isTimeMode: SessionData.nightModeAutoMode === "time"
visible: isTimeMode
spacing: Theme.spacingM
Row {
spacing: Theme.spacingM
height: 20
leftPadding: 45
StyledText {
text: I18n.tr("Hour")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 50
horizontalAlignment: Text.AlignHCenter
anchors.bottom: parent.bottom
}
StyledText {
text: I18n.tr("Minute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 50
horizontalAlignment: Text.AlignHCenter
anchors.bottom: parent.bottom
}
}
Row {
spacing: Theme.spacingM
height: 32
StyledText {
id: startLabel
text: I18n.tr("Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 50
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeStartHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value))
}
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeStartMinute(parseInt(value))
}
}
}
Row {
spacing: Theme.spacingM
height: 32
StyledText {
text: I18n.tr("End")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: startLabel.width
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeEndHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value))
}
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeEndMinute(parseInt(value))
}
}
}
}
Column {
property bool isLocationMode: SessionData.nightModeAutoMode === "location"
visible: isLocationMode
spacing: Theme.spacingM
width: parent.width
DankToggle {
width: parent.width
text: I18n.tr("Auto-location")
description: DisplayService.geoclueAvailable ? "Use automatic location detection (geoclue2)" : "Geoclue service not running - cannot auto-detect location"
checked: SessionData.nightModeLocationProvider === "geoclue2"
enabled: DisplayService.geoclueAvailable
onToggled: checked => {
if (checked && DisplayService.geoclueAvailable) {
SessionData.setNightModeLocationProvider("geoclue2")
SessionData.setLatitude(0.0)
SessionData.setLongitude(0.0)
} else {
SessionData.setNightModeLocationProvider("")
}
}
}
StyledText {
text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
visible: SessionData.nightModeLocationProvider !== "geoclue2"
}
Row {
spacing: Theme.spacingM
visible: SessionData.nightModeLocationProvider !== "geoclue2"
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lat = parseFloat(text) || 0.0
if (lat >= -90 && lat <= 90) {
SessionData.setLatitude(lat)
}
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lon = parseFloat(text) || 0.0
if (lon >= -180 && lon <= 180) {
SessionData.setLongitude(lon)
}
}
}
}
}
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
}
}
}
StyledRect {
width: parent.width
height: screensInfoSection.implicitHeight + Theme.spacingL * 2

View File

@@ -905,7 +905,63 @@ Item {
}
}
// Dynamic Theme Section
StyledRect {
width: parent.width
height: lightModeRow.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Row {
id: lightModeRow
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankIcon {
name: "contrast"
size: Theme.iconSize
color: Theme.primary
rotation: SessionData.isLightMode ? 180 : 0
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - lightModeToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Light Mode")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use light theme instead of dark theme")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: lightModeToggle
anchors.verticalCenter: parent.verticalCenter
checked: SessionData.isLightMode
onToggleCompleted: checked => {
Theme.screenTransition()
Theme.setLightMode(checked)
}
}
}
}
StyledRect {
width: parent.width
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
@@ -932,6 +988,26 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Matugen Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "auto_awesome"
size: Theme.iconSize
color: Theme.currentTheme === Theme.dynamic ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - toggle.width - Theme.spacingM
spacing: Theme.spacingXS
@@ -998,6 +1074,56 @@ Item {
width: parent.width
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "code"
size: Theme.iconSize
color: SettingsData.runUserMatugenTemplates ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - runUserTemplatesToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Run User Templates")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Execute templates from ~/.config/matugen/config.toml")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankToggle {
id: runUserTemplatesToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.runUserMatugenTemplates
enabled: Theme.matugenAvailable
onToggled: checked => {
SettingsData.setRunUserMatugenTemplates(checked)
}
}
}
StyledText {
text: I18n.tr("matugen not detected - dynamic theming unavailable")
font.pixelSize: Theme.fontSizeSmall
@@ -1009,721 +1135,6 @@ Item {
}
}
// Display Settings
StyledRect {
width: parent.width
height: displaySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: displaySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Display Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Light Mode")
description: I18n.tr("Use light theme instead of dark theme")
checked: SessionData.isLightMode
onToggleCompleted: checked => {
Theme.screenTransition()
Theme.setLightMode(checked)
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
DankToggle {
id: nightModeToggle
width: parent.width
text: I18n.tr("Night Mode")
description: "Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates."
checked: DisplayService.nightModeEnabled
onToggled: checked => {
DisplayService.toggleNightMode()
}
Connections {
function onNightModeEnabledChanged() {
nightModeToggle.checked = DisplayService.nightModeEnabled
}
target: DisplayService
}
}
DankDropdown {
text: I18n.tr("Temperature")
description: I18n.tr("Color temperature for night mode")
currentValue: SessionData.nightModeTemperature + "K"
options: {
var temps = []
for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K")
}
return temps
}
onValueChanged: value => {
var temp = parseInt(value.replace("K", ""))
SessionData.setNightModeTemperature(temp)
}
}
DankToggle {
id: automaticToggle
width: parent.width
text: I18n.tr("Automatic Control")
description: "Only adjust gamma based on time or location rules."
checked: SessionData.nightModeAutoEnabled
onToggled: checked => {
if (checked && !DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
} else if (!checked && DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
}
SessionData.setNightModeAutoEnabled(checked)
}
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticToggle.checked = SessionData.nightModeAutoEnabled
}
}
}
Column {
id: automaticSettings
width: parent.width
spacing: Theme.spacingS
visible: SessionData.nightModeAutoEnabled
leftPadding: Theme.spacingM
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticSettings.visible = SessionData.nightModeAutoEnabled
}
}
Item {
width: 200
height: 45 + Theme.spacingM
DankTabBar {
id: modeTabBarNight
width: 200
height: 45
model: [{
"text": "Time",
"icon": "access_time"
}, {
"text": "Location",
"icon": "place"
}]
Component.onCompleted: {
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(updateIndicator)
}
onTabClicked: index => {
console.log("Tab clicked:", index, "Setting mode to:", index === 1 ? "location" : "time")
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index
}
Connections {
target: SessionData
function onNightModeAutoModeChanged() {
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(modeTabBarNight.updateIndicator)
}
}
}
}
Column {
property bool isTimeMode: SessionData.nightModeAutoMode === "time"
visible: isTimeMode
spacing: Theme.spacingM
// Header row
Row {
spacing: Theme.spacingM
height: 20
leftPadding: 45
StyledText {
text: I18n.tr("Hour")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 50
horizontalAlignment: Text.AlignHCenter
anchors.bottom: parent.bottom
}
StyledText {
text: I18n.tr("Minute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 50
horizontalAlignment: Text.AlignHCenter
anchors.bottom: parent.bottom
}
}
// Start time row
Row {
spacing: Theme.spacingM
height: 32
StyledText {
id: startLabel
text: I18n.tr("Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 50
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeStartHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value))
}
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeStartMinute(parseInt(value))
}
}
}
// End time row
Row {
spacing: Theme.spacingM
height: 32
StyledText {
text: I18n.tr("End")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: startLabel.width
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeEndHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value))
}
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeEndMinute(parseInt(value))
}
}
}
}
Column {
property bool isLocationMode: SessionData.nightModeAutoMode === "location"
visible: isLocationMode
spacing: Theme.spacingM
width: parent.width
DankToggle {
width: parent.width
text: I18n.tr("Auto-location")
description: DisplayService.geoclueAvailable ? "Use automatic location detection (geoclue2)" : "Geoclue service not running - cannot auto-detect location"
checked: SessionData.nightModeLocationProvider === "geoclue2"
enabled: DisplayService.geoclueAvailable
onToggled: checked => {
if (checked && DisplayService.geoclueAvailable) {
SessionData.setNightModeLocationProvider("geoclue2")
SessionData.setLatitude(0.0)
SessionData.setLongitude(0.0)
} else {
SessionData.setNightModeLocationProvider("")
}
}
}
StyledText {
text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
visible: SessionData.nightModeLocationProvider !== "geoclue2"
}
Row {
spacing: Theme.spacingM
visible: SessionData.nightModeLocationProvider !== "geoclue2"
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lat = parseFloat(text) || 0.0
if (lat >= -90 && lat <= 90) {
SessionData.setLatitude(lat)
}
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lon = parseFloat(text) || 0.0
if (lon >= -180 && lon <= 180) {
SessionData.setLongitude(lon)
}
}
}
}
}
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
}
}
}
// Notification Popup Settings
StyledRect {
width: parent.width
height: notificationPopupSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: notificationPopupSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "notifications"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notification Popups")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
text: I18n.tr("Popup Position")
description: I18n.tr("Choose where notification popups appear on screen")
currentValue: {
if (SettingsData.notificationPopupPosition === -1) {
return "Top Center"
}
switch (SettingsData.notificationPopupPosition) {
case SettingsData.Position.Top:
return "Top Right"
case SettingsData.Position.Bottom:
return "Bottom Left"
case SettingsData.Position.Left:
return "Top Left"
case SettingsData.Position.Right:
return "Bottom Right"
default:
return "Top Right"
}
}
options: ["Top Right", "Top Left", "Top Center", "Bottom Right", "Bottom Left"]
onValueChanged: value => {
switch (value) {
case "Top Right":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Top)
break
case "Top Left":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Left)
break
case "Top Center":
SettingsData.setNotificationPopupPosition(-1)
break
case "Bottom Right":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Right)
break
case "Bottom Left":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Bottom)
break
}
SettingsData.sendTestNotifications()
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
DankToggle {
width: parent.width
text: I18n.tr("Always Show OSD Percentage")
description: I18n.tr("Display volume and brightness percentage values by default in OSD popups")
checked: SettingsData.osdAlwaysShowValue
onToggled: checked => {
SettingsData.setOsdAlwaysShowValue(checked)
}
}
}
}
// Font Settings
StyledRect {
width: parent.width
height: fontSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: fontSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "font_download"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Font Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
text: I18n.tr("Font Family")
description: I18n.tr("Select system font family")
currentValue: {
if (SettingsData.fontFamily === SettingsData.defaultFontFamily)
return "Default"
else
return SettingsData.fontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedFontFamilies
onValueChanged: value => {
if (value.startsWith("Default"))
SettingsData.setFontFamily(SettingsData.defaultFontFamily)
else
SettingsData.setFontFamily(value)
}
}
DankDropdown {
text: I18n.tr("Font Weight")
description: I18n.tr("Select font weight")
currentValue: {
switch (SettingsData.fontWeight) {
case Font.Thin:
return "Thin"
case Font.ExtraLight:
return "Extra Light"
case Font.Light:
return "Light"
case Font.Normal:
return "Regular"
case Font.Medium:
return "Medium"
case Font.DemiBold:
return "Demi Bold"
case Font.Bold:
return "Bold"
case Font.ExtraBold:
return "Extra Bold"
case Font.Black:
return "Black"
default:
return "Regular"
}
}
options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"]
onValueChanged: value => {
var weight
switch (value) {
case "Thin":
weight = Font.Thin
break
case "Extra Light":
weight = Font.ExtraLight
break
case "Light":
weight = Font.Light
break
case "Regular":
weight = Font.Normal
break
case "Medium":
weight = Font.Medium
break
case "Demi Bold":
weight = Font.DemiBold
break
case "Bold":
weight = Font.Bold
break
case "Extra Bold":
weight = Font.ExtraBold
break
case "Black":
weight = Font.Black
break
default:
weight = Font.Normal
break
}
SettingsData.setFontWeight(weight)
}
}
DankDropdown {
text: I18n.tr("Monospace Font")
description: I18n.tr("Select monospace font for process list and technical displays")
currentValue: {
if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily)
return "Default"
return SettingsData.monoFontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedFontFamilies
onValueChanged: value => {
if (value === "Default")
SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily)
else
SettingsData.setMonoFontFamily(value)
}
}
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: "transparent"
Column {
anchors.left: parent.left
anchors.right: fontScaleControls.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Font Scale")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Scale all font sizes")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: fontScaleControls
width: 180
height: 36
anchors.right: parent.right
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
buttonSize: 32
iconName: "remove"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.fontScale > 1.0
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
onClicked: {
var newScale = Math.max(1.0, SettingsData.fontScale - 0.05)
SettingsData.setFontScale(newScale)
}
}
StyledRect {
width: 60
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
StyledText {
anchors.centerIn: parent
text: (SettingsData.fontScale * 100).toFixed(0) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
DankActionButton {
buttonSize: 32
iconName: "add"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.fontScale < 2.0
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
onClicked: {
var newScale = Math.min(2.0, SettingsData.fontScale + 0.05)
SettingsData.setFontScale(newScale)
}
}
}
}
}
}
// Animation Settings
StyledRect {
width: parent.width
@@ -1752,7 +1163,7 @@ Item {
}
StyledText {
text: I18n.tr("Animations")
text: I18n.tr("Animation Speed")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
@@ -1760,28 +1171,13 @@ Item {
}
}
Column {
Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Animation Speed")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Control the speed of animations throughout the interface")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
height: childrenRect.height
DankButtonGroup {
id: animationSpeedGroup
width: parent.width
x: (parent.width - width) / 2
model: ["None", "Shortest", "Short", "Medium", "Long"]
selectionMode: "single"
currentIndex: SettingsData.animationSpeed
@@ -1794,54 +1190,6 @@ Item {
}
}
}
// Lock Screen Settings
StyledRect {
width: parent.width
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: lockScreenSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "lock"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Lock Screen")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Power Actions")
description: "Show power, restart, and logout buttons on the lock screen"
checked: SettingsData.lockScreenShowPowerActions
onToggled: checked => {
SettingsData.setLockScreenShowPowerActions(checked)
}
}
}
}
}
}

View File

@@ -256,10 +256,9 @@ FocusScope {
anchors.bottomMargin: pluginDelegate.isExpanded ? settingsContainer.height : 0
hoverEnabled: true
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: pluginDelegate.hasSettings
onClicked: {
if (pluginDelegate.hasSettings) {
pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
}
pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
}
}
@@ -504,6 +503,10 @@ FocusScope {
clip: true
focus: pluginDelegate.isExpanded && pluginDelegate.hasSettings
Keys.onPressed: event => {
event.accepted = true
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHighest
@@ -600,6 +603,9 @@ FocusScope {
pluginsTab.expandedPluginId = ""
}
}
function onPluginListUpdated() {
refreshPluginList()
}
}
Connections {

View File

@@ -17,67 +17,37 @@ Item {
property bool fontsEnumerated: false
function enumerateFonts() {
var fonts = ["Default"]
var fonts = []
var availableFonts = Qt.fontFamilies()
var rootFamilies = []
var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]
if (fontName.startsWith("."))
continue
if (fontName === SettingsData.defaultFontFamily)
continue
var rootName = fontName.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName)
rootFamilies.push(rootName)
}
fonts.push(fontName)
}
cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]
var monoFamilies = []
var seenMonoFamilies = new Set()
fonts.sort()
fonts.unshift("Default")
cachedFontFamilies = fonts
var monoFonts = []
for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]
if (fontName2.startsWith("."))
continue
if (fontName2 === SettingsData.defaultMonoFontFamily)
continue
var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes(
"code") || lowerName.includes(
"console") || lowerName.includes(
"terminal") || lowerName.includes(
"courier") || lowerName.includes(
"dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2)
}
if (lowerName.includes("mono") || lowerName.includes("code") ||
lowerName.includes("console") || lowerName.includes("terminal") ||
lowerName.includes("courier") || lowerName.includes("jetbrains") ||
lowerName.includes("fira") || lowerName.includes("hack") ||
lowerName.includes("source code") || lowerName.includes("cascadia")) {
monoFonts.push(fontName2)
}
}
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
monoFonts.sort()
monoFonts.unshift("Default")
cachedMonoFamilies = monoFonts
}
Component.onCompleted: {
@@ -929,6 +899,238 @@ Item {
}
}
StyledRect {
width: parent.width
height: fontSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: fontSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "font_download"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Font Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
text: I18n.tr("Font Family")
description: I18n.tr("Select system font family")
currentValue: {
if (SettingsData.fontFamily === SettingsData.defaultFontFamily)
return "Default"
else
return SettingsData.fontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedFontFamilies
onValueChanged: value => {
if (value.startsWith("Default"))
SettingsData.setFontFamily(SettingsData.defaultFontFamily)
else
SettingsData.setFontFamily(value)
}
}
DankDropdown {
text: I18n.tr("Font Weight")
description: I18n.tr("Select font weight")
currentValue: {
switch (SettingsData.fontWeight) {
case Font.Thin:
return "Thin"
case Font.ExtraLight:
return "Extra Light"
case Font.Light:
return "Light"
case Font.Normal:
return "Regular"
case Font.Medium:
return "Medium"
case Font.DemiBold:
return "Demi Bold"
case Font.Bold:
return "Bold"
case Font.ExtraBold:
return "Extra Bold"
case Font.Black:
return "Black"
default:
return "Regular"
}
}
options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"]
onValueChanged: value => {
var weight
switch (value) {
case "Thin":
weight = Font.Thin
break
case "Extra Light":
weight = Font.ExtraLight
break
case "Light":
weight = Font.Light
break
case "Regular":
weight = Font.Normal
break
case "Medium":
weight = Font.Medium
break
case "Demi Bold":
weight = Font.DemiBold
break
case "Bold":
weight = Font.Bold
break
case "Extra Bold":
weight = Font.ExtraBold
break
case "Black":
weight = Font.Black
break
default:
weight = Font.Normal
break
}
SettingsData.setFontWeight(weight)
}
}
DankDropdown {
text: I18n.tr("Monospace Font")
description: I18n.tr("Select monospace font for process list and technical displays")
currentValue: {
if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily)
return "Default"
return SettingsData.monoFontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedMonoFamilies
onValueChanged: value => {
if (value === "Default")
SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily)
else
SettingsData.setMonoFontFamily(value)
}
}
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: "transparent"
Column {
anchors.left: parent.left
anchors.right: fontScaleControls.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Font Scale")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Scale all font sizes")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: fontScaleControls
width: 180
height: 36
anchors.right: parent.right
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
buttonSize: 32
iconName: "remove"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.fontScale > 1.0
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
onClicked: {
var newScale = Math.max(1.0, SettingsData.fontScale - 0.05)
SettingsData.setFontScale(newScale)
}
}
StyledRect {
width: 60
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
StyledText {
anchors.centerIn: parent
text: (SettingsData.fontScale * 100).toFixed(
0) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
DankActionButton {
buttonSize: 32
iconName: "add"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.fontScale < 2.0
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
onClicked: {
var newScale = Math.min(2.0,
SettingsData.fontScale + 0.05)
SettingsData.setFontScale(newScale)
}
}
}
}
}
}
// System Configuration Warning
Rectangle {
width: parent.width
@@ -992,6 +1194,7 @@ Item {
}
DankDropdown {
width: parent.width - Theme.iconSize - Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Icon Theme")
description: "DankShell & System Icons\n(requires restart)"

View File

@@ -1,378 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: timeTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Time Format
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: timeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("24-Hour Format")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use 24-hour time format instead of 12-hour AM/PM")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: toggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.use24HourClock
onToggled: checked => {
return SettingsData.setClockFormat(
checked)
}
}
}
}
}
// Date Format Section
StyledRect {
width: parent.width
height: dateSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: dateSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "calendar_today"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Date Format")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
height: 50
text: I18n.tr("Top Bar Format")
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
currentValue: {
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0) {
return "System Default"
}
// Find matching preset or show "Custom"
const presets = [{
"format": "ddd d",
"label": "Day Date"
}, {
"format": "ddd MMM d",
"label": "Day Month Date"
}, {
"format": "MMM d",
"label": "Month Date"
}, {
"format": "M/d",
"label": "Numeric (M/D)"
}, {
"format": "d/M",
"label": "Numeric (D/M)"
}, {
"format": "ddd d MMM yyyy",
"label": "Full with Year"
}, {
"format": "yyyy-MM-dd",
"label": "ISO Date"
}, {
"format": "dddd, MMMM d",
"label": "Full Day & Month"
}]
const match = presets.find(p => {
return p.format
=== SettingsData.clockDateFormat
})
return match ? match.label: I18n.tr("Custom: ") + SettingsData.clockDateFormat
}
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: value => {
const formatMap = {
"System Default": "",
"Day Date": "ddd d",
"Day Month Date": "ddd MMM d",
"Month Date": "MMM d",
"Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d"
}
if (value === "Custom...") {
customFormatInput.visible = true
} else {
customFormatInput.visible = false
SettingsData.setClockDateFormat(
formatMap[value])
}
}
}
DankDropdown {
height: 50
text: I18n.tr("Lock Screen Format")
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
currentValue: {
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0) {
return "System Default"
}
// Find matching preset or show "Custom"
const presets = [{
"format": "ddd d",
"label": "Day Date"
}, {
"format": "ddd MMM d",
"label": "Day Month Date"
}, {
"format": "MMM d",
"label": "Month Date"
}, {
"format": "M/d",
"label": "Numeric (M/D)"
}, {
"format": "d/M",
"label": "Numeric (D/M)"
}, {
"format": "ddd d MMM yyyy",
"label": "Full with Year"
}, {
"format": "yyyy-MM-dd",
"label": "ISO Date"
}, {
"format": "dddd, MMMM d",
"label": "Full Day & Month"
}]
const match = presets.find(p => {
return p.format
=== SettingsData.lockDateFormat
})
return match ? match.label: I18n.tr("Custom: ") + SettingsData.lockDateFormat
}
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: value => {
const formatMap = {
"System Default": "",
"Day Date": "ddd d",
"Day Month Date": "ddd MMM d",
"Month Date": "MMM d",
"Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d"
}
if (value === "Custom...") {
customLockFormatInput.visible = true
} else {
customLockFormatInput.visible = false
SettingsData.setLockDateFormat(
formatMap[value])
}
}
}
DankTextField {
id: customFormatInput
width: parent.width
visible: false
placeholderText: I18n.tr("Enter custom top bar format (e.g., ddd MMM d)")
text: SettingsData.clockDateFormat
onTextChanged: {
if (visible && text)
SettingsData.setClockDateFormat(text)
}
}
DankTextField {
id: customLockFormatInput
width: parent.width
visible: false
placeholderText: I18n.tr("Enter custom lock screen format (e.g., dddd, MMMM d)")
text: SettingsData.lockDateFormat
onTextChanged: {
if (visible && text)
SettingsData.setLockDateFormat(text)
}
}
Rectangle {
width: parent.width
height: formatHelp.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 0
Column {
id: formatHelp
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Format Legend")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingL
Column {
width: (parent.width - Theme.spacingL) / 2
spacing: 2
StyledText {
text: I18n.tr("• d - Day (1-31)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• dd - Day (01-31)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• ddd - Day name (Mon)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• dddd - Day name (Monday)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• M - Month (1-12)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Column {
width: (parent.width - Theme.spacingL) / 2
spacing: 2
StyledText {
text: I18n.tr("• MM - Month (01-12)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• MMM - Month (Jan)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• MMMM - Month (January)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• yy - Year (24)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• yyyy - Year (2024)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,390 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: weatherTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Enable Weather
StyledRect {
width: parent.width
height: enableWeatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: enableWeatherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "cloud"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Enable Weather")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Show weather information in top bar and control center")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: enableToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.weatherEnabled
onToggled: checked => {
return SettingsData.setWeatherEnabled(
checked)
}
}
}
}
}
// Temperature Unit
StyledRect {
width: parent.width
height: temperatureSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0
Column {
id: temperatureSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "thermostat"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- temperatureToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Use Fahrenheit")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use Fahrenheit instead of Celsius for temperature")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: temperatureToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useFahrenheit
onToggled: checked => {
return SettingsData.setTemperatureUnit(
checked)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Location Settings
StyledRect {
width: parent.width
height: locationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0
Column {
id: locationSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "location_on"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoLocationToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Auto Location")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Automatically determine your location using your IP address")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoLocationToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useAutoLocation
onToggled: checked => {
return SettingsData.setAutoLocation(
checked)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !SettingsData.useAutoLocation
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
StyledText {
text: I18n.tr("Custom Location")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: latitudeInput
width: parent.width
height: 48
placeholderText: "40.7128"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
keyNavigationTab: longitudeInput
Component.onCompleted: {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 0) {
text = coords[0].trim()
}
}
}
Connections {
target: SettingsData
function onWeatherCoordinatesChanged() {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 0) {
latitudeInput.text = coords[0].trim()
}
}
}
}
onTextEdited: {
if (text && longitudeInput.text) {
const coords = text + "," + longitudeInput.text
SettingsData.weatherCoordinates = coords
SettingsData.saveSettings()
}
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: longitudeInput
width: parent.width
height: 48
placeholderText: "-74.0060"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
keyNavigationTab: locationSearchInput
keyNavigationBacktab: latitudeInput
Component.onCompleted: {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 1) {
text = coords[1].trim()
}
}
}
Connections {
target: SettingsData
function onWeatherCoordinatesChanged() {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 1) {
longitudeInput.text = coords[1].trim()
}
}
}
}
onTextEdited: {
if (text && latitudeInput.text) {
const coords = latitudeInput.text + "," + text
SettingsData.weatherCoordinates = coords
SettingsData.saveSettings()
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Location Search")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
font.weight: Font.Medium
}
DankLocationSearch {
id: locationSearchInput
width: parent.width
currentLocation: ""
placeholderText: I18n.tr("New York, NY")
keyNavigationBacktab: longitudeInput
onLocationSelected: (displayName, coordinates) => {
SettingsData.setWeatherLocation(displayName, coordinates)
const coords = coordinates.split(',')
if (coords.length >= 2) {
latitudeInput.text = coords[0].trim()
longitudeInput.text = coords[1].trim()
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}

View File

@@ -184,7 +184,126 @@ Item {
description: "Use animated wave progress bars for media playback"
checked: SettingsData.waveProgressEnabled
onToggled: checked => {
return SettingsData.setWaveProgressEnabled(checked)
return SettingsData.setWaveProgressEnabled(checked);
}
}
}
}
StyledRect {
width: parent.width
height: updaterSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: updaterSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "refresh"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("System Updater")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")
checked: SettingsData.updaterUseCustomCommand
onToggled: checked => {
if (!checked) {
updaterCustomCommand.text = "";
updaterTerminalCustomClass.text = "";
SettingsData.setUpdaterCustomCommand("");
SettingsData.setUpdaterTerminalAdditionalParams("");
}
return SettingsData.setUpdaterUseCustomCommandEnabled(checked);
}
}
Column {
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
StyledText {
text: I18n.tr("System update custom command")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterCustomCommand
width: parent.width
height: 48
placeholderText: "myPkgMngr --sysupdate"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterCustomCommand) {
text = SettingsData.updaterCustomCommand;
}
}
onTextEdited: {
SettingsData.setUpdaterCustomCommand(text.trim());
}
}
}
Column {
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
StyledText {
text: I18n.tr("Terminal custom additional parameters")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterTerminalCustomClass
width: parent.width
height: 48
placeholderText: "-T udpClass"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterTerminalAdditionalParams) {
text = SettingsData.updaterTerminalAdditionalParams;
}
}
onTextEdited: {
SettingsData.setUpdaterTerminalAdditionalParams(text.trim());
}
}
}
}
@@ -396,6 +515,151 @@ Item {
}
}
}
StyledRect {
width: parent.width
height: notificationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: notificationSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "notifications"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notification Popups")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 0
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
DankDropdown {
width: parent.width - parent.leftPadding - parent.rightPadding
text: I18n.tr("Popup Position")
description: I18n.tr("Choose where notification popups appear on screen")
currentValue: {
if (SettingsData.notificationPopupPosition === -1) {
return "Top Center"
}
switch (SettingsData.notificationPopupPosition) {
case SettingsData.Position.Top:
return "Top Right"
case SettingsData.Position.Bottom:
return "Bottom Left"
case SettingsData.Position.Left:
return "Top Left"
case SettingsData.Position.Right:
return "Bottom Right"
default:
return "Top Right"
}
}
options: ["Top Right", "Top Left", "Top Center", "Bottom Right", "Bottom Left"]
onValueChanged: value => {
switch (value) {
case "Top Right":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Top)
break
case "Top Left":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Left)
break
case "Top Center":
SettingsData.setNotificationPopupPosition(-1)
break
case "Bottom Right":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Right)
break
case "Bottom Left":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Bottom)
break
}
SettingsData.sendTestNotifications()
}
}
}
}
}
StyledRect {
width: parent.width
height: osdRow.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Row {
id: osdRow
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankIcon {
name: "tune"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - osdToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Always Show OSD Percentage")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display volume and brightness percentage values by default in OSD popups")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: osdToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.osdAlwaysShowValue
onToggleCompleted: checked => {
SettingsData.setOsdAlwaysShowValue(checked)
}
}
}
}
}
}
}

View File

@@ -67,7 +67,7 @@ PanelWindow {
}
}
width: Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : 0) + Theme.spacingL * 2 + Theme.spacingM * 2)
width: shouldBeVisible ? Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : 0) + Theme.spacingL * 2 + Theme.spacingM * 2) : frozenWidth
height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight - 4 + SettingsData.dankBarSpacing + 2
@@ -206,84 +206,121 @@ PanelWindow {
Rectangle {
width: parent.width
height: detailsText.height + Theme.spacingS * 2
height: detailsColumn.height + Theme.spacingS * 2
color: Qt.rgba(0, 0, 0, 0.2)
radius: Theme.cornerRadius / 2
visible: toast.expanded && ToastService.hasDetails
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
isMonospace: true
Column {
id: detailsColumn
anchors.left: parent.left
anchors.right: copyButton.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS
anchors.rightMargin: Theme.spacingS
wrapMode: Text.Wrap
}
DankActionButton {
id: copyButton
iconName: "content_copy"
iconSize: Theme.iconSizeSmall
iconColor: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
buttonSize: Theme.iconSizeSmall + 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
property bool showTooltip: false
onClicked: {
Quickshell.execDetached(
["wl-copy", ToastService.currentDetails])
showTooltip = true
tooltipTimer.start()
}
Timer {
id: tooltipTimer
interval: 1500
onTriggered: copyButton.showTooltip = false
StyledText {
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
visible: ToastService.currentDetails.length > 0
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.Wrap
}
Rectangle {
visible: copyButton.showTooltip
width: tooltipLabel.implicitWidth + 16
height: tooltipLabel.implicitHeight + 8
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyButton.width / 2
width: parent.width - Theme.spacingS * 2
height: commandText.height + Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
color: Qt.rgba(0, 0, 0, 0.3)
radius: Theme.cornerRadius / 2
visible: ToastService.currentCommand.length > 0
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: root.copiedText
id: commandText
text: ToastService.currentCommand
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
isMonospace: true
anchors.left: parent.left
anchors.right: copyButton.visible ? copyButton.left : parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS / 2
anchors.rightMargin: Theme.spacingS / 2
wrapMode: Text.Wrap
}
DankActionButton {
id: copyButton
iconName: "content_copy"
iconSize: Theme.iconSizeSmall
iconColor: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
buttonSize: Theme.iconSizeSmall + 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS / 2
visible: ToastService.currentCommand.length > 0
property bool showTooltip: false
onClicked: {
Quickshell.execDetached(["wl-copy", ToastService.currentCommand])
showTooltip = true
tooltipTimer.start()
}
Timer {
id: tooltipTimer
interval: 1500
onTriggered: copyButton.showTooltip = false
}
Rectangle {
visible: copyButton.showTooltip
width: tooltipLabel.implicitWidth + 16
height: tooltipLabel.implicitHeight + 8
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyButton.width / 2
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: root.copiedText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
import QtQuick
import qs.Services
Item {
id: root
// Plugin properties
property var pluginService: null
property string trigger: "#"
// Plugin interface signals
signal itemsChanged()
Component.onCompleted: {
console.log("LauncherExample: Plugin loaded")
// Load custom trigger from settings
if (pluginService) {
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
}
}
// Required function: Get items for launcher
function getItems(query) {
const baseItems = [
{
name: "Test Item 1",
icon: "lightbulb",
comment: "This is a test item that shows a toast notification",
action: "toast:Test Item 1 executed!",
categories: ["LauncherExample"]
},
{
name: "Test Item 2",
icon: "star",
comment: "Another test item with different action",
action: "toast:Test Item 2 clicked!",
categories: ["LauncherExample"]
},
{
name: "Test Item 3",
icon: "favorite",
comment: "Third test item for demonstration",
action: "toast:Test Item 3 activated!",
categories: ["LauncherExample"]
},
{
name: "Example Copy Action",
icon: "content_copy",
comment: "Demonstrates copying text to clipboard",
action: "copy:This text was copied by the launcher plugin!",
categories: ["LauncherExample"]
},
{
name: "Example Script Action",
icon: "terminal",
comment: "Demonstrates running a simple command",
action: "script:echo 'Hello from launcher plugin!'",
categories: ["LauncherExample"]
}
]
if (!query || query.length === 0) {
return baseItems
}
// Filter items based on query
const lowerQuery = query.toLowerCase()
return baseItems.filter(item => {
return item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
})
}
// Required function: Execute item action
function executeItem(item) {
if (!item || !item.action) {
console.warn("LauncherExample: Invalid item or action")
return
}
console.log("LauncherExample: Executing item:", item.name, "with action:", item.action)
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
showToast(actionData)
break
case "copy":
copyToClipboard(actionData)
break
case "script":
runScript(actionData)
break
default:
console.warn("LauncherExample: Unknown action type:", actionType)
showToast("Unknown action: " + actionType)
}
}
// Helper functions for different action types
function showToast(message) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("LauncherExample", message)
} else {
console.log("LauncherExample Toast:", message)
}
}
function copyToClipboard(text) {
if (typeof globalThis !== "undefined" && globalThis.clipboard) {
globalThis.clipboard.setText(text)
showToast("Copied to clipboard: " + text)
} else {
console.log("LauncherExample: Would copy to clipboard:", text)
showToast("Copy feature not available")
}
}
function runScript(command) {
console.log("LauncherExample: Would run script:", command)
showToast("Script executed: " + command)
// In a real plugin, you might create a Process component here
// For demo purposes, we just show what would happen
}
// Watch for trigger changes
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("launcherExample", "trigger", trigger)
}
}
}

View File

@@ -0,0 +1,244 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
implicitHeight: settingsColumn.implicitHeight
height: implicitHeight
Column {
id: settingsColumn
anchors.fill: parent
anchors.margins: 16
spacing: 16
Text {
text: "Launcher Example Plugin Settings"
font.pixelSize: 18
font.weight: Font.Bold
color: "#FFFFFF"
}
Text {
text: "This plugin demonstrates the launcher plugin system with example items and actions."
font.pixelSize: 14
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width - 32
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 12
width: parent.width - 32
Text {
text: "Trigger Configuration"
font.pixelSize: 16
font.weight: Font.Medium
color: "#FFFFFF"
}
Text {
text: noTriggerToggle.checked ? "Items will always show in the launcher (no trigger needed)." : "Set the trigger text to activate this plugin. Type the trigger in the launcher to filter to this plugin's items."
font.pixelSize: 12
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width
}
Row {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
contentItem: Text {
text: noTriggerToggle.text
font.pixelSize: 14
color: "#FFFFFF"
leftPadding: noTriggerToggle.indicator.width + 8
verticalAlignment: Text.AlignVCenter
}
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
radius: 4
border.color: noTriggerToggle.checked ? "#4CAF50" : "#60FFFFFF"
border.width: 2
color: noTriggerToggle.checked ? "#4CAF50" : "transparent"
Rectangle {
width: 12
height: 12
anchors.centerIn: parent
radius: 2
color: "#FFFFFF"
visible: noTriggerToggle.checked
}
}
onCheckedChanged: {
saveSettings("noTrigger", checked)
if (checked) {
saveSettings("trigger", "")
} else {
saveSettings("trigger", triggerField.text || "#")
}
}
}
}
Row {
spacing: 12
anchors.left: parent.left
anchors.right: parent.right
visible: !noTriggerToggle.checked
Text {
text: "Trigger:"
font.pixelSize: 14
color: "#FFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: triggerField
width: 100
height: 40
text: loadSettings("trigger", "#")
placeholderText: "#"
backgroundColor: "#30FFFFFF"
textColor: "#FFFFFF"
onTextEdited: {
const newTrigger = text.trim()
saveSettings("trigger", newTrigger || "#")
saveSettings("noTrigger", newTrigger === "")
}
}
Text {
text: "Examples: #, !, @, !ex, etc."
font.pixelSize: 12
color: "#AAFFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "Example Items:"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
Text {
text: "• Test Item 1, 2, 3 - Show toast notifications"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "• Example Copy Action - Copy text to clipboard"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "• Example Script Action - Demonstrate script execution"
font.pixelSize: 12
color: "#CCFFFFFF"
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "Usage:"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
bottomPadding: 24
Text {
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: noTriggerToggle.checked ? "2. Items are always visible in the launcher" : "2. Type your trigger (default: #) to filter to this plugin"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: noTriggerToggle.checked ? "3. Search works normally with plugin items included" : "3. Optionally add search terms: '# test' to find test items"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "4. Select an item and press Enter to execute its action"
font.pixelSize: 12
color: "#CCFFFFFF"
}
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("launcherExample", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("launcherExample", key, defaultValue)
}
return defaultValue
}
}

View File

@@ -0,0 +1,206 @@
# LauncherExample Plugin
A demonstration plugin that showcases the DMS launcher plugin system capabilities.
## Purpose
This plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:
- **Plugin Structure**: Proper manifest, launcher, and settings components
- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)
- **Item Management**: Providing searchable items to the launcher
- **Action Execution**: Handling different types of actions (toast, copy, script)
- **Settings Integration**: Configurable plugin settings with persistence
## Features
### Example Items
- **Test Items 1-3**: Demonstrate toast notifications
- **Copy Action**: Shows clipboard integration
- **Script Action**: Demonstrates command execution
### Trigger System
- **Default Trigger**: `#` (configurable in settings)
- **Empty Trigger Option**: Items can always be visible without needing a trigger
- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)
- **Search**: Type `# test` to search within plugin items
### Action Types
- `toast:message` - Shows toast notification
- `copy:text` - Copies text to clipboard
- `script:command` - Executes shell command (demo only)
## File Structure
```
PLUGINS/LauncherExample/
├── plugin.json # Plugin manifest
├── LauncherExampleLauncher.qml # Main launcher component
├── LauncherExampleSettings.qml # Settings interface
└── README.md # This documentation
```
## Installation
1. **Plugin Directory**: Copy to `~/.config/DankMaterialShell/plugins/LauncherExample`
2. **Enable Plugin**: Settings → Plugins → Enable "LauncherExample"
3. **Configure**: Set custom trigger in plugin settings if desired
## Usage
### With Trigger (Default)
1. Open launcher (Ctrl+Space or launcher button)
2. Type `#` to activate plugin trigger
3. Browse available items or add search terms
4. Press Enter to execute selected item
### Without Trigger (Empty Trigger Mode)
1. Enable "No trigger (always show)" in plugin settings
2. Open launcher - plugin items are always visible
3. Search works normally with plugin items included
4. Press Enter to execute selected item
### Search Examples
- `#` - Show all plugin items (with trigger enabled)
- `# test` - Show items matching "test"
- `# copy` - Show items matching "copy"
- `test` - Show all items matching "test" (with empty trigger enabled)
## Developer Guide
### Plugin Contract
**Launcher Component Requirements**:
```qml
// Required properties
property var pluginService: null
property string trigger: "#"
// Required signals
signal itemsChanged()
// Required functions
function getItems(query): array
function executeItem(item): void
```
**Item Structure**:
```javascript
{
name: "Item Name", // Display name
icon: "icon_name", // Material icon
comment: "Description", // Subtitle text
action: "type:data", // Action to execute
categories: ["PluginName"] // Category array
}
```
**Action Format**: `type:data` where:
- `type` - Action handler (toast, copy, script, etc.)
- `data` - Action-specific data
### Settings Integration
```qml
// Save setting
pluginService.savePluginData("pluginId", "key", value)
// Load setting
pluginService.loadPluginData("pluginId", "key", defaultValue)
```
### Trigger Configuration
The trigger can be configured in two ways:
1. **Empty Trigger** (No Trigger Mode):
- Check "No trigger (always show)" in settings
- Saves `trigger: ""` and `noTrigger: true`
- Items always appear in launcher alongside regular apps
2. **Custom Trigger**:
- Enter any string (e.g., `#`, `!`, `@`, `!ex`)
- Uncheck "No trigger" checkbox
- Items only appear when trigger is typed
### Manifest Structure
```json
{
"id": "launcherExample",
"name": "LauncherExample",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./LauncherExampleLauncher.qml",
"settings": "./LauncherExampleSettings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
Note: The `trigger` field in the manifest is optional and serves as the default trigger value.
## Extending This Plugin
### Adding New Items
```qml
function getItems(query) {
return [
{
name: "My Item",
icon: "custom_icon",
comment: "Does something cool",
action: "custom:action_data",
categories: ["LauncherExample"]
}
]
}
```
### Adding New Actions
```qml
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "custom":
handleCustomAction(actionData)
break
}
}
```
### Custom Trigger Logic
```qml
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
}
}
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("launcherExample", "trigger", trigger)
}
}
```
## Best Practices
1. **Unique Triggers**: Choose triggers that don't conflict with other plugins
2. **Clear Descriptions**: Write helpful item comments
3. **Error Handling**: Gracefully handle action failures
4. **Performance**: Return results quickly in getItems()
5. **Cleanup**: Destroy temporary objects in executeItem()
6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode
## Testing
Test the plugin by:
1. Installing and enabling in DMS
2. Testing with trigger enabled
3. Testing with empty trigger (no trigger mode)
4. Trying each action type
5. Testing search functionality
6. Verifying settings persistence
This plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!

View File

@@ -0,0 +1,17 @@
{
"id": "launcherExample",
"name": "LauncherExample",
"description": "Example launcher plugin demonstrating the launcher plugin system",
"version": "1.0.0",
"author": "DMS Team",
"icon": "extension",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./LauncherExampleLauncher.qml",
"settings": "./LauncherExampleSettings.qml",
"trigger": "#",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -771,12 +771,266 @@ The plugin API is currently **experimental**. Breaking changes may occur in mino
- Plugin update notifications
- Inter-plugin communication
## Launcher Plugins
Launcher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.
### Overview
Launcher plugins enable you to:
- Add custom items to the launcher/app drawer
- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)
- Execute custom actions when items are selected
- Provide searchable, categorized content
- Integrate seamlessly with the existing launcher
### Plugin Type Configuration
To create a launcher plugin, set the plugin type in `plugin.json`:
```json
{
"id": "myLauncher",
"name": "My Launcher Plugin",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./MyLauncher.qml",
"settings": "./MySettings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
### Launcher Component Contract
Create `MyLauncher.qml` with the following interface:
```qml
import QtQuick
import qs.Services
Item {
id: root
// Required properties
property var pluginService: null
property string trigger: "#"
// Required signals
signal itemsChanged()
// Required: Return array of launcher items
function getItems(query) {
return [
{
name: "Item Name",
icon: "icon_name",
comment: "Description",
action: "type:data",
categories: ["MyLauncher"]
}
]
}
// Required: Execute item action
function executeItem(item) {
const [type, data] = item.action.split(":", 2)
// Handle action based on type
}
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
}
}
}
```
### Item Structure
Each item returned by `getItems()` must include:
- `name` (string): Display name shown in launcher
- `icon` (string): Material Design icon name
- `comment` (string): Description/subtitle text
- `action` (string): Action identifier in `type:data` format
- `categories` (array): Array containing your plugin name
### Trigger System
Triggers control when your plugin's items appear in the launcher:
**Empty Trigger Mode** (No trigger):
- Items always visible alongside regular apps
- Search includes your items automatically
- Configure by saving empty trigger: `trigger: ""`
**Custom Trigger Mode**:
- Items only appear when trigger is typed
- Example: Type `#` to show only your plugin's items
- Type `# query` to search within your plugin
- Configure any string: `#`, `!`, `@`, `!custom`, etc.
### Trigger Configuration in Settings
Provide a settings component with trigger configuration:
```qml
import QtQuick
import QtQuick.Controls
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
Column {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
onCheckedChanged: {
saveSettings("noTrigger", checked)
if (checked) {
saveSettings("trigger", "")
} else {
saveSettings("trigger", triggerField.text || "#")
}
}
}
DankTextField {
id: triggerField
visible: !noTriggerToggle.checked
text: loadSettings("trigger", "#")
placeholderText: "#"
onTextEdited: {
saveSettings("trigger", text || "#")
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("myLauncher", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("myLauncher", key, defaultValue)
}
return defaultValue
}
}
```
### Action Execution
Handle different action types in `executeItem()`:
```qml
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Plugin", actionData)
}
break
case "copy":
// Copy to clipboard
break
case "script":
// Execute command
break
default:
console.warn("Unknown action:", actionType)
}
}
```
### Search and Filtering
The launcher automatically handles search when:
**With empty trigger**:
- Your items appear in all searches
- No prefix needed
**With custom trigger**:
- Type trigger alone: Shows all your items
- Type trigger + query: Filters your items by query
- The query parameter is passed to your `getItems(query)` function
Example `getItems()` implementation:
```qml
function getItems(query) {
const allItems = [
{name: "Item 1", ...},
{name: "Item 2", ...},
{name: "Test Item", ...}
]
if (!query || query.length === 0) {
return allItems
}
const lowerQuery = query.toLowerCase()
return allItems.filter(item => {
return item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
})
}
```
### Integration Flow
1. User opens launcher
2. If empty trigger: Your items appear alongside apps
3. If custom trigger: User types trigger (e.g., `#`)
4. Launcher calls `getItems(query)` on your plugin
5. Your items displayed with your plugin's category
6. User selects item and presses Enter
7. Launcher calls `executeItem(item)` on your plugin
### Best Practices
1. **Unique Triggers**: Choose non-conflicting trigger strings
2. **Fast Response**: Return results quickly from `getItems()`
3. **Clear Names**: Use descriptive item names and comments
4. **Error Handling**: Gracefully handle failures in `executeItem()`
5. **Cleanup**: Destroy temporary objects after use
6. **Empty Trigger Support**: Consider if your plugin benefits from always being visible
### Example Plugin
See `PLUGINS/LauncherExample/` for a complete working example demonstrating:
- Trigger configuration (including empty trigger mode)
- Multiple action types (toast, copy, script)
- Search/filtering implementation
- Settings integration
- Proper error handling
## Resources
- **Example Plugins**: [Emoji Picker](./ExampleEmojiPlugin/) [WorldClock](https://github.com/rochacbruno/WorldClock)
- **Example Plugins**:
- [Emoji Picker](./ExampleEmojiPlugin/)
- [WorldClock](https://github.com/rochacbruno/WorldClock)
- [LauncherExample](./LauncherExample/)
- [Calculator](https://github.com/rochacbruno/DankCalculator)
- **PluginService**: `Services/PluginService.qml`
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
- **Launcher Integration**: `Modules/AppDrawer/AppLauncher.qml`
- **Theme Reference**: `Common/Theme.qml`
- **Widget Library**: `Widgets/`

View File

@@ -12,7 +12,7 @@
</div>
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.
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/). Optimized for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors.
Features notifications, app launcher, wallpaper customization, and fully customizable with [plugins](https://github.com/AvengeMedia/dms-plugin-registry).
@@ -21,7 +21,7 @@ Features notifications, app launcher, wallpaper customization, and fully customi
<div align="center">
<div style="max-width: 700px; margin: 0 auto;">
https://github.com/user-attachments/assets/9b99dbbf-42d3-44ab-83b6-fae6c2aa3cc0
https://github.com/user-attachments/assets/40d2c56e-c1c9-4671-b04f-8f8b7b83b9ec
</div>
</div>
@@ -134,7 +134,7 @@ curl -fsSL https://install.danklinux.com | sh
### Compositor Setup
DankMaterialShell supports both **niri** and **Hyprland** compositors:
DankMaterialShell particularly aims at supporting the **niri** and **Hyprland** compositors, but it does support more wayland compositors with a diminished feature set (no monitor off, workspace switcher, overview integration, etc.):
**Niri**:
```bash
@@ -171,6 +171,9 @@ For detailed Hyprland installation instructions, see the [Hyprland wiki](https:/
#### Arch Linux - via AUR
```bash
# Stable release
paru -S dms-shell-bin
# Latest -git
paru -S dms-shell-git
```
@@ -661,6 +664,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

@@ -11,10 +11,13 @@ Singleton {
id: root
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
function searchApplications(query) {
if (!query || query.length === 0)
if (!query || query.length === 0) {
return applications
}
if (applications.length === 0)
return []
@@ -202,6 +205,11 @@ Singleton {
})
function getCategoryIcon(category) {
// Check if it's a plugin category
const pluginIcon = getPluginCategoryIcon(category)
if (pluginIcon) {
return pluginIcon
}
return categoryIcons[category] || "folder"
}
@@ -213,7 +221,12 @@ Singleton {
appCategories.forEach(cat => categories.add(cat))
}
return Array.from(categories).sort()
// Add plugin categories
const pluginCategories = getPluginCategories()
pluginCategories.forEach(cat => categories.add(cat))
const result = Array.from(categories).sort()
return result
}
function getAppsInCategory(category) {
@@ -221,9 +234,146 @@ Singleton {
return applications
}
// Check if it's a plugin category
const pluginItems = getPluginItems(category, "")
if (pluginItems.length > 0) {
return pluginItems
}
return applications.filter(app => {
const appCategories = getCategoriesForApp(app)
return appCategories.includes(category)
})
}
// Plugin launcher support functions
function getPluginCategories() {
if (typeof PluginService === "undefined") {
return []
}
const categories = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
const categoryName = plugin.name || pluginId
categories.push(categoryName)
}
return categories
}
function getPluginCategoryIcon(category) {
if (typeof PluginService === "undefined") return null
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
if ((plugin.name || pluginId) === category) {
return plugin.icon || "extension"
}
}
return null
}
function getAllPluginItems() {
if (typeof PluginService === "undefined") {
return []
}
let allItems = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const categoryName = launchers[pluginId].name || pluginId
const items = getPluginItems(categoryName, "")
allItems = allItems.concat(items)
}
return allItems
}
function getPluginItems(category, query) {
if (typeof PluginService === "undefined") return []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
if ((plugin.name || pluginId) === category) {
return getPluginItemsForPlugin(pluginId, query)
}
}
return []
}
function getPluginItemsForPlugin(pluginId, query) {
if (typeof PluginService === "undefined") {
return []
}
const component = PluginService.pluginLauncherComponents[pluginId]
if (!component) return []
try {
const instance = component.createObject(root, {
"pluginService": PluginService
})
if (instance && typeof instance.getItems === "function") {
const items = instance.getItems(query || "")
instance.destroy()
return items || []
}
if (instance) {
instance.destroy()
}
} catch (e) {
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e)
}
return []
}
function executePluginItem(item, pluginId) {
if (typeof PluginService === "undefined") return false
const component = PluginService.pluginLauncherComponents[pluginId]
if (!component) return false
try {
const instance = component.createObject(root, {
"pluginService": PluginService
})
if (instance && typeof instance.executeItem === "function") {
instance.executeItem(item)
instance.destroy()
return true
}
if (instance) {
instance.destroy()
}
} catch (e) {
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e)
}
return false
}
function searchPluginItems(query) {
if (typeof PluginService === "undefined") return []
let allItems = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const items = getPluginItemsForPlugin(pluginId, query)
allItems = allItems.concat(items)
}
return allItems
}
}

View File

@@ -13,15 +13,22 @@ Singleton {
property bool dmsAvailable: false
property var capabilities: []
property int apiVersion: 0
readonly property int expectedApiVersion: 1
property var availablePlugins: []
property var installedPlugins: []
property bool isConnected: false
property bool isConnecting: false
property bool subscribeConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
readonly property bool verboseLogs: Quickshell.env("DMS_VERBOSE_LOGS") === "1"
property var pendingRequests: ({})
property int requestIdCounter: 0
property bool shownOutdatedError: false
property string updateCommand: "dms update"
property bool checkingUpdateCommand: false
signal pluginsListReceived(var plugins)
signal installedPluginsReceived(var plugins)
@@ -30,12 +37,87 @@ Singleton {
signal operationError(string error)
signal connectionStateChanged()
signal networkStateUpdate(var data)
signal loginctlStateUpdate(var data)
signal loginctlEvent(var event)
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
detectUpdateCommand()
}
}
function detectUpdateCommand() {
checkingUpdateCommand = true
checkAurHelper.running = true
}
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true
}
}
Process {
id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const helper = text.trim()
if (helper.includes("paru")) {
checkDmsPackage.helper = "paru"
checkDmsPackage.running = true
} else if (helper.includes("yay")) {
checkDmsPackage.helper = "yay"
checkDmsPackage.running = true
} else {
updateCommand = "dms update"
checkingUpdateCommand = false
startSocketConnection()
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
updateCommand = "dms update"
checkingUpdateCommand = false
startSocketConnection()
}
}
}
Process {
id: checkDmsPackage
property string helper: ""
command: ["sh", "-c", "pacman -Qi dms-shell-git 2>/dev/null || pacman -Qi dms-shell-bin 2>/dev/null"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("dms-shell-git")) {
updateCommand = checkDmsPackage.helper + " -S dms-shell-git"
} else if (text.includes("dms-shell-bin")) {
updateCommand = checkDmsPackage.helper + " -S dms-shell-bin"
} else {
updateCommand = "dms update"
}
checkingUpdateCommand = false
startSocketConnection()
}
}
onExited: exitCode => {
if (exitCode !== 0) {
updateCommand = "dms update"
checkingUpdateCommand = false
startSocketConnection()
}
}
}
Process {
id: testProcess
command: ["test", "-S", root.socketPath]
@@ -56,11 +138,11 @@ Singleton {
}
isConnecting = true
socket.connected = true
requestSocket.connected = true
}
DankSocket {
id: socket
id: requestSocket
path: root.socketPath
connected: false
@@ -69,9 +151,12 @@ Singleton {
root.isConnected = true
root.isConnecting = false
root.connectionStateChanged()
subscribeSocket.connected = true
} else {
root.isConnected = false
root.isConnecting = false
root.apiVersion = 0
root.capabilities = []
root.connectionStateChanged()
}
}
@@ -82,28 +167,112 @@ Singleton {
return
}
if (root.verboseLogs) {
console.log("DMSService: Request socket <<", line)
}
try {
const response = JSON.parse(line)
if (response.capabilities) {
root.capabilities = response.capabilities
return
}
handleResponse(response)
} catch (e) {
console.warn("DMSService: Failed to parse response:", line, e)
console.warn("DMSService: Failed to parse request response:", line, e)
}
}
}
}
DankSocket {
id: subscribeSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
root.subscribeConnected = connected
if (connected) {
sendSubscribeRequest()
}
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
if (root.verboseLogs) {
console.log("DMSService: Subscribe socket <<", line)
}
try {
const response = JSON.parse(line)
handleSubscriptionEvent(response)
} catch (e) {
console.warn("DMSService: Failed to parse subscription event:", line, e)
}
}
}
}
function sendSubscribeRequest() {
const request = {
"method": "subscribe"
}
if (verboseLogs) {
console.log("DMSService: Subscribing to all services")
}
subscribeSocket.send(request)
}
function handleSubscriptionEvent(response) {
if (response.error) {
if (response.error.includes("unknown method") && response.error.includes("subscribe")) {
if (!shownOutdatedError) {
console.error("DMSService: Server does not support subscribe method")
ToastService.showError(
I18n.tr("DMS out of date"),
I18n.tr("To update, run the following command:"),
updateCommand
)
shownOutdatedError = true
}
}
return
}
if (!response.result) {
return
}
const service = response.result.service
const data = response.result.data
if (service === "server") {
apiVersion = data.apiVersion || 0
capabilities = data.capabilities || []
console.log("DMSService: Connected (API v" + apiVersion + ") -", JSON.stringify(capabilities))
if (apiVersion < expectedApiVersion) {
ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")")
}
} else if (service === "network") {
networkStateUpdate(data)
} else if (service === "loginctl") {
if (data.event) {
loginctlEvent(data)
} else {
loginctlStateUpdate(data)
}
}
}
function sendRequest(method, params, callback) {
if (!isConnected) {
if (callback) {
callback({
"error": "not connected to DMS socket"
})
"error": "not connected to DMS socket"
})
}
return
}
@@ -123,21 +292,10 @@ Singleton {
pendingRequests[id] = callback
}
socket.send(request)
requestSocket.send(request)
}
property var networkUpdateCallback: null
function handleResponse(response) {
if (response.id === undefined && response.result) {
if (response.result.type === "state_changed" && response.result.data) {
if (networkUpdateCallback) {
networkUpdateCallback(response.result.data)
}
}
return
}
const callback = pendingRequests[response.id]
if (callback) {

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

@@ -76,8 +76,6 @@ Singleton {
signal networksUpdated
signal connectionChanged
property bool subscriptionConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Component.onCompleted: {
@@ -87,51 +85,16 @@ Singleton {
}
}
DankSocket {
id: subscriptionSocket
path: root.socketPath
connected: networkAvailable
Connections {
target: DMSService
onConnectionStateChanged: {
root.subscriptionConnected = connected
if (connected) {
console.log("NetworkManagerService: Subscription socket connected")
function onNetworkStateUpdate(data) {
if (DMSService.verboseLogs) {
const networksCount = data.wifiNetworks?.length ?? "null"
console.log("NetworkManagerService: Subscription update received, networks:", networksCount)
}
updateState(data)
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
try {
const response = JSON.parse(line)
if (response.capabilities) {
console.log("NetworkManagerService: Subscription socket received capabilities")
Qt.callLater(() => sendSubscribeRequest())
return
}
if (response.result && response.result.type === "state_changed" && response.result.data) {
const networksCount = response.result.data.wifiNetworks?.length ?? "null"
console.log("NetworkManagerService: Subscription update received, networks:", networksCount)
updateState(response.result.data)
}
} catch (e) {
console.warn("NetworkManagerService: Failed to parse subscription response:", line, e)
}
}
}
}
function sendSubscribeRequest() {
subscriptionSocket.send({
"id": 1,
"method": "network.subscribe"
})
console.log("NetworkManagerService: Sent network.subscribe request")
}
Connections {
@@ -162,15 +125,15 @@ Singleton {
return
}
console.log("NetworkManagerService: Capabilities:", JSON.stringify(DMSService.capabilities))
networkAvailable = DMSService.capabilities.includes("network")
console.log("NetworkManagerService: Network available:", networkAvailable)
if (DMSService.verboseLogs) {
console.log("NetworkManagerService: Network available:", networkAvailable)
}
if (networkAvailable && !stateInitialized) {
console.log("NetworkManagerService: Requesting network state and starting subscription socket...")
stateInitialized = true
getState()
subscriptionSocket.connected = true
}
}
@@ -197,7 +160,9 @@ Singleton {
if (response.result) {
updateState(response.result)
if (!initialStateFetched && response.result.wifiEnabled && (!response.result.wifiNetworks || response.result.wifiNetworks.length === 0)) {
console.log("NetworkManagerService: Initial state has no networks, triggering scan")
if (DMSService.verboseLogs) {
console.log("NetworkManagerService: Initial state has no networks, triggering scan")
}
initialStateFetched = true
Qt.callLater(() => scanWifi())
}
@@ -258,14 +223,18 @@ Singleton {
function scanWifi() {
if (!networkAvailable || isScanning || !wifiEnabled) return
console.log("NetworkManagerService: Starting WiFi scan...")
if (DMSService.verboseLogs) {
console.log("NetworkManagerService: Starting WiFi scan...")
}
isScanning = true
DMSService.sendRequest("network.wifi.scan", null, response => {
isScanning = false
if (response.error) {
console.warn("NetworkManagerService: WiFi scan failed:", response.error)
} else {
console.log("NetworkManagerService: Scan completed, requesting fresh state...")
if (DMSService.verboseLogs) {
console.log("NetworkManagerService: Scan completed")
}
Qt.callLater(() => getState())
}
})

View File

@@ -23,6 +23,7 @@ Singleton {
property var outputs: ({})
property var windows: []
property var displayScales: ({})
property bool inOverview: false
@@ -68,6 +69,7 @@ Singleton {
const outputsData = JSON.parse(text)
outputs = outputsData
console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
updateDisplayScales()
if (windows.length > 0) {
windows = sortWindowsByLayout(windows)
}
@@ -169,6 +171,20 @@ Singleton {
outputsProcess.running = true
}
function updateDisplayScales() {
if (!outputs || Object.keys(outputs).length === 0) return
const scales = {}
for (const outputName in outputs) {
const output = outputs[outputName]
if (output.logical && output.logical.scale !== undefined) {
scales[outputName] = output.logical.scale
}
}
displayScales = scales
}
function sortWindowsByLayout(windowList) {
return [...windowList].sort((a, b) => {
const aWorkspace = workspaces[a.workspace_id]
@@ -395,6 +411,7 @@ Singleton {
function handleOutputsChanged(data) {
if (!data.outputs) return
outputs = data.outputs
updateDisplayScales()
windows = sortWindowsByLayout(windows)
}
@@ -407,9 +424,10 @@ Singleton {
validateProcess.running = true
} else {
configValidationOutput = ""
if (ToastService.toastVisible && ToastService.currentLevel === ToastService.levelError) {
if (ToastService.toastVisible && ToastService.currentLevel === ToastService.levelError && ToastService.currentMessage.startsWith("niri:")) {
ToastService.hideToast()
}
fetchOutputs()
if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
ToastService.showInfo("niri: config reloaded")
} else if (suppressNextConfigToast) {
@@ -467,6 +485,10 @@ Singleton {
return send({"Action": {"DoScreenTransition": {"delay_ms": 0}}})
}
function toggleOverview() {
return send({"Action": {"ToggleOverview": {}}})
}
function switchToWorkspace(workspaceIndex) {
return send({"Action": {"FocusWorkspace": {"reference": {"Index": workspaceIndex}}}})
}

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
@@ -15,6 +16,7 @@ Singleton {
property var loadedPlugins: ({})
property var pluginWidgetComponents: ({})
property var pluginDaemonComponents: ({})
property var pluginLauncherComponents: ({})
property string pluginDirectory: {
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
var configDirStr = configDir.toString()
@@ -24,197 +26,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 +232,48 @@ Singleton {
return true
}
var isDaemon = plugin.type === "daemon"
var componentMap = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
const isDaemon = plugin.type === "daemon"
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
const map = isDaemon ? pluginDaemonComponents : isLauncher ? pluginLauncherComponents : 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 if (isLauncher) {
const newLaunchers = Object.assign({}, pluginLauncherComponents)
newLaunchers[pluginId] = comp
pluginLauncherComponents = newLaunchers
} else {
var newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = component
const newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = comp
pluginWidgetComponents = newComponents
}
@@ -276,31 +283,42 @@ 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 isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"))
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 (isLauncher && pluginLauncherComponents[pluginId]) {
const newLaunchers = Object.assign({}, pluginLauncherComponents)
delete newLaunchers[pluginId]
pluginLauncherComponents = newLaunchers
} else if (pluginWidgetComponents[pluginId]) {
pluginWidgetComponents[pluginId]?.destroy()
var newComponents = Object.assign({}, pluginWidgetComponents)
const newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
}
@@ -326,30 +344,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 +379,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 +397,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 +410,44 @@ 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)
const fullId = pluginId + ":" + variantId
removeWidgetFromDankBar(fullId)
pluginDataChanged(pluginId)
}
function removeWidgetFromDankBar(widgetId) {
function filterWidget(widget) {
const id = typeof widget === "string" ? widget : widget.id
return id !== widgetId
}
const leftWidgets = SettingsData.dankBarLeftWidgets
const centerWidgets = SettingsData.dankBarCenterWidgets
const rightWidgets = SettingsData.dankBarRightWidgets
const newLeft = leftWidgets.filter(filterWidget)
const newCenter = centerWidgets.filter(filterWidget)
const newRight = rightWidgets.filter(filterWidget)
if (newLeft.length !== leftWidgets.length) {
SettingsData.setDankBarLeftWidgets(newLeft)
}
if (newCenter.length !== centerWidgets.length) {
SettingsData.setDankBarCenterWidgets(newCenter)
}
if (newRight.length !== rightWidgets.length) {
SettingsData.setDankBarRightWidgets(newRight)
}
}
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 +458,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 +468,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 +510,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) {
@@ -481,4 +532,61 @@ Singleton {
return false
}
}
// Launcher plugin helper functions
function getLauncherPlugins() {
const launchers = {}
// Check plugins that have launcher components
for (const pluginId in pluginLauncherComponents) {
const plugin = availablePlugins[pluginId]
if (plugin && plugin.loaded) {
launchers[pluginId] = plugin
}
}
return launchers
}
function getLauncherPlugin(pluginId) {
const plugin = availablePlugins[pluginId]
if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) {
return plugin
}
return null
}
function getPluginTrigger(pluginId) {
const plugin = getLauncherPlugin(pluginId)
if (plugin) {
const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!")
return customTrigger
}
return null
}
function getAllPluginTriggers() {
const triggers = {}
const launchers = getLauncherPlugins()
for (const pluginId in launchers) {
const trigger = getPluginTrigger(pluginId)
if (trigger && trigger.trim() !== "") {
triggers[trigger] = pluginId
}
}
return triggers
}
function getPluginsWithEmptyTrigger() {
const plugins = []
const launchers = getLauncherPlugins()
for (const pluginId in launchers) {
const trigger = getPluginTrigger(pluginId)
if (!trigger || trigger.trim() === "") {
plugins.push(pluginId)
}
}
return plugins
}
}

View File

@@ -22,24 +22,22 @@ Singleton {
function init() {}
function getSystemProfileImage() {
if (freedeskAvailable) {
const username = Quickshell.env("USER")
if (!username) return
if (!freedeskAvailable) return
DMSService.sendRequest("freedesktop.accounts.getUserIconFile", { username: username }, response => {
if (response.result && response.result.success) {
const iconFile = response.result.value || ""
if (iconFile && iconFile !== "" && iconFile !== "/var/lib/AccountsService/icons/") {
systemProfileImage = iconFile
if (!profileImage || profileImage === "") {
profileImage = iconFile
}
const username = Quickshell.env("USER")
if (!username) return
DMSService.sendRequest("freedesktop.accounts.getUserIconFile", { username: username }, response => {
if (response.result && response.result.success) {
const iconFile = response.result.value || ""
if (iconFile && iconFile !== "" && iconFile !== "/var/lib/AccountsService/icons/") {
systemProfileImage = iconFile
if (!profileImage || profileImage === "") {
profileImage = iconFile
}
}
})
} else {
systemProfileCheckProcess.running = true
}
}
})
}
function getUserProfileImage(username) {
@@ -52,26 +50,23 @@ Singleton {
return
}
if (freedeskAvailable) {
DMSService.sendRequest("freedesktop.accounts.getUserIconFile", { username: username }, response => {
if (response.result && response.result.success) {
const icon = response.result.value || ""
if (icon && icon !== "" && icon !== "/var/lib/AccountsService/icons/") {
profileImage = icon
} else {
profileImage = ""
}
if (!freedeskAvailable) {
profileImage = ""
return
}
DMSService.sendRequest("freedesktop.accounts.getUserIconFile", { username: username }, response => {
if (response.result && response.result.success) {
const icon = response.result.value || ""
if (icon && icon !== "" && icon !== "/var/lib/AccountsService/icons/") {
profileImage = icon
} else {
profileImage = ""
}
})
} else {
userProfileCheckProcess.command = [
"bash", "-c",
`uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`
]
userProfileCheckProcess.running = true
}
} else {
profileImage = ""
}
})
}
function setProfileImage(imagePath) {
@@ -86,25 +81,23 @@ Singleton {
}
function getSystemColorScheme() {
if (freedeskAvailable) {
DMSService.sendRequest("freedesktop.settings.getColorScheme", null, response => {
if (response.result) {
systemColorScheme = response.result.value || 0
if (!freedeskAvailable) return
if (typeof Theme !== "undefined") {
const shouldBeLightMode = (systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
DMSService.sendRequest("freedesktop.settings.getColorScheme", null, response => {
if (response.result) {
systemColorScheme = response.result.value || 0
if (typeof Theme !== "undefined") {
const shouldBeLightMode = (systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
}
}
})
} else {
systemColorSchemeCheckProcess.running = true
}
}
})
}
function setLightMode(isLightMode) {
@@ -114,49 +107,42 @@ Singleton {
}
function setSystemColorScheme(isLightMode) {
if (!settingsPortalAvailable) return
if (!settingsPortalAvailable || !freedeskAvailable) return
const colorScheme = isLightMode ? "default" : "prefer-dark"
colorSchemeSetProcess.command = ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", colorScheme]
colorSchemeSetProcess.running = true
}
Process {
id: colorSchemeSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
DMSService.sendRequest("freedesktop.settings.setColorScheme", { preferDark: !isLightMode }, response => {
if (!response.error) {
Qt.callLater(() => getSystemColorScheme())
}
}
})
}
function setSystemIconTheme(themeName) {
if (!settingsPortalAvailable || !freedeskAvailable) return
DMSService.sendRequest("freedesktop.settings.setIconTheme", { iconTheme: themeName }, response => {
if (response.error) {
console.warn("PortalService: Failed to set icon theme:", response.error)
}
})
}
function setSystemProfileImage(imagePath) {
if (!accountsServiceAvailable) return
if (!accountsServiceAvailable || !freedeskAvailable) return
if (freedeskAvailable) {
DMSService.sendRequest("freedesktop.accounts.setIconFile", { path: imagePath || "" }, response => {
if (response.error) {
console.warn("PortalService: Failed to set icon file:", response.error)
} else {
Qt.callLater(() => getSystemProfileImage())
}
})
} else {
const path = imagePath || ""
systemProfileSetProcess.command = ["bash", "-c", `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${path}'`]
systemProfileSetProcess.running = true
}
DMSService.sendRequest("freedesktop.accounts.setIconFile", { path: imagePath || "" }, response => {
if (response.error) {
console.warn("PortalService: Failed to set icon file:", response.error)
} else {
Qt.callLater(() => getSystemProfileImage())
}
})
}
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities()
} else {
console.log("PortalService: DMS_SOCKET not set, using fallback methods")
checkAccountsServiceFallback()
checkSettingsPortalFallback()
console.log("PortalService: DMS_SOCKET not set")
}
}
@@ -193,9 +179,7 @@ Singleton {
checkAccountsService()
checkSettingsPortal()
} else {
console.log("PortalService: freedesktop capability not available in DMS, using fallback methods")
checkAccountsServiceFallback()
checkSettingsPortalFallback()
console.log("PortalService: freedesktop capability not available in DMS")
}
}
@@ -225,14 +209,6 @@ Singleton {
})
}
function checkAccountsServiceFallback() {
accountsServiceCheckProcess.running = true
}
function checkSettingsPortalFallback() {
settingsPortalCheckProcess.running = true
}
function getGreeterUserProfileImage(username) {
if (!username) {
profileImage = ""
@@ -245,54 +221,6 @@ Singleton {
userProfileCheckProcess.running = true
}
Process {
id: accountsServiceCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
running: false
onExited: exitCode => {
accountsServiceAvailable = (exitCode === 0)
if (accountsServiceAvailable) {
getSystemProfileImage()
}
}
}
Process {
id: systemProfileCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const match = text.match(/string\s+"([^"]+)"/)
if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") {
systemProfileImage = match[1]
if (!profileImage || profileImage === "") {
profileImage = systemProfileImage
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
systemProfileImage = ""
}
}
}
Process {
id: systemProfileSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
getSystemProfileImage()
}
}
}
Process {
id: userProfileCheckProcess
command: []
@@ -316,50 +244,6 @@ Singleton {
}
}
Process {
id: settingsPortalCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
onExited: exitCode => {
settingsPortalAvailable = (exitCode === 0)
if (settingsPortalAvailable) {
getSystemColorScheme()
}
}
}
Process {
id: systemColorSchemeCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const match = text.match(/uint32 (\d+)/)
if (match && match[1]) {
systemColorScheme = parseInt(match[1])
if (typeof Theme !== "undefined") {
const shouldBeLightMode = (systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
}
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
systemColorScheme = 0
}
}
}
IpcHandler {
target: "profile"

View File

@@ -46,7 +46,6 @@ Singleton {
signal prepareForSleep()
signal loginctlStateChanged()
property bool subscriptionConnected: false
property bool stateInitialized: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -61,11 +60,14 @@ 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 {
console.log("SessionService: DMS_SOCKET not set, using fallback")
initFallbackLoginctl()
console.log("SessionService: DMS_SOCKET not set")
}
}
}
@@ -291,46 +293,41 @@ Singleton {
}
}
DankSocket {
id: subscriptionSocket
path: root.socketPath
connected: loginctlAvailable
Connections {
target: SessionData
onConnectionStateChanged: {
root.subscriptionConnected = connected
function onLoginctlLockIntegrationChanged() {
if (SessionData.loginctlLockIntegration) {
if (socketPath && socketPath.length > 0 && loginctlAvailable) {
if (!stateInitialized) {
stateInitialized = true
getLoginctlState()
syncLockBeforeSuspend()
}
}
} else {
stateInitialized = false
}
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
try {
const response = JSON.parse(line)
if (response.capabilities) {
Qt.callLater(() => sendSubscribeRequest())
return
}
if (response.result && response.result.type === "loginctl_event") {
handleLoginctlEvent(response.result)
} else if (response.result && response.result.type === "state_changed" && response.result.data) {
updateLoginctlState(response.result.data)
}
} catch (e) {
console.warn("SessionService: Failed to parse subscription response:", line, e)
}
function onLockBeforeSuspendChanged() {
if (SessionData.loginctlLockIntegration) {
syncLockBeforeSuspend()
}
}
}
function sendSubscribeRequest() {
subscriptionSocket.send({
"id": 2,
"method": "loginctl.subscribe"
})
Connections {
target: DMSService
enabled: SessionData.loginctlLockIntegration
function onLoginctlStateUpdate(data) {
updateLoginctlState(data)
}
function onLoginctlEvent(event) {
handleLoginctlEvent(event)
}
}
function checkDMSCapabilities() {
@@ -344,14 +341,14 @@ Singleton {
if (DMSService.capabilities.includes("loginctl")) {
loginctlAvailable = true
if (!stateInitialized) {
if (SessionData.loginctlLockIntegration && !stateInitialized) {
stateInitialized = true
getLoginctlState()
subscriptionSocket.connected = true
syncLockBeforeSuspend()
}
} else {
console.log("SessionService: loginctl capability not available in DMS, using fallback")
initFallbackLoginctl()
loginctlAvailable = false
console.log("SessionService: loginctl capability not available in DMS")
}
}
@@ -365,7 +362,23 @@ Singleton {
})
}
function syncLockBeforeSuspend() {
if (!loginctlAvailable) return
DMSService.sendRequest("loginctl.setLockBeforeSuspend", {
enabled: SessionData.lockBeforeSuspend
}, response => {
if (response.error) {
console.warn("SessionService: Failed to sync lock before suspend:", response.error)
} else {
console.log("SessionService: Synced lock before suspend:", SessionData.lockBeforeSuspend)
}
})
}
function updateLoginctlState(state) {
const wasLocked = locked
sessionId = state.sessionId || ""
sessionPath = state.sessionPath || ""
locked = state.locked || false
@@ -377,11 +390,10 @@ Singleton {
seat = state.seat || ""
display = state.display || ""
const wasPreparing = preparingForSleep
preparingForSleep = state.preparingForSleep || false
if (preparingForSleep && !wasPreparing) {
prepareForSleep()
if (locked && !wasLocked) {
sessionLocked()
} else if (!locked && wasLocked) {
sessionUnlocked()
}
loginctlStateChanged()
@@ -396,97 +408,7 @@ Singleton {
locked = false
lockedHint = false
sessionUnlocked()
} else if (event.event === "PrepareForSleep") {
preparingForSleep = event.data?.sleeping || false
if (preparingForSleep) {
prepareForSleep()
}
}
}
function initFallbackLoginctl() {
getSessionPathFallback.running = true
}
Process {
id: getSessionPathFallback
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", Quickshell.env("XDG_SESSION_ID") || "self"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const match = text.match(/objectpath '([^']+)'/)
if (match) {
sessionPath = match[1]
console.log("SessionService: Found session path (fallback):", sessionPath)
checkCurrentLockStateFallback.running = true
lockStateMonitorFallback.running = true
}
}
}
}
Process {
id: checkCurrentLockStateFallback
command: sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("true")) {
locked = true
lockedHint = true
sessionLocked()
}
}
}
}
Process {
id: lockStateMonitorFallback
command: sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1"] : []
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (sessionPath && line.includes(sessionPath)) {
if (line.includes("org.freedesktop.login1.Session.Lock")) {
locked = true
lockedHint = true
sessionLocked()
} else if (line.includes("org.freedesktop.login1.Session.Unlock")) {
locked = false
lockedHint = false
sessionUnlocked()
} else if (line.includes("LockedHint") && line.includes("true")) {
locked = true
lockedHint = true
loginctlStateChanged()
} else if (line.includes("LockedHint") && line.includes("false")) {
locked = false
lockedHint = false
loginctlStateChanged()
}
}
if (line.includes("PrepareForSleep") && line.includes("true") && SessionData.lockBeforeSuspend) {
preparingForSleep = true
prepareForSleep()
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("SessionService: gdbus monitor fallback failed, exit code:", exitCode)
}
}
}
Process {
id: lockSessionFallback
command: ["loginctl", "lock-session"]
running: false
}
}

View File

@@ -176,11 +176,27 @@ Singleton {
if (!distributionSupported || !pkgManager || updateCount === 0) return
const terminal = Quickshell.env("TERMINAL") || "xterm"
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]
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
const updateCommand = `${SettingsData.updaterCustomCommand} && echo "Updates complete! Press Enter to close..." && read`
const termClass = SettingsData.updaterTerminalAdditionalParams
var finalCommand = [terminal]
if (termClass.length > 0) {
finalCommand = finalCommand.concat(termClass.split(" "))
}
finalCommand.push("-e")
finalCommand.push("sh")
finalCommand.push("-c")
finalCommand.push(updateCommand)
updater.command = finalCommand
} else {
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
}

View File

@@ -16,36 +16,39 @@ Singleton {
property bool toastVisible: false
property var toastQueue: []
property string currentDetails: ""
property string currentCommand: ""
property bool hasDetails: false
property string wallpaperErrorStatus: ""
function showToast(message, level = levelInfo, details = "") {
function showToast(message, level = levelInfo, details = "", command = "") {
toastQueue.push({
"message": message,
"level": level,
"details": details
"details": details,
"command": command
})
if (!toastVisible) {
processQueue()
}
}
function showInfo(message, details = "") {
showToast(message, levelInfo, details)
function showInfo(message, details = "", command = "") {
showToast(message, levelInfo, details, command)
}
function showWarning(message, details = "") {
showToast(message, levelWarn, details)
function showWarning(message, details = "", command = "") {
showToast(message, levelWarn, details, command)
}
function showError(message, details = "") {
showToast(message, levelError, details)
function showError(message, details = "", command = "") {
showToast(message, levelError, details, command)
}
function hideToast() {
toastVisible = false
currentMessage = ""
currentDetails = ""
currentCommand = ""
hasDetails = false
currentLevel = levelInfo
toastTimer.stop()
@@ -64,7 +67,8 @@ Singleton {
currentMessage = toast.message
currentLevel = toast.level
currentDetails = toast.details || ""
hasDetails = currentDetails.length > 0
currentCommand = toast.command || ""
hasDetails = currentDetails.length > 0 || currentCommand.length > 0
toastVisible = true
resetToastState()

View File

@@ -143,6 +143,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}

View File

@@ -1,7 +1,9 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
PanelWindow {
id: root
@@ -69,33 +71,40 @@ PanelWindow {
readonly property real screenWidth: root.screen.width
readonly property real screenHeight: root.screen.height
readonly property real dpr: root.screen.devicePixelRatio
readonly property real dpr: {
if (CompositorService.isNiri && root.screen) {
const niriScale = NiriService.displayScales[root.screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && root.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === root.screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return root.screen?.devicePixelRatio || 1
}
readonly property real calculatedX: {
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
readonly property real alignedX: Theme.snap((() => {
if (SettingsData.dankBarPosition === SettingsData.Position.Left) {
return triggerY
return triggerY + SettingsData.dankBarBottomGap
} else if (SettingsData.dankBarPosition === SettingsData.Position.Right) {
return screenWidth - triggerY - popupWidth
return screenWidth - triggerY - SettingsData.dankBarBottomGap - popupWidth
} else {
const centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2)
return Math.max(Theme.popupDistance, Math.min(screenWidth - popupWidth - Theme.popupDistance, centerX))
}
}
readonly property real calculatedY: {
})(), dpr)
readonly property real alignedY: Theme.snap((() => {
if (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right) {
const centerY = triggerX + (triggerWidth / 2) - (popupHeight / 2)
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, centerY))
} else if (SettingsData.dankBarPosition === SettingsData.Position.Bottom) {
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, screenHeight - triggerY - popupHeight + Theme.popupDistance))
return Math.max(Theme.popupDistance, screenHeight - triggerY - popupHeight)
} else {
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY + Theme.popupDistance))
return Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY)
}
}
readonly property real alignedWidth: Theme.snap(popupWidth, dpr)
readonly property real alignedHeight: Theme.snap(popupHeight, dpr)
readonly property real alignedX: Theme.snap(calculatedX, dpr)
readonly property real alignedY: Theme.snap(calculatedY, dpr)
})(), dpr)
MouseArea {
anchors.fill: parent
@@ -117,6 +126,7 @@ PanelWindow {
height: alignedHeight
active: root.visible
asynchronous: false
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true"
opacity: shouldBeVisible ? 1 : 0
Behavior on opacity {

View File

@@ -42,6 +42,7 @@ StyledRect {
property real topPadding: Theme.spacingM
property real bottomPadding: Theme.spacingM
property bool ignoreLeftRightKeys: false
property bool ignoreTabKeys: false
property var keyForwardTargets: []
property Item keyNavigationTab: null
property Item keyNavigationBacktab: null
@@ -109,7 +110,7 @@ StyledRect {
onEditingFinished: root.editingFinished()
onAccepted: root.accepted()
onActiveFocusChanged: root.focusStateChanged(activeFocus)
Keys.forwardTo: root.ignoreLeftRightKeys ? root.keyForwardTargets : []
Keys.forwardTo: root.keyForwardTargets
Keys.onLeftPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true
@@ -122,10 +123,19 @@ StyledRect {
if (root.ignoreLeftRightKeys) {
event.accepted = true
} else {
// Allow normal TextInput cursor movement
event.accepted = false
}
}
Keys.onPressed: event => {
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i]) {
root.keyForwardTargets[i].Keys.pressed(event)
}
}
}
}
MouseArea {
anchors.fill: parent

View File

@@ -48,13 +48,13 @@ PanelWindow {
margins {
left: {
if (alignLeft) return Math.round(targetX)
if (alignRight) return Math.round(targetX - implicitWidth)
return Math.round(targetX - implicitWidth / 2)
if (alignLeft) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX)))
if (alignRight) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX - implicitWidth)))
return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.width ?? Screen.width) - implicitWidth - Theme.spacingS, targetX - implicitWidth / 2)))
}
top: {
if (alignLeft || alignRight) return Math.round(targetY - implicitHeight / 2)
return Math.round(targetY)
if (alignLeft || alignRight) return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.height ?? Screen.height) - implicitHeight - Theme.spacingS, targetY - implicitHeight / 2)))
return Math.round(Math.max(Theme.spacingS, Math.min((targetScreen?.height ?? Screen.height) - implicitHeight - Theme.spacingS, targetY)))
}
}

111
dms.spec Normal file
View File

@@ -0,0 +1,111 @@
# 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)
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Requires: fira-code-fonts
Requires: material-symbols-fonts
Requires: rsms-inter-fonts
Requires: quickshell-git
# Core utilities (Highly recommended for DMS functionality)
Recommends: brightnessctl
Recommends: cava
Recommends: cliphist
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
# Recommended system packages
Recommends: gammastep
Recommends: NetworkManager
Suggests: qt6ct
%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
# Use RPM version and build info
BUILD_TIME=$(date -u '+%%Y-%%m-%%d_%%H:%%M:%%S')
# Build with CGO disabled and version info
export CGO_ENABLED=0
export GOFLAGS="-trimpath -mod=readonly -modcacherw"
go build \
-tags distro_binary \
-ldflags="-s -w -X main.Version=%{version}-%{release} -X main.buildTime=${BUILD_TIME} -X main.commit=%{version}" \
-o dms \
./cmd/dms
popd
%install
# Install dms-cli binary
install -Dm755 %{_builddir}/danklinux-master/dms %{buildroot}%{_bindir}/dms
# 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
%changelog
{{{ git_dir_changelog }}}

26
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1759769087,
"narHash": "sha256-b4dEAjvIfIkw2/C47aZGDnwhTBEjqptDo8J5PizeTCo=",
"lastModified": 1760238269,
"narHash": "sha256-7CeGZM/Z/5Qt3AYByCRohGYGR1MRuXYzTTbkV/JxyAs=",
"owner": "AvengeMedia",
"repo": "dgop",
"rev": "ad6ad285e8b882c41eb8994ef7c91e151afb9a97",
"rev": "95acdfce2d323e28fa8f5a4f345160962034f2b5",
"type": "github"
},
"original": {
@@ -27,11 +27,11 @@
]
},
"locked": {
"lastModified": 1759946376,
"narHash": "sha256-/kQpJPH1y+U6V7N3bbGzvNRGfk9VuxdZev9Os4bS5ZQ=",
"lastModified": 1760241259,
"narHash": "sha256-DlLGn+4M6tIafoDsHr2WhHG2hrHrC24S2IL3+KAvjEU=",
"owner": "AvengeMedia",
"repo": "danklinux",
"rev": "98db89ffba290265bc4a886d13b8a27a53fdaca1",
"rev": "dae4c3ff4ce0feb930361c399747edb29d081775",
"type": "github"
},
"original": {
@@ -42,11 +42,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1759826507,
"narHash": "sha256-vwXL9H5zDHEQA0oFpww2one0/hkwnPAjc47LRph6d0I=",
"lastModified": 1760164275,
"narHash": "sha256-gKl2Gtro/LNf8P+4L3S2RsZ0G390ccd5MyXYrTdMCFE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "bce5fe2bb998488d8e7e7856315f90496723793c",
"rev": "362791944032cb532aabbeed7887a441496d5e6e",
"type": "github"
},
"original": {
@@ -63,11 +63,11 @@
]
},
"locked": {
"lastModified": 1759610621,
"narHash": "sha256-P3UPFd95mS/3aNgy40nCXAmyfR2bEEBd+tX6xfkYFb0=",
"lastModified": 1760228179,
"narHash": "sha256-4Z6k7lv3Zcgk3K+4h60LpqB9wCkR+utkYERU735U068=",
"ref": "refs/heads/master",
"rev": "c5c438f1cd1a76660a8658ef929a3d19e968e2ce",
"revCount": 689,
"rev": "c9d3ffb6043c5bf3f3009202bad7e0e5132c4a25",
"revCount": 693,
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
},

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

@@ -10,42 +10,40 @@
user = config.services.greetd.settings.default_session.user;
buildCompositorConfig = conf: pkgs.writeText "dmsgreeter-compositor-config" ''
${(lib.replaceString "_DMS_PATH_" "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms" (lib.fileContents conf))}
${cfg.compositor.extraConfig}
'';
sessionCommands = {
niri = ''
export PATH=$PATH:${lib.makeBinPath [ config.programs.niri.package ]}
niri -c ${buildCompositorConfig ../Modules/Greetd/assets/dms-niri.kdl} \
'';
hyprland = ''
export PATH=$PATH:${lib.makeBinPath [ config.programs.hyprland.package ]}
hyprland -c ${buildCompositorConfig ../Modules/Greetd/assets/dms-hypr.conf} \
'';
};
greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
export QT_QPA_PLATFORM=wayland
export XDG_SESSION_TYPE=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
export DMS_GREET_CFG_DIR="/var/lib/dmsgreeter/"
export PATH=$PATH:${lib.makeBinPath [ cfg.quickshell.package ]}
${sessionCommands.${cfg.compositor.name}} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
export PATH=$PATH:${lib.makeBinPath [ cfg.quickshell.package config.programs.${cfg.compositor.name}.package ]}
${lib.escapeShellArgs ([
"sh"
"${../Modules/Greetd/assets/dms-greeter}"
"--cache-dir"
"/var/lib/dmsgreeter"
"--command"
cfg.compositor.name
"-p"
"${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms"
]
++ lib.optionals (cfg.compositor.customConfig != "") [
"-C"
"${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}"
])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
'';
in {
imports =
let
msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead.";
in
[ (lib.mkRemovedOptionModule [ "programs" "dankMaterialShell" "greeter" "compositor" "extraConfig" ] msg) ];
options.programs.dankMaterialShell.greeter = {
enable = lib.mkEnableOption "DankMaterialShell greeter";
compositor.name = lib.mkOption {
type = types.enum ["niri" "hyprland"];
type = types.enum ["niri" "hyprland" "sway"];
description = "Compositor to run greeter in";
};
compositor.extraConfig = lib.mkOption {
compositor.customConfig = lib.mkOption {
type = types.lines;
default = "";
description = "Exra compositor config to include";
description = "Custom compositor config";
};
configFiles = lib.mkOption {
type = types.listOf types.path;

View File

@@ -54,34 +54,45 @@ key_of() {
local icon=$(echo "$json" | sed 's/.*"iconTheme": *"\([^"]*\)".*/\1/')
local matugen_type=$(echo "$json" | sed 's/.*"matugenType": *"\([^"]*\)".*/\1/')
local surface_base=$(echo "$json" | sed 's/.*"surfaceBase": *"\([^"]*\)".*/\1/')
local run_user_templates=$(echo "$json" | sed 's/.*"runUserTemplates": *\([^,}]*\).*/\1/')
[[ -z "$icon" ]] && icon="System Default"
[[ -z "$matugen_type" ]] && matugen_type="scheme-tonal-spot"
[[ -z "$surface_base" ]] && surface_base="sc"
echo "${kind}|${value}|${mode}|${icon}|${matugen_type}|${surface_base}" | sha256sum | cut -d' ' -f1
[[ -z "$run_user_templates" ]] && run_user_templates="true"
echo "${kind}|${value}|${mode}|${icon}|${matugen_type}|${surface_base}|${run_user_templates}" | sha256sum | cut -d' ' -f1
}
build_once() {
local json="$1"
local kind value mode icon matugen_type surface_base
local kind value mode icon matugen_type surface_base run_user_templates
kind=$(echo "$json" | sed 's/.*"kind": *"\([^"]*\)".*/\1/')
value=$(echo "$json" | sed 's/.*"value": *"\([^"]*\)".*/\1/')
mode=$(echo "$json" | sed 's/.*"mode": *"\([^"]*\)".*/\1/')
icon=$(echo "$json" | sed 's/.*"iconTheme": *"\([^"]*\)".*/\1/')
matugen_type=$(echo "$json" | sed 's/.*"matugenType": *"\([^"]*\)".*/\1/')
surface_base=$(echo "$json" | sed 's/.*"surfaceBase": *"\([^"]*\)".*/\1/')
run_user_templates=$(echo "$json" | sed 's/.*"runUserTemplates": *\([^,}]*\).*/\1/')
[[ -z "$icon" ]] && icon="System Default"
[[ -z "$matugen_type" ]] && matugen_type="scheme-tonal-spot"
[[ -z "$surface_base" ]] && surface_base="sc"
[[ -z "$run_user_templates" ]] && run_user_templates="true"
USER_MATUGEN_DIR="$CONFIG_DIR/matugen/dms"
TMP_CFG="$(mktemp)"
trap 'rm -f "$TMP_CFG"' RETURN
cat "$SHELL_DIR/matugen/configs/base.toml" > "$TMP_CFG"
if [[ "$run_user_templates" == "true" ]] && [[ -f "$CONFIG_DIR/matugen/config.toml" ]]; then
awk '/^\[config/{p=1} /^\[templates/{p=0} p' "$CONFIG_DIR/matugen/config.toml" >> "$TMP_CFG"
echo "" >> "$TMP_CFG"
else
echo "[config]" >> "$TMP_CFG"
echo "" >> "$TMP_CFG"
fi
grep -v '^\[config\]' "$SHELL_DIR/matugen/configs/base.toml" >> "$TMP_CFG"
echo "" >> "$TMP_CFG"
# Generate dank config dynamically with correct state directory
cat >> "$TMP_CFG" << EOF
[templates.dank]
input_path = '$SHELL_DIR/matugen/templates/dank.json'
@@ -118,8 +129,12 @@ EOF
cat "$SHELL_DIR/matugen/configs/vesktop.toml" >> "$TMP_CFG"
echo "" >> "$TMP_CFG"
fi
# Load user's matugen configurations
if [[ "$run_user_templates" == "true" ]] && [[ -f "$CONFIG_DIR/matugen/config.toml" ]]; then
awk '/^\[templates/{p=1} p' "$CONFIG_DIR/matugen/config.toml" >> "$TMP_CFG"
echo "" >> "$TMP_CFG"
fi
for config in "$USER_MATUGEN_DIR/configs"/*.toml; do
[[ -f "$config" ]] || continue
cat "$config" >> "$TMP_CFG"
@@ -258,14 +273,6 @@ EOF
mv "$TMP" "$CONFIG_DIR/kitty/dank-theme.conf"
fi
fi
COLOR_SCHEME=$([[ "$mode" == "light" ]] && echo default || echo prefer-dark)
if command -v dconf >/dev/null 2>&1; then
dconf write /org/gnome/desktop/interface/color-scheme "\"$COLOR_SCHEME\"" 2>/dev/null || true
[[ "$icon" != "System Default" && -n "$icon" ]] && dconf write /org/gnome/desktop/interface/icon-theme "\"$icon\"" 2>/dev/null || true
elif command -v gsettings >/dev/null 2>&1; then
gsettings set org.gnome.desktop.interface color-scheme "$COLOR_SCHEME" 2>/dev/null || true
[[ "$icon" != "System Default" && -n "$icon" ]] && gsettings set org.gnome.desktop.interface icon-theme "$icon" 2>/dev/null || true
fi
}
if command -v pywalfox >/dev/null 2>&1 && [[ -f "$HOME/.cache/wal/colors.json" ]]; then

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ from collections import defaultdict
def extract_qstr_strings(root_dir):
translations = defaultdict(list)
qstr_pattern = re.compile(r'qsTr\(["\']([^"\']+)["\']\)')
i18n_pattern = re.compile(r'I18n\.tr\(["\']([^"\']+)["\'],\s*["\']([^"\']+)["\']\)')
i18n_pattern = re.compile(r'I18n\.tr\(["\']([^"\']+)["\']\)')
for qml_file in Path(root_dir).rglob('*.qml'):
relative_path = qml_file.relative_to(root_dir)
@@ -24,8 +24,7 @@ def extract_qstr_strings(root_dir):
i18n_matches = i18n_pattern.findall(line)
for match in i18n_matches:
term = match[0]
translations[term].append({
translations[match].append({
'file': str(relative_path),
'line': line_num
})

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,28 +936,25 @@
"Pressure": "プレッシャー"
},
"Prevent screen timeout": {
"Prevent screen timeout": ""
"Prevent screen timeout": "画面のタイムアウトを防止"
},
"Primary": {
"Primary": ""
"Primary": "プライマリー"
},
"Privacy Indicator": {
"Privacy Indicator": ""
"Privacy Indicator": "プライバシーインジケーター"
},
"Process": {
"Process": "プロセス"
},
"QML (Qt Modeling Language)": {
"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 +966,7 @@
"Recent Colors": "最近の色"
},
"Recently Used Apps": {
"Recently Used Apps": ""
"Recently Used Apps": "最近使用したアプリ"
},
"Refresh": {
"Refresh": "リフレッシュ"
@@ -981,7 +978,7 @@
"Reset": "リセット"
},
"Running Apps": {
"Running Apps": ""
"Running Apps": "実行中のアプリ"
},
"Running Apps Only In Current Workspace": {
"Running Apps Only In Current Workspace": "現在のワークスペースでのみアプリを実行する"
@@ -1020,34 +1017,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 +1074,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 +1086,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 +1122,7 @@
"Storage & Disks": "ストレージとディスク"
},
"Surface": {
"Surface": ""
"Surface": "表面"
},
"Suspend": {
"Suspend": "一時停止"
@@ -1155,19 +1152,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 +1173,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 +1191,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 +1221,7 @@
"Turn off monitors after": "後にモニターの電源を切る"
},
"Unpin from Dock": {
"Unpin from Dock": ""
"Unpin from Dock": "ドックから固定を解除"
},
"Unsaved Changes": {
"Unsaved Changes": "保存されていない変更"
@@ -1242,7 +1239,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 +1251,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 +1260,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 +1290,10 @@
"Wave Progress Bars": "ウェーブプログレスバー"
},
"Weather": {
"Weather": ""
"Weather": "天気"
},
"Weather Widget": {
"Weather Widget": ""
"Weather Widget": "天気ウィジェット"
},
"WiFi is off": {
"WiFi is off": "Wi-Fiはオフ中"
@@ -1308,7 +1305,7 @@
"Widget Styling": "ウィジェットのスタイル"
},
"Widgets": {
"Widgets": ""
"Widgets": "ウィジェット"
},
"Wind": {
"Wind": "風"
@@ -1326,7 +1323,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 +1344,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": "• 信頼できるソースからのみインストールする"
@@ -1387,5 +1384,77 @@
},
"• yyyy - Year (2024)": {
"• yyyy - Year (2024)": "• yyyy - 年2024年"
},
"Border Color": {
"Border Color": "ボーダーの色"
},
"Border Opacity": {
"Border Opacity": "ボーダーの透明度"
},
"Border Thickness": {
"Border Thickness": "ボーダーの太さ"
},
"Choose the border accent color": {
"Choose the border accent color": "ボーダーの強調色を選ぶ"
},
"Enable loginctl lock integration": {
"Enable loginctl lock integration": "ログインロックの統合を有効に"
},
"Execute templates from ~/.config/matugen/config.toml": {
"Execute templates from ~/.config/matugen/config.toml": "~/.config/matugen/config.tomlからテンプレを実行"
},
"QML, JavaScript, Go": {
"QML, JavaScript, Go": "QML, JavaScript, Go"
},
"Run User Templates": {
"Run User Templates": "ユーザーのテンプレを実行"
},
"Current Weather": {
"Current Weather": "現在の天気"
},
"DMS out of date": {
"DMS out of date": "DMSに更新があります"
},
"Gamma Control": {
"Gamma Control": "ガンマ設定"
},
"Idle & Lock Screen": {
"Idle & Lock Screen": "アイドルおよびロックスクリーン"
},
"Matugen Settings": {
"Matugen Settings": "Matugen設定"
},
"Time & Weather": {
"Time & Weather": "時間および天気"
},
"To update, run the following command:": {
"To update, run the following command:": "更新するには以下のコマンドを実行してください:"
},
"loginctl not available - lock integration requires DMS socket connection": {
"loginctl not available - lock integration requires DMS socket connection": "loginctlが利用できません- ロック統合のためにDMS socketの接続が必要です。"
},
"Auto Popup Gaps": {
"Auto Popup Gaps": ""
},
"Automatically calculate popup distance from bar edge.": {
"Automatically calculate popup distance from bar edge.": ""
},
"Manual Gap Size": {
"Manual Gap Size": ""
},
"System Updater": {
"System Updater": ""
},
"System update custom command": {
"System update custom command": ""
},
"Terminal custom additional parameters": {
"Terminal custom additional parameters": ""
},
"Use Custom Command": {
"Use Custom Command": ""
},
"Use custom command for update your system": {
"Use custom command for update your system": ""
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -167,13 +167,6 @@
"reference": "",
"comment": ""
},
{
"term": "Animations",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "App Launcher",
"translation": "",
@@ -300,6 +293,13 @@
"reference": "",
"comment": ""
},
{
"term": "Auto Popup Gaps",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Auto-hide",
"translation": "",
@@ -342,6 +342,13 @@
"reference": "",
"comment": ""
},
{
"term": "Automatically calculate popup distance from bar edge.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Automatically cycle through wallpapers in the same folder",
"translation": "",
@@ -447,6 +454,27 @@
"reference": "",
"comment": ""
},
{
"term": "Border Color",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Border Opacity",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Border Thickness",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Brightness",
"translation": "",
@@ -545,6 +573,13 @@
"reference": "",
"comment": ""
},
{
"term": "Choose the border accent color",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Choose the logo displayed on the launcher button in DankBar",
"translation": "",
@@ -706,13 +741,6 @@
"reference": "",
"comment": ""
},
{
"term": "Control the speed of animations throughout the interface",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Copied to clipboard",
"translation": "",
@@ -776,6 +804,13 @@
"reference": "",
"comment": ""
},
{
"term": "Current Weather",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Current time and date display",
"translation": "",
@@ -839,6 +874,13 @@
"reference": "",
"comment": ""
},
{
"term": "DMS out of date",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "DMS_SOCKET not available",
"translation": "",
@@ -951,13 +993,6 @@
"reference": "",
"comment": ""
},
{
"term": "Display Settings",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen",
"translation": "",
@@ -1084,6 +1119,13 @@
"reference": "",
"comment": ""
},
{
"term": "Enable loginctl lock integration",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "End",
"translation": "",
@@ -1119,6 +1161,13 @@
"reference": "",
"comment": ""
},
{
"term": "Execute templates from ~/.config/matugen/config.toml",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "F1/I: Toggle • F10: Help",
"translation": "",
@@ -1273,6 +1322,13 @@
"reference": "",
"comment": ""
},
{
"term": "Gamma Control",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Github:",
"translation": "",
@@ -1392,6 +1448,13 @@
"reference": "",
"comment": ""
},
{
"term": "Idle & Lock Screen",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Idle Inhibitor",
"translation": "",
@@ -1616,6 +1679,13 @@
"reference": "",
"comment": ""
},
{
"term": "Manual Gap Size",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Manual Show/Hide",
"translation": "",
@@ -1637,6 +1707,13 @@
"reference": "",
"comment": ""
},
{
"term": "Matugen Settings",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Max apps to show",
"translation": "",
@@ -2148,13 +2225,6 @@
"reference": "",
"comment": ""
},
{
"term": "Power",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Power Off",
"translation": "",
@@ -2212,7 +2282,7 @@
"comment": ""
},
{
"term": "QML (Qt Modeling Language)",
"term": "QML, JavaScript, Go",
"translation": "",
"context": "",
"reference": "",
@@ -2288,6 +2358,13 @@
"reference": "",
"comment": ""
},
{
"term": "Run User Templates",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Running Apps",
"translation": "",
@@ -2708,6 +2785,13 @@
"reference": "",
"comment": ""
},
{
"term": "System Updater",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "System Updates",
"translation": "",
@@ -2722,6 +2806,13 @@
"reference": "",
"comment": ""
},
{
"term": "System update custom command",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select",
"translation": "",
@@ -2743,6 +2834,13 @@
"reference": "",
"comment": ""
},
{
"term": "Terminal custom additional parameters",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Text",
"translation": "",
@@ -2807,7 +2905,14 @@
"comment": ""
},
{
"term": "Time & Date",
"term": "Time & Weather",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "To update, run the following command:",
"translation": "",
"context": "",
"reference": "",
@@ -2904,6 +3009,13 @@
"reference": "",
"comment": ""
},
{
"term": "Use Custom Command",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Use Fahrenheit",
"translation": "",
@@ -2925,6 +3037,13 @@
"reference": "",
"comment": ""
},
{
"term": "Use custom command for update your system",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Use light theme instead of dark theme",
"translation": "",
@@ -3128,6 +3247,13 @@
"reference": "",
"comment": ""
},
{
"term": "loginctl not available - lock integration requires DMS socket connection",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "matugen not detected - dynamic theming unavailable",
"translation": "",