From 46e16a6c69c9d00ef04fb3ec93aaf1121d5e3f28 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 1 Oct 2025 13:04:48 -0400 Subject: [PATCH] Greetd: Add a greeter --- Common/Facts.qml | 53 + Common/SessionData.qml | 57 +- Common/SettingsData.qml | 26 +- Common/Theme.qml | 50 +- DMSGreeter.qml | 25 + DMSShell.qml | 638 ++++++++++ Modules/DankDash/Overview/UserInfoCard.qml | 89 +- Modules/Greetd/GreetdMemory.qml | 105 ++ Modules/Greetd/GreetdSettings.qml | 114 ++ Modules/Greetd/GreeterContent.qml | 1214 ++++++++++++++++++++ Modules/Greetd/GreeterState.qml | 28 + Modules/Greetd/GreeterSurface.qml | 18 + Modules/Greetd/README.md | 79 ++ Modules/Greetd/assets/dms-hypr.conf | 6 + Modules/Greetd/assets/dms-niri.kdl | 21 + Modules/Greetd/assets/greet-hyprland.sh | 3 + Modules/Greetd/assets/greet-niri.sh | 3 + Modules/Lock/LockScreenContent.qml | 92 +- Modules/WallpaperBackground.qml | 7 +- README.md | 6 + Services/PortalService.qml | 64 +- Widgets/DankCircularImage.qml | 6 +- Widgets/DankDropdown.qml | 41 +- shell.qml | 608 +--------- 24 files changed, 2540 insertions(+), 813 deletions(-) create mode 100644 Common/Facts.qml create mode 100644 DMSGreeter.qml create mode 100644 DMSShell.qml create mode 100644 Modules/Greetd/GreetdMemory.qml create mode 100644 Modules/Greetd/GreetdSettings.qml create mode 100644 Modules/Greetd/GreeterContent.qml create mode 100644 Modules/Greetd/GreeterState.qml create mode 100644 Modules/Greetd/GreeterSurface.qml create mode 100644 Modules/Greetd/README.md create mode 100644 Modules/Greetd/assets/dms-hypr.conf create mode 100644 Modules/Greetd/assets/dms-niri.kdl create mode 100755 Modules/Greetd/assets/greet-hyprland.sh create mode 100755 Modules/Greetd/assets/greet-niri.sh diff --git a/Common/Facts.qml b/Common/Facts.qml new file mode 100644 index 00000000..00df2ab8 --- /dev/null +++ b/Common/Facts.qml @@ -0,0 +1,53 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property var facts: [ + "A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.", + "A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.", + "Right now, 100 trillion solar neutrinos are passing through your body every second.", + "The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.", + "The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.", + "There's a nebula out there that's actually colder than empty space itself.", + "We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.", + "Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.", + "Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.", + "Distant galaxies can move away from us faster than light because space itself is stretching.", + "The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.", + "The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.", + "A day on Venus lasts longer than its entire year around the Sun.", + "On Mercury, the time between sunrises is 176 Earth days long.", + "In about 4.5 billion years, our galaxy will smash into Andromeda.", + "Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.", + "PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.", + "Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.", + "Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.", + "Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.", + "Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.", + "Counting to a billion at one number per second would take over 31 years.", + "Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.", + "Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.", + "Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.", + "Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.", + "Even at light-speed, you'd never catch up to most galaxies—space expands faster.", + "Only around 5% of galaxies are ever reachable—even at light-speed.", + "If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.", + "If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.", + "Our oldest radio signals will reach the Milky Way's center in 26,000 years.", + "Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.", + "The Moon moves 3.8 centimeters farther from Earth every year.", + "The universe creates 275 million new stars every single day.", + "Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.", + "If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.", + "The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have." + ] + + function getRandomFact() { + return facts[Math.floor(Math.random() * facts.length)] + } +} diff --git a/Common/SessionData.qml b/Common/SessionData.qml index c4231372..bf1b7cb5 100644 --- a/Common/SessionData.qml +++ b/Common/SessionData.qml @@ -12,6 +12,8 @@ Singleton { id: root + readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" + property bool isLightMode: false property string wallpaperPath: "" property string wallpaperLastPath: "" @@ -71,11 +73,17 @@ Singleton { Component.onCompleted: { - loadSettings() + if (!isGreeterMode) { + loadSettings() + } } function loadSettings() { - parseSettings(settingsFile.text()) + if (isGreeterMode) { + parseSettings(greeterSessionFile.text()) + } else { + parseSettings(settingsFile.text()) + } } function parseSettings(content) { @@ -142,10 +150,11 @@ Singleton { batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0 batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0 lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false - - // Generate system themes but don't override user's theme choice - if (typeof Theme !== "undefined") { - Theme.generateSystemThemesFromCurrentTheme() + + if (!isGreeterMode) { + if (typeof Theme !== "undefined") { + Theme.generateSystemThemesFromCurrentTheme() + } } } } catch (e) { @@ -154,6 +163,7 @@ Singleton { } function saveSettings() { + if (isGreeterMode) return settingsFile.setText(JSON.stringify({ "isLightMode": isLightMode, "wallpaperPath": wallpaperPath, @@ -620,22 +630,43 @@ Singleton { FileView { id: settingsFile - path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" - blockLoading: true + path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" + blockLoading: isGreeterMode blockWrites: true - watchChanges: true + watchChanges: !isGreeterMode onLoaded: { - parseSettings(settingsFile.text()) - hasTriedDefaultSession = false + if (!isGreeterMode) { + parseSettings(settingsFile.text()) + hasTriedDefaultSession = false + } } onLoadFailed: error => { - if (!hasTriedDefaultSession) { - hasTriedDefaultSession = true + if (!isGreeterMode && !hasTriedDefaultSettings) { + hasTriedDefaultSettings = true defaultSessionCheckProcess.running = true } } } + FileView { + id: greeterSessionFile + + path: { + const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms" + return greetCfgDir + "/session.json" + } + preload: isGreeterMode + blockLoading: false + blockWrites: true + watchChanges: false + printErrors: true + onLoaded: { + if (isGreeterMode) { + parseSettings(greeterSessionFile.text()) + } + } + } + Process { id: defaultSessionCheckProcess diff --git a/Common/SettingsData.qml b/Common/SettingsData.qml index 7808fd06..f0cc9cce 100644 --- a/Common/SettingsData.qml +++ b/Common/SettingsData.qml @@ -12,6 +12,8 @@ import qs.Services Singleton { id: root + readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" + enum Position { Top, Bottom, @@ -1274,9 +1276,11 @@ Singleton { } Component.onCompleted: { - loadSettings() - fontCheckTimer.start() - initializeListModels() + if (!isGreeterMode) { + loadSettings() + fontCheckTimer.start() + initializeListModels() + } } ListModel { @@ -1317,20 +1321,22 @@ Singleton { FileView { id: settingsFile - path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" - blockLoading: true + path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json" + blockLoading: isGreeterMode blockWrites: true atomicWrites: true - watchChanges: true + watchChanges: !isGreeterMode onLoaded: { - parseSettings(settingsFile.text()) - hasTriedDefaultSettings = false + if (!isGreeterMode) { + parseSettings(settingsFile.text()) + hasTriedDefaultSettings = false + } } onLoadFailed: error => { - if (!hasTriedDefaultSettings) { + if (!isGreeterMode && !hasTriedDefaultSettings) { hasTriedDefaultSettings = true defaultSettingsCheckProcess.running = true - } else { + } else if (!isGreeterMode) { applyStoredTheme() } } diff --git a/Common/Theme.qml b/Common/Theme.qml index be2b6bcd..a163f70c 100644 --- a/Common/Theme.qml +++ b/Common/Theme.qml @@ -31,9 +31,8 @@ Singleton { readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "") readonly property string wallpaperPath: { if (typeof SessionData === "undefined") return "" - + if (SessionData.perMonitorWallpaper) { - // Use first monitor's wallpaper for dynamic theming var screens = Quickshell.screens if (screens.length > 0) { var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name) @@ -93,6 +92,13 @@ Singleton { } } + function applyGreeterTheme(themeName) { + switchTheme(themeName, false, false) + if (themeName === dynamic && dynamicColorsFileView.path) { + dynamicColorsFileView.reload() + } + } + function getMatugenColor(path, fallback) { colorUpdateTrigger const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark" @@ -303,10 +309,13 @@ Singleton { currentThemeCategory = "generic" } } - if (savePrefs && typeof SettingsData !== "undefined") + const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode) + if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) SettingsData.setTheme(currentTheme) - generateSystemThemesFromCurrentTheme() + if (!isGreeterMode) { + generateSystemThemesFromCurrentTheme() + } } function setLightMode(light, savePrefs = true, enableTransition = false) { @@ -318,11 +327,14 @@ Singleton { return } + const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode) isLightMode = light - if (savePrefs && typeof SessionData !== "undefined") + if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) SessionData.setLightMode(isLightMode) - PortalService.setLightMode(isLightMode) - generateSystemThemesFromCurrentTheme() + if (!isGreeterMode) { + PortalService.setLightMode(isLightMode) + generateSystemThemesFromCurrentTheme() + } } function toggleLightMode(savePrefs = true) { @@ -599,7 +611,8 @@ Singleton { } function generateSystemThemesFromCurrentTheme() { - if (!matugenAvailable) + const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode) + if (!matugenAvailable || isGreeterMode) return const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) @@ -670,8 +683,9 @@ Singleton { command: ["which", "matugen"] onExited: code => { matugenAvailable = (code === 0) && !envDisableMatugen - if (!matugenAvailable) { - console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN") + const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode) + + if (!matugenAvailable || isGreeterMode) { return } @@ -722,10 +736,7 @@ Singleton { onExited: exitCode => { workerRunning = false - if (exitCode === 2) { - // Exit code 2 means wallpaper/color not found - this is expected on first run - console.log("Theme worker: wallpaper/color not found, skipping theme generation") - } else if (exitCode !== 0) { + if (exitCode !== 0 && exitCode !== 2) { if (typeof ToastService !== "undefined") { ToastService.showError("Theme worker failed (" + exitCode + ")") } @@ -814,8 +825,14 @@ Singleton { FileView { id: dynamicColorsFileView - path: stateDir + "/dms-colors.json" - watchChanges: currentTheme === dynamic + path: { + const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms" + const colorsPath = SessionData.isGreeterMode + ? greetCfgDir + "/colors.json" + : stateDir + "/dms-colors.json" + return colorsPath + } + watchChanges: currentTheme === dynamic && !SessionData.isGreeterMode function parseAndLoadColors() { try { @@ -828,6 +845,7 @@ Singleton { } } } catch (e) { + console.error("Theme: Failed to parse dynamic colors:", e) if (typeof ToastService !== "undefined") { ToastService.wallpaperErrorStatus = "error" ToastService.showError("Dynamic colors parse error: " + e.message) diff --git a/DMSGreeter.qml b/DMSGreeter.qml new file mode 100644 index 00000000..635b3e30 --- /dev/null +++ b/DMSGreeter.qml @@ -0,0 +1,25 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Greetd +import qs.Common +import "Modules/Greetd" + +ShellRoot { + id: root + + WlSessionLock { + id: sessionLock + locked: true + + onLockedChanged: { + if (!locked) { + console.log("Greetd session unlocked, exiting") + } + } + + GreeterSurface { + lock: sessionLock + } + } +} diff --git a/DMSShell.qml b/DMSShell.qml new file mode 100644 index 00000000..cde52c63 --- /dev/null +++ b/DMSShell.qml @@ -0,0 +1,638 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Modals +import qs.Modals.Clipboard +import qs.Modals.Common +import qs.Modals.Settings +import qs.Modals.Spotlight +import qs.Modules +import qs.Modules.AppDrawer +import qs.Modules.DankDash +import qs.Modules.ControlCenter +import qs.Modules.Dock +import qs.Modules.Lock +import qs.Modules.Notepad +import qs.Modules.Notifications.Center +import qs.Widgets +import qs.Modules.Notifications.Popup +import qs.Modules.OSD +import qs.Modules.ProcessList +import qs.Modules.Settings +import qs.Modules.DankBar +import qs.Modules.DankBar.Popouts +import qs.Services + + +Item { + Component.onCompleted: { + PortalService.init() + // Initialize DisplayService night mode functionality + DisplayService.nightModeEnabled + // Initialize WallpaperCyclingService + WallpaperCyclingService.cyclingActive + } + + WallpaperBackground {} + + Lock { + id: lock + + anchors.fill: parent + } + + Loader { + id: dankBarLoader + asynchronous: false + + property var currentPosition: SettingsData.dankBarPosition + + sourceComponent: DankBar { + onColorPickerRequested: colorPickerModal.show() + } + + onCurrentPositionChanged: { + const component = sourceComponent + sourceComponent = null + Qt.callLater(() => { + sourceComponent = component + }) + } + } + + Loader { + id: dockLoader + active: true + asynchronous: false + + property var currentPosition: SettingsData.dockPosition + + sourceComponent: Dock { + contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null + } + + onLoaded: { + if (item) { + dockContextMenuLoader.active = true + } + } + + onCurrentPositionChanged: { + console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock") + const comp = sourceComponent + sourceComponent = null + Qt.callLater(() => { + sourceComponent = comp + }) + } + } + + Loader { + id: dankDashPopoutLoader + + active: false + asynchronous: true + + sourceComponent: Component { + DankDashPopout { + id: dankDashPopout + } + } + } + + LazyLoader { + id: dockContextMenuLoader + + active: false + + DockContextMenu { + id: dockContextMenu + } + } + + LazyLoader { + id: notificationCenterLoader + + active: false + + NotificationCenterPopout { + id: notificationCenter + } + } + + Variants { + model: SettingsData.getFilteredScreens("notifications") + + delegate: NotificationPopupManager { + modelData: item + } + } + + LazyLoader { + id: controlCenterLoader + + active: false + + property var modalRef: colorPickerModal + + ControlCenterPopout { + id: controlCenterPopout + colorPickerModal: controlCenterLoader.modalRef + + onPowerActionRequested: (action, title, message) => { + powerConfirmModalLoader.active = true + if (powerConfirmModalLoader.item) { + powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary + powerConfirmModalLoader.item.show(title, message, function () { + 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 + } + }, function () {}) + } + } + onLockRequested: { + lock.activate() + } + } + } + + LazyLoader { + id: wifiPasswordModalLoader + + active: false + + WifiPasswordModal { + id: wifiPasswordModal + } + } + + LazyLoader { + id: networkInfoModalLoader + + active: false + + NetworkInfoModal { + id: networkInfoModal + } + } + + LazyLoader { + id: batteryPopoutLoader + + active: false + + BatteryPopout { + id: batteryPopout + } + } + + LazyLoader { + id: vpnPopoutLoader + + active: false + + VpnPopout { + id: vpnPopout + } + } + + LazyLoader { + id: powerMenuLoader + + active: false + + PowerMenu { + id: powerMenu + + onPowerActionRequested: (action, title, message) => { + powerConfirmModalLoader.active = true + if (powerConfirmModalLoader.item) { + powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary + powerConfirmModalLoader.item.show(title, message, function () { + 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 + } + }, function () {}) + } + } + } + } + + LazyLoader { + id: powerConfirmModalLoader + + active: false + + ConfirmModal { + id: powerConfirmModal + } + } + + LazyLoader { + id: processListPopoutLoader + + active: false + + ProcessListPopout { + id: processListPopout + } + } + + SettingsModal { + id: settingsModal + } + + LazyLoader { + id: appDrawerLoader + + active: false + + AppDrawerPopout { + id: appDrawerPopout + } + } + + SpotlightModal { + id: spotlightModal + } + + ClipboardHistoryModal { + id: clipboardHistoryModalPopup + } + + NotificationModal { + id: notificationModal + } + ColorPickerModal { + id: colorPickerModal + } + + LazyLoader { + id: processListModalLoader + + active: false + + ProcessListModal { + id: processListModal + } + } + + LazyLoader { + id: systemUpdateLoader + + active: false + + SystemUpdatePopout { + id: systemUpdatePopout + } + } + + Variants { + id: notepadSlideoutVariants + model: SettingsData.getFilteredScreens("notepad") + + delegate: DankSlideout { + id: notepadSlideout + modelData: item + title: qsTr("Notepad") + slideoutWidth: 480 + expandable: true + expandedWidthValue: 960 + customTransparency: SettingsData.notepadTransparencyOverride + + content: Component { + Notepad { + onHideRequested: { + notepadSlideout.hide() + } + } + } + + function toggle() { + if (isVisible) { + hide() + } else { + show() + } + } + } + } + + LazyLoader { + id: powerMenuModalLoader + + active: false + + PowerMenuModal { + id: powerMenuModal + + onPowerActionRequested: (action, title, message) => { + powerConfirmModalLoader.active = true + if (powerConfirmModalLoader.item) { + powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary + powerConfirmModalLoader.item.show(title, message, function () { + 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 + } + }, function () {}) + } + } + } + } + + IpcHandler { + function open() { + powerMenuModalLoader.active = true + if (powerMenuModalLoader.item) + powerMenuModalLoader.item.open() + + return "POWERMENU_OPEN_SUCCESS" + } + + function close() { + if (powerMenuModalLoader.item) + powerMenuModalLoader.item.close() + + return "POWERMENU_CLOSE_SUCCESS" + } + + function toggle() { + powerMenuModalLoader.active = true + if (powerMenuModalLoader.item) + powerMenuModalLoader.item.toggle() + + return "POWERMENU_TOGGLE_SUCCESS" + } + + target: "powermenu" + } + + IpcHandler { + function open(): string { + processListModalLoader.active = true + if (processListModalLoader.item) + processListModalLoader.item.show() + + return "PROCESSLIST_OPEN_SUCCESS" + } + + function close(): string { + if (processListModalLoader.item) + processListModalLoader.item.hide() + + return "PROCESSLIST_CLOSE_SUCCESS" + } + + function toggle(): string { + processListModalLoader.active = true + if (processListModalLoader.item) + processListModalLoader.item.toggle() + + return "PROCESSLIST_TOGGLE_SUCCESS" + } + + target: "processlist" + } + + IpcHandler { + function open(): string { + controlCenterLoader.active = true + if (controlCenterLoader.item) { + controlCenterLoader.item.open() + return "CONTROL_CENTER_OPEN_SUCCESS" + } + return "CONTROL_CENTER_OPEN_FAILED" + } + + function close(): string { + if (controlCenterLoader.item) { + controlCenterLoader.item.close() + return "CONTROL_CENTER_CLOSE_SUCCESS" + } + return "CONTROL_CENTER_CLOSE_FAILED" + } + + function toggle(): string { + controlCenterLoader.active = true + if (controlCenterLoader.item) { + controlCenterLoader.item.toggle() + return "CONTROL_CENTER_TOGGLE_SUCCESS" + } + return "CONTROL_CENTER_TOGGLE_FAILED" + } + + target: "control-center" + } + + IpcHandler { + function open(tab: string): string { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + switch (tab.toLowerCase()) { + case "media": + dankDashPopoutLoader.item.currentTabIndex = 1 + break + case "weather": + dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0 + break + default: + dankDashPopoutLoader.item.currentTabIndex = 0 + break + } + dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) + dankDashPopoutLoader.item.dashVisible = true + return "DASH_OPEN_SUCCESS" + } + return "DASH_OPEN_FAILED" + } + + function close(): string { + if (dankDashPopoutLoader.item) { + dankDashPopoutLoader.item.dashVisible = false + return "DASH_CLOSE_SUCCESS" + } + return "DASH_CLOSE_FAILED" + } + + function toggle(tab: string): string { + dankDashPopoutLoader.active = true + if (dankDashPopoutLoader.item) { + if (dankDashPopoutLoader.item.dashVisible) { + dankDashPopoutLoader.item.dashVisible = false + } else { + switch (tab.toLowerCase()) { + case "media": + dankDashPopoutLoader.item.currentTabIndex = 1 + break + case "weather": + dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0 + break + default: + dankDashPopoutLoader.item.currentTabIndex = 0 + break + } + dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) + dankDashPopoutLoader.item.dashVisible = true + } + return "DASH_TOGGLE_SUCCESS" + } + return "DASH_TOGGLE_FAILED" + } + + target: "dash" + } + + IpcHandler { + function getFocusedScreenName() { + if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) { + return Hyprland.focusedWorkspace.monitor.name + } + if (CompositorService.isNiri && NiriService.currentOutput) { + return NiriService.currentOutput + } + return "" + } + + function getActiveNotepadInstance() { + if (notepadSlideoutVariants.instances.length === 0) { + return null + } + + if (notepadSlideoutVariants.instances.length === 1) { + return notepadSlideoutVariants.instances[0] + } + + var focusedScreen = getFocusedScreenName() + if (focusedScreen && notepadSlideoutVariants.instances.length > 0) { + for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { + var slideout = notepadSlideoutVariants.instances[i] + if (slideout.modelData && slideout.modelData.name === focusedScreen) { + return slideout + } + } + } + + for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { + var slideout = notepadSlideoutVariants.instances[i] + if (slideout.isVisible) { + return slideout + } + } + + return notepadSlideoutVariants.instances[0] + } + + function open(): string { + var instance = getActiveNotepadInstance() + if (instance) { + instance.show() + return "NOTEPAD_OPEN_SUCCESS" + } + return "NOTEPAD_OPEN_FAILED" + } + + function close(): string { + var instance = getActiveNotepadInstance() + if (instance) { + instance.hide() + return "NOTEPAD_CLOSE_SUCCESS" + } + return "NOTEPAD_CLOSE_FAILED" + } + + function toggle(): string { + var instance = getActiveNotepadInstance() + if (instance) { + instance.toggle() + return "NOTEPAD_TOGGLE_SUCCESS" + } + return "NOTEPAD_TOGGLE_FAILED" + } + + target: "notepad" + } + + Variants { + model: SettingsData.getFilteredScreens("toast") + + delegate: Toast { + modelData: item + visible: ToastService.toastVisible + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: VolumeOSD { + modelData: item + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: MicMuteOSD { + modelData: item + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: BrightnessOSD { + modelData: item + } + } + + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: IdleInhibitorOSD { + modelData: item + } + } +} diff --git a/Modules/DankDash/Overview/UserInfoCard.qml b/Modules/DankDash/Overview/UserInfoCard.qml index 1939181e..59f3b6e6 100644 --- a/Modules/DankDash/Overview/UserInfoCard.qml +++ b/Modules/DankDash/Overview/UserInfoCard.qml @@ -13,83 +13,22 @@ Card { anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingM - Item { + DankCircularImage { id: avatarContainer - - property bool hasImage: profileImageLoader.status === Image.Ready - + width: 77 height: 77 anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.fill: parent - radius: 36 - color: Theme.primary - visible: !avatarContainer.hasImage - - StyledText { - anchors.centerIn: parent - text: UserInfoService.username.length > 0 ? UserInfoService.username.charAt(0).toUpperCase() : "b" - font.pixelSize: Theme.fontSizeXLarge + 4 - font.weight: Font.Bold - color: Theme.background - } - } - - Image { - id: profileImageLoader - - source: { - if (PortalService.profileImage === "") - return "" - - if (PortalService.profileImage.startsWith("/")) - return "file://" + PortalService.profileImage - - return PortalService.profileImage - } - smooth: true - asynchronous: true - mipmap: true - cache: true - visible: false - } - - MultiEffect { - anchors.fill: parent - anchors.margins: 2 - source: profileImageLoader - maskEnabled: true - maskSource: circularMask - visible: avatarContainer.hasImage - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - - Item { - id: circularMask - width: 77 - 4 - height: 77 - 4 - layer.enabled: true - layer.smooth: true - visible: false - - Rectangle { - anchors.fill: parent - radius: width / 2 - color: "black" - antialiasing: true - } - } - - DankIcon { - anchors.centerIn: parent - name: "person" - size: Theme.iconSize + 8 - color: Theme.error - visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error + imageSource: { + if (PortalService.profileImage === "") + return "" + + if (PortalService.profileImage.startsWith("/")) + return "file://" + PortalService.profileImage + + return PortalService.profileImage } + fallbackIcon: "person" } Column { @@ -104,7 +43,7 @@ Card { elide: Text.ElideRight width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 } - + Row { spacing: Theme.spacingS @@ -128,7 +67,7 @@ Card { width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS } } - + Row { spacing: Theme.spacingS @@ -141,7 +80,7 @@ Card { StyledText { id: uptimeText - + property real availableWidth: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS property real longTextWidth: { const fontSize = Math.round(Theme.fontSizeSmall || 12) diff --git a/Modules/Greetd/GreetdMemory.qml b/Modules/Greetd/GreetdMemory.qml new file mode 100644 index 00000000..ecb46b7a --- /dev/null +++ b/Modules/Greetd/GreetdMemory.qml @@ -0,0 +1,105 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms" + readonly property string sessionConfigPath: greetCfgDir + "/session.json" + readonly property string memoryFile: greetCfgDir + "/memory.json" + + property string lastSessionId: "" + property string lastSuccessfulUser: "" + property bool isLightMode: false + property bool nightModeEnabled: false + + Component.onCompleted: { + Quickshell.execDetached(["mkdir", "-p", greetCfgDir]) + loadMemory() + loadSessionConfig() + } + + function loadMemory() { + parseMemory(memoryFileView.text()) + } + + function loadSessionConfig() { + parseSessionConfig(sessionConfigFileView.text()) + } + + function parseSessionConfig(content) { + try { + if (content && content.trim()) { + const config = JSON.parse(content) + isLightMode = config.isLightMode !== undefined ? config.isLightMode : false + nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false + } + } catch (e) { + console.warn("Failed to parse greeter session config:", e) + } + } + + function parseMemory(content) { + try { + if (content && content.trim()) { + const memory = JSON.parse(content) + lastSessionId = memory.lastSessionId !== undefined ? memory.lastSessionId : "" + lastSuccessfulUser = memory.lastSuccessfulUser !== undefined ? memory.lastSuccessfulUser : "" + } + } catch (e) { + console.warn("Failed to parse greetd memory:", e) + } + } + + function saveMemory() { + memoryFileView.setText(JSON.stringify({ + "lastSessionId": lastSessionId, + "lastSuccessfulUser": lastSuccessfulUser + }, null, 2)) + } + + function setLastSessionId(id) { + lastSessionId = id || "" + saveMemory() + } + + function setLastSuccessfulUser(username) { + lastSuccessfulUser = username || "" + saveMemory() + } + + FileView { + id: memoryFileView + path: root.memoryFile + blockLoading: false + blockWrites: false + atomicWrites: true + watchChanges: false + printErrors: false + onLoaded: { + parseMemory(memoryFileView.text()) + } + } + + FileView { + id: sessionConfigFileView + path: root.sessionConfigPath + blockLoading: false + blockWrites: true + atomicWrites: false + watchChanges: false + printErrors: true + onLoaded: { + parseSessionConfig(sessionConfigFileView.text()) + } + onLoadFailed: error => { + console.warn("Could not load greeter session config from", root.sessionConfigPath, "error:", error) + } + } +} diff --git a/Modules/Greetd/GreetdSettings.qml b/Modules/Greetd/GreetdSettings.qml new file mode 100644 index 00000000..c495ff95 --- /dev/null +++ b/Modules/Greetd/GreetdSettings.qml @@ -0,0 +1,114 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + readonly property string configPath: { + const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms" + return greetCfgDir + "/settings.json" + } + + property string currentThemeName: "blue" + property bool settingsLoaded: false + property string customThemeFile: "" + property string matugenScheme: "scheme-tonal-spot" + property bool use24HourClock: true + property bool useFahrenheit: false + property bool nightModeEnabled: false + property string weatherLocation: "New York, NY" + property string weatherCoordinates: "40.7128,-74.0060" + property bool useAutoLocation: false + property bool weatherEnabled: true + property string iconTheme: "System Default" + property bool useOSLogo: false + property string osLogoColorOverride: "" + property real osLogoBrightness: 0.5 + property real osLogoContrast: 1 + property string fontFamily: "Inter Variable" + property string monoFontFamily: "Fira Code" + property int fontWeight: Font.Normal + property real fontScale: 1.0 + property real cornerRadius: 12 + property string widgetBackgroundColor: "sch" + property string surfaceBase: "s" + property string lockDateFormat: "" + property bool lockScreenShowPowerActions: true + property var screenPreferences: ({}) + property int animationSpeed: 2 + + readonly property string defaultFontFamily: "Inter Variable" + readonly property string defaultMonoFontFamily: "Fira Code" + + function parseSettings(content) { + try { + if (content && content.trim()) { + const settings = JSON.parse(content) + currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue" + customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "" + matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot" + use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true + useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false + nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false + weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY" + weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060" + useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false + weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true + iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default" + useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false + osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "" + osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5 + osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1 + fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily + monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily + fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal + fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0 + cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12 + widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch" + surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s" + lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "" + lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true + screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({}) + animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2 + settingsLoaded = true + + if (typeof Theme !== "undefined") { + Theme.applyGreeterTheme(currentThemeName) + } + } + } catch (e) { + console.warn("Failed to parse greetd settings:", e) + } + } + + function getEffectiveLockDateFormat() { + return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat + } + + function getFilteredScreens(componentId) { + const prefs = screenPreferences && screenPreferences[componentId] || ["all"] + if (prefs.includes("all")) { + return Quickshell.screens + } + return Quickshell.screens.filter(screen => prefs.includes(screen.name)) + } + + FileView { + id: settingsFile + path: root.configPath + blockLoading: false + blockWrites: true + atomicWrites: false + watchChanges: false + printErrors: true + onLoaded: { + parseSettings(settingsFile.text()) + } + } +} diff --git a/Modules/Greetd/GreeterContent.qml b/Modules/Greetd/GreeterContent.qml new file mode 100644 index 00000000..ebeb48c5 --- /dev/null +++ b/Modules/Greetd/GreeterContent.qml @@ -0,0 +1,1214 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Services.Greetd +import Quickshell.Services.Pam +import Quickshell.Services.Mpris +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + required property var sessionLock + + property string screenName: "" + property string randomFact: "" + property bool isPrimaryScreen: { + if (!Qt.application.screens || Qt.application.screens.length === 0) + return true + if (!screenName || screenName === "") + return true + return screenName === Qt.application.screens[0].name + } + + 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() + + if (isPrimaryScreen) { + sessionListProc.running = true + applyLastSuccessfulUser() + } + } + + function applyLastSuccessfulUser() { + const lastUser = GreetdMemory.lastSuccessfulUser + if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { + GreeterState.username = lastUser + GreeterState.usernameInput = lastUser + GreeterState.showPasswordInput = true + PortalService.getGreeterUserProfileImage(lastUser) + } + } + + Component.onDestruction: { + WeatherService.removeRef() + } + + // ! This was for development and testing, just leaving so people can see how I did it. + // Timer { + // id: autoUnlockTimer + // interval: 10000 + // running: true + // onTriggered: { + // root.sessionLock.locked = false + // GreeterState.unlocking = true + // const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex] + // if (sessionCmd) { + // GreetdMemory.setLastSessionId(sessionCmd.split(" ")[0]) + // Greetd.launch(sessionCmd.split(" "), [], true) + // } + // } + // } + + Connections { + target: GreetdMemory + enabled: isPrimaryScreen + function onLastSuccessfulUserChanged() { + applyLastSuccessfulUser() + } + } + + Connections { + target: GreeterState + function onUsernameChanged() { + if (GreeterState.username) { + PortalService.getGreeterUserProfileImage(GreeterState.username) + } + } + } + + DankBackdrop { + anchors.fill: parent + screenName: root.screenName + visible: { + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#")) + } + } + + Image { + id: wallpaperBackground + + anchors.fill: parent + source: { + var currentWallpaper = SessionData.getMonitorWallpaper(screenName) + if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) { + const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString() + const baseDir = Paths.strip(cacheHome) + const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg" + return screenshotPath + } + return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "" + } + fillMode: Image.PreserveAspectCrop + smooth: true + asynchronous: false + cache: true + visible: source !== "" + layer.enabled: true + + layer.effect: MultiEffect { + autoPaddingEnabled: false + blurEnabled: true + blur: 0.8 + blurMax: 32 + blurMultiplier: 1 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.4 + } + + SystemClock { + id: systemClock + precision: SystemClock.Minutes + } + + Rectangle { + anchors.fill: parent + color: "transparent" + + Item { + anchors.centerIn: parent + anchors.verticalCenterOffset: -100 + width: 400 + height: 140 + + StyledText { + id: clockText + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + text: { + const format = GreetdSettings.use24HourClock ? "HH:mm" : "h:mm AP" + return systemClock.date.toLocaleTimeString(Qt.locale(), format) + } + font.pixelSize: 120 + font.weight: Font.Light + color: "white" + lineHeight: 0.8 + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: clockText.bottom + anchors.topMargin: -20 + text: { + if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) { + return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat) + } + return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat) + } + font.pixelSize: Theme.fontSizeXLarge + color: "white" + opacity: 0.9 + } + } + + Item { + anchors.centerIn: parent + anchors.verticalCenterOffset: 80 + width: 380 + height: 140 + + ColumnLayout { + anchors.fill: parent + spacing: Theme.spacingM + + RowLayout { + spacing: Theme.spacingL + Layout.fillWidth: true + + DankCircularImage { + Layout.preferredWidth: 60 + Layout.preferredHeight: 60 + imageSource: { + if (PortalService.profileImage === "") { + return "" + } + + if (PortalService.profileImage.startsWith("/")) { + return "file://" + PortalService.profileImage + } + + return PortalService.profileImage + } + fallbackIcon: "person" + } + + Rectangle { + property bool showPassword: false + + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) + border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) + border.width: inputField.activeFocus ? 2 : 1 + + DankIcon { + id: lockIcon + + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + name: GreeterState.showPasswordInput ? "lock" : "person" + size: 20 + color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText + } + + TextInput { + id: inputField + + anchors.fill: parent + anchors.leftMargin: lockIcon.width + Theme.spacingM * 2 + anchors.rightMargin: { + let margin = Theme.spacingM + if (GreeterState.showPasswordInput && revealButton.visible) { + margin += revealButton.width + } + if (enterButton.visible) { + margin += enterButton.width + 2 + } + return margin + } + opacity: 0 + focus: true + echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal + onTextChanged: { + if (GreeterState.showPasswordInput) { + GreeterState.passwordBuffer = text + } else { + GreeterState.usernameInput = text + } + } + onAccepted: { + if (GreeterState.showPasswordInput) { + if (Greetd.state === GreetdState.Inactive && GreeterState.username) { + Greetd.createSession(GreeterState.username) + } + } else { + if (text.trim()) { + GreeterState.username = text.trim() + GreeterState.showPasswordInput = true + PortalService.getGreeterUserProfileImage(GreeterState.username) + GreeterState.passwordBuffer = "" + inputField.text = "" + } + } + } + + Component.onCompleted: { + text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput + if (isPrimaryScreen) + forceActiveFocus() + } + onVisibleChanged: { + if (visible && isPrimaryScreen) + forceActiveFocus() + } + } + + StyledText { + id: placeholder + + anchors.left: lockIcon.right + anchors.leftMargin: Theme.spacingM + anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : parent.right)) + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + text: { + if (GreeterState.unlocking) { + return "Logging in..." + } + if (Greetd.state !== GreetdState.Inactive) { + return "Authenticating..." + } + if (GreeterState.showPasswordInput) { + return "Password..." + } + return "Username..." + } + color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline) + font.pixelSize: Theme.fontSizeMedium + opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + StyledText { + anchors.left: lockIcon.right + anchors.leftMargin: Theme.spacingM + anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : parent.right)) + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + text: { + if (GreeterState.showPasswordInput) { + if (parent.showPassword) { + return GreeterState.passwordBuffer + } + return "•".repeat(Math.min(GreeterState.passwordBuffer.length, 25)) + } + return GreeterState.usernameInput + } + color: Theme.surfaceText + font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium + opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : GreeterState.usernameInput.length > 0) ? 1 : 0 + elide: Text.ElideRight + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + } + + DankActionButton { + id: revealButton + + anchors.right: enterButton.visible ? enterButton.left : parent.right + anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + iconName: parent.showPassword ? "visibility_off" : "visibility" + buttonSize: 32 + visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking + enabled: visible + onClicked: parent.showPassword = !parent.showPassword + } + + DankActionButton { + id: enterButton + + anchors.right: parent.right + anchors.rightMargin: 2 + anchors.verticalCenter: parent.verticalCenter + iconName: "keyboard_return" + buttonSize: 36 + visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking + enabled: true + onClicked: { + if (GreeterState.showPasswordInput) { + if (GreeterState.username) { + Greetd.createSession(GreeterState.username) + } + } else { + if (inputField.text.trim()) { + GreeterState.username = inputField.text.trim() + GreeterState.showPasswordInput = true + PortalService.getGreeterUserProfileImage(GreeterState.username) + GreeterState.passwordBuffer = "" + inputField.text = "" + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledText { + Layout.fillWidth: true + Layout.preferredHeight: 20 + text: { + if (GreeterState.pamState === "error") + return "Authentication error - try again" + if (GreeterState.pamState === "fail") + return "Incorrect password" + return "" + } + color: Theme.error + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignHCenter + visible: GreeterState.pamState !== "" + opacity: GreeterState.pamState !== "" ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Theme.spacingS + Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2 + Layout.preferredHeight: 40 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + opacity: GreeterState.showPasswordInput ? 1 : 0 + enabled: GreeterState.showPasswordInput + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.standardEasing + } + } + + Row { + id: switchUserRow + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "people" + size: Theme.iconSize - 4 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Switch User" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StateLayer { + stateColor: Theme.primary + cornerRadius: parent.radius + enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput + onClicked: { + GreeterState.reset() + inputField.text = "" + PortalService.profileImage = "" + } + } + } + } + } + + Row { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingXL + spacing: Theme.spacingL + + Row { + spacing: Theme.spacingS + visible: MprisController.activePlayer + anchors.verticalCenter: parent.verticalCenter + + Item { + width: 20 + height: Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + + Loader { + active: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing + + sourceComponent: Component { + Ref { + service: CavaService + } + } + } + + Timer { + running: !CavaService.cavaAvailable && MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing + interval: 256 + repeat: true + onTriggered: { + CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25] + } + } + + Row { + anchors.centerIn: parent + spacing: 1.5 + + Repeater { + model: 6 + + Rectangle { + width: 2 + height: { + if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) { + const rawLevel = CavaService.values[index] || 0 + const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100 + const maxHeight = Theme.iconSize - 2 + const minHeight = 3 + return minHeight + (scaledLevel / 100) * (maxHeight - minHeight) + } + return 3 + } + radius: 1.5 + color: "white" + anchors.verticalCenter: parent.verticalCenter + + Behavior on height { + NumberAnimation { + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.standardDecel + } + } + } + } + } + } + + StyledText { + text: { + const player = MprisController.activePlayer + if (!player?.trackTitle) + return "" + const title = player.trackTitle + const artist = player.trackArtist || "" + return artist ? title + " • " + artist : title + } + font.pixelSize: Theme.fontSizeLarge + color: "white" + opacity: 0.9 + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: Math.min(implicitWidth, 400) + wrapMode: Text.NoWrap + maximumLineCount: 1 + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + color: prevArea.containsMouse ? Qt.rgba(255, 255, 255, 0.2) : "transparent" + visible: MprisController.activePlayer + opacity: (MprisController.activePlayer?.canGoPrevious ?? false) ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: "skip_previous" + size: 12 + color: "white" + } + + MouseArea { + id: prevArea + anchors.fill: parent + enabled: MprisController.activePlayer?.canGoPrevious ?? false + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: MprisController.activePlayer?.previous() + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + anchors.verticalCenter: parent.verticalCenter + color: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? Qt.rgba(255, 255, 255, 0.9) : Qt.rgba(255, 255, 255, 0.2) + visible: MprisController.activePlayer + + DankIcon { + anchors.centerIn: parent + name: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" + size: 14 + color: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? "black" : "white" + } + + MouseArea { + anchors.fill: parent + enabled: MprisController.activePlayer + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: MprisController.activePlayer?.togglePlaying() + } + } + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + color: nextArea.containsMouse ? Qt.rgba(255, 255, 255, 0.2) : "transparent" + visible: MprisController.activePlayer + opacity: (MprisController.activePlayer?.canGoNext ?? false) ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: "skip_next" + size: 12 + color: "white" + } + + MouseArea { + id: nextArea + anchors.fill: parent + enabled: MprisController.activePlayer?.canGoNext ?? false + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: MprisController.activePlayer?.next() + } + } + } + } + + Rectangle { + width: 1 + height: 24 + color: Qt.rgba(255, 255, 255, 0.2) + anchors.verticalCenter: parent.verticalCenter + visible: MprisController.activePlayer && WeatherService.weather.available + } + + Row { + spacing: 6 + visible: WeatherService.weather.available + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.iconSize + color: "white" + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: (GreetdSettings.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Light + color: "white" + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: 1 + height: 24 + color: Qt.rgba(255, 255, 255, 0.2) + anchors.verticalCenter: parent.verticalCenter + visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable) + } + + Row { + spacing: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio) + + DankIcon { + name: NetworkService.networkStatus === "ethernet" ? "lan" : NetworkService.wifiSignalIcon + size: Theme.iconSize - 2 + color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5) + anchors.verticalCenter: parent.verticalCenter + visible: NetworkService.networkStatus !== "disconnected" + } + + DankIcon { + name: "bluetooth" + size: Theme.iconSize - 2 + color: "white" + anchors.verticalCenter: parent.verticalCenter + visible: BluetoothService.available && BluetoothService.enabled + } + + DankIcon { + name: { + if (!AudioService.sink?.audio) { + return "volume_up" + } + if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) { + return "volume_off" + } + if (AudioService.sink.audio.volume * 100 < 33) { + return "volume_down" + } + return "volume_up" + } + size: Theme.iconSize - 2 + color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white" + anchors.verticalCenter: parent.verticalCenter + visible: AudioService.sink && AudioService.sink.audio + } + } + + Rectangle { + width: 1 + height: 24 + color: Qt.rgba(255, 255, 255, 0.2) + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio)) + } + + Row { + spacing: 4 + visible: BatteryService.batteryAvailable + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: { + if (BatteryService.isCharging) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full" + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90" + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80" + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60" + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50" + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30" + } + + return "battery_charging_20" + } + if (BatteryService.isPluggedIn) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full" + } + + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90" + } + + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80" + } + + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60" + } + + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50" + } + + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30" + } + + return "battery_charging_20" + } + if (BatteryService.batteryLevel >= 95) { + return "battery_full" + } + + if (BatteryService.batteryLevel >= 85) { + return "battery_6_bar" + } + + if (BatteryService.batteryLevel >= 70) { + return "battery_5_bar" + } + + if (BatteryService.batteryLevel >= 55) { + return "battery_4_bar" + } + + if (BatteryService.batteryLevel >= 40) { + return "battery_3_bar" + } + + if (BatteryService.batteryLevel >= 25) { + return "battery_2_bar" + } + + return "battery_1_bar" + } + size: Theme.iconSize + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + + if (BatteryService.isCharging || BatteryService.isPluggedIn) { + return Theme.primary + } + + return "white" + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: BatteryService.batteryLevel + "%" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Light + color: "white" + anchors.verticalCenter: parent.verticalCenter + } + } + } + + StyledText { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Theme.spacingL + width: Math.min(parent.width - Theme.spacingXL * 2, implicitWidth) + text: root.randomFact + font.pixelSize: Theme.fontSizeSmall + color: "white" + opacity: 0.8 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.NoWrap + visible: root.randomFact !== "" + } + + Row { + 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() + }) + } + } + } + + Item { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: Theme.spacingXL + width: Math.max(200, currentSessionMetrics.width + 80) + height: 60 + + StyledTextMetrics { + id: currentSessionMetrics + text: root.currentSessionName + } + + property real longestSessionWidth: { + let maxWidth = 0 + for (var i = 0; i < sessionMetricsRepeater.count; i++) { + const item = sessionMetricsRepeater.itemAt(i) + if (item && item.width > maxWidth) { + maxWidth = item.width + } + } + return maxWidth + } + + Repeater { + id: sessionMetricsRepeater + model: GreeterState.sessionList + delegate: StyledTextMetrics { + text: modelData + } + } + + DankDropdown { + id: sessionDropdown + anchors.fill: parent + text: "" + description: "" + currentValue: root.currentSessionName + options: GreeterState.sessionList + enableFuzzySearch: GreeterState.sessionList.length > 5 + popupWidthOffset: 0 + popupWidth: Math.max(250, parent.longestSessionWidth + 100) + openUpwards: true + alignPopupRight: true + onValueChanged: value => { + const idx = GreeterState.sessionList.indexOf(value) + if (idx >= 0) { + GreeterState.currentSessionIndex = idx + GreeterState.selectedSession = GreeterState.sessionExecs[idx] + GreetdMemory.setLastSessionId(GreeterState.sessionExecs[idx].split(" ")[0]) + } + } + } + } + } + + FileView { + id: pamConfigWatcher + path: "/etc/pam.d/dankshell" + printErrors: false + } + + property int sessionCount: 0 + property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || "" + property int pendingParsers: 0 + + function finalizeSessionSelection() { + if (GreeterState.sessionList.length === 0) { + return + } + + root.sessionCount = GreeterState.sessionList.length + + const savedSession = GreetdMemory.lastSessionId + let foundSaved = false + if (savedSession) { + for (var i = 0; i < GreeterState.sessionExecs.length; i++) { + if (GreeterState.sessionExecs[i].toLowerCase().includes(savedSession.toLowerCase()) || GreeterState.sessionList[i].toLowerCase().includes(savedSession.toLowerCase())) { + GreeterState.currentSessionIndex = i + foundSaved = true + break + } + } + } + + if (!foundSaved) { + GreeterState.currentSessionIndex = 0 + } + + GreeterState.selectedSession = GreeterState.sessionExecs[GreeterState.currentSessionIndex] || GreeterState.sessionExecs[0] || "" + } + + Process { + id: sessionListProc + command: ["find", "/usr/share/wayland-sessions", "/usr/share/xsessions", "-name", "*.desktop", "-type", "f"] + running: false + + stdout: SplitParser { + onRead: data => { + if (data.trim()) { + root.pendingParsers++ + parseDesktopFile(data.trim()) + } + } + } + } + + function parseDesktopFile(path) { + const parser = desktopParser.createObject(null, { + "desktopPath": path + }) + } + + Component { + id: desktopParser + Process { + property string desktopPath: "" + command: ["bash", "-c", `grep -E '^(Name|Exec)=' "${desktopPath}"`] + running: true + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n") + let name = "" + let exec = "" + + for (const line of lines) { + if (line.startsWith("Name=")) { + name = line.substring(5).trim() + } else if (line.startsWith("Exec=")) { + exec = line.substring(5).trim() + } + } + + if (name && exec) { + if (!GreeterState.sessionList.includes(name)) { + let newList = GreeterState.sessionList.slice() + let newExecs = GreeterState.sessionExecs.slice() + newList.push(name) + newExecs.push(exec) + GreeterState.sessionList = newList + GreeterState.sessionExecs = newExecs + root.sessionCount = GreeterState.sessionList.length + } + } + } + } + + onExited: code => { + root.pendingParsers-- + if (root.pendingParsers === 0) { + Qt.callLater(root.finalizeSessionSelection) + } + destroy() + } + } + } + + Connections { + target: Greetd + enabled: isPrimaryScreen + + function onAuthMessage(message, error, responseRequired, echoResponse) { + if (responseRequired) { + Greetd.respond(GreeterState.passwordBuffer) + GreeterState.passwordBuffer = "" + inputField.text = "" + } else if (!error) { + Greetd.respond("") + } + } + + function onReadyToLaunch() { + root.sessionLock.locked = false + GreeterState.unlocking = true + const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex] + if (sessionCmd) { + GreetdMemory.setLastSessionId(sessionCmd.split(" ")[0]) + GreetdMemory.setLastSuccessfulUser(GreeterState.username) + Greetd.launch(sessionCmd.split(" "), [], true) + } + } + + function onAuthFailure(message) { + GreeterState.pamState = "fail" + GreeterState.reset() + inputField.text = "" + PortalService.profileImage = "" + placeholderDelay.restart() + } + + function onError(error) { + GreeterState.pamState = "error" + placeholderDelay.restart() + } + } + + Timer { + id: placeholderDelay + interval: 4000 + 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: "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() + } + } + } + } + } + } + } +} diff --git a/Modules/Greetd/GreeterState.qml b/Modules/Greetd/GreeterState.qml new file mode 100644 index 00000000..8431e29a --- /dev/null +++ b/Modules/Greetd/GreeterState.qml @@ -0,0 +1,28 @@ +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + property string passwordBuffer: "" + property string username: "" + property string usernameInput: "" + property bool showPasswordInput: false + property string selectedSession: "" + property string pamState: "" + property bool unlocking: false + + property var sessionList: [] + property var sessionExecs: [] + property int currentSessionIndex: 0 + + function reset() { + showPasswordInput = false + username = "" + usernameInput = "" + passwordBuffer = "" + pamState = "" + } +} diff --git a/Modules/Greetd/GreeterSurface.qml b/Modules/Greetd/GreeterSurface.qml new file mode 100644 index 00000000..bc62426a --- /dev/null +++ b/Modules/Greetd/GreeterSurface.qml @@ -0,0 +1,18 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Greetd + +WlSessionLockSurface { + id: root + + required property WlSessionLock lock + + color: "transparent" + + GreeterContent { + anchors.fill: parent + screenName: root.screen?.name ?? "" + sessionLock: root.lock + } +} diff --git a/Modules/Greetd/README.md b/Modules/Greetd/README.md new file mode 100644 index 00000000..6e3ea3a7 --- /dev/null +++ b/Modules/Greetd/README.md @@ -0,0 +1,79 @@ +# Dank (dms) Greeter + +A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the aesthetics of the dms lock screen. + +## Features + +- **Multi user**: Login with any system user +- **dms sync**: Sync settings with dms for consistent styling between shell and greeter +- **niri or Hyprland**: Use either niri or Hyprland for the greeter's compositor. +- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/dankshell` +- **Session Memory**: Remembers last selected session and user + +## Installation + +The easiest thing is to run `dms greeter install` or `dms` for interactive installation. + +Manual installation: +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 `/etc/greetd/start-dms.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` +```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 = "/etc/greetd/start-dms.sh"% +``` + +Enable the greeter with `sudo systemctl enable greetd` + +## Usage + +To run dms in greeter mode you just need to set `DMS_RUN_GREETER=1` in the environment. + +```bash +DMS_RUN_GREETER=1 qs -p /path/to/dms +``` + +### Configuration + +#### Compositor + +You can configure compositor specific settings such as outputs/displays the same as you would in niri or Hyprland. + +Simply edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` to change compositor settings for the greeter + +#### Personalization + +Wallpapers and themes and weather and clock formats and things are a TODO on the documentation, but it's configured exactly the same as dms. + +You can synchronize those configurations with a specific user if you want greeter settings to always mirror the shell. + +```bash +# For core settings (theme, clock formats, etc) +sudo ln -sf ~/.config/DankMaterialShell/settings.json /etc/greetd/.dms/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 +# For wallpaper based theming +sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /etc/greetd/.dms/dms-colors.json +``` + +You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable, the default is `/etc/greetd/.dms` + +It should be writable by the greeter user. diff --git a/Modules/Greetd/assets/dms-hypr.conf b/Modules/Greetd/assets/dms-hypr.conf new file mode 100644 index 00000000..ef16a8f5 --- /dev/null +++ b/Modules/Greetd/assets/dms-hypr.conf @@ -0,0 +1,6 @@ +env = DMS_RUN_GREETER,1 +env = QT_QPA_PLATFORM,wayland +env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1 +env = EGL_PLATFORM,gbm + +exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit" diff --git a/Modules/Greetd/assets/dms-niri.kdl b/Modules/Greetd/assets/dms-niri.kdl new file mode 100644 index 00000000..271d2ebf --- /dev/null +++ b/Modules/Greetd/assets/dms-niri.kdl @@ -0,0 +1,21 @@ +hotkey-overlay { + skip-at-startup +} + +environment { + DMS_RUN_GREETER "1" + QT_QPA_PLATFORM "wayland" + QT_WAYLAND_DISABLE_WINDOWDECORATION "1" +} + +spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation" + +debug { + keep-max-bpc-unchanged +} + +gestures { + hot-corners { + off + } +} \ No newline at end of file diff --git a/Modules/Greetd/assets/greet-hyprland.sh b/Modules/Greetd/assets/greet-hyprland.sh new file mode 100755 index 00000000..1ea99f64 --- /dev/null +++ b/Modules/Greetd/assets/greet-hyprland.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +EGL_PLATFORM=gbm Hyprland -c /etc/greetd/dms-hypr.conf diff --git a/Modules/Greetd/assets/greet-niri.sh b/Modules/Greetd/assets/greet-niri.sh new file mode 100755 index 00000000..e1c4bcfb --- /dev/null +++ b/Modules/Greetd/assets/greet-niri.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +EGL_PLATFORM=gbm niri -c /etc/greetd/dms-niri.kdl \ No newline at end of file diff --git a/Modules/Lock/LockScreenContent.qml b/Modules/Lock/LockScreenContent.qml index ebfba7f6..f8070f5d 100644 --- a/Modules/Lock/LockScreenContent.qml +++ b/Modules/Lock/LockScreenContent.qml @@ -44,10 +44,8 @@ Item { powerDialogVisible = false } - property var facts: ["A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.", "A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.", "Right now, 100 trillion solar neutrinos are passing through your body every second.", "The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.", "The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.", "There's a nebula out there that's actually colder than empty space itself.", "We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.", "Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.", "Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.", "Distant galaxies can move away from us faster than light because space itself is stretching.", "The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.", "The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.", "A day on Venus lasts longer than its entire year around the Sun.", "On Mercury, the time between sunrises is 176 Earth days long.", "In about 4.5 billion years, our galaxy will smash into Andromeda.", "Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.", "PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.", "Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.", "Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.", "Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.", "Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.", "Counting to a billion at one number per second would take over 31 years.", "Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.", "Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.", "Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.", "Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.", "Even at light-speed, you'd never catch up to most galaxies—space expands faster.", "Only around 5% of galaxies are ever reachable—even at light-speed.", "If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.", "If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.", "Our oldest radio signals will reach the Milky Way's center in 26,000 years.", "Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.", "The Moon moves 3.8 centimeters farther from Earth every year.", "The universe creates 275 million new stars every single day.", "Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.", "If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.", "The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."] - function pickRandomFact() { - randomFact = facts[Math.floor(Math.random() * facts.length)] + randomFact = Facts.getRandomFact() } Component.onCompleted: { @@ -180,93 +178,21 @@ Item { spacing: Theme.spacingL Layout.fillWidth: true - Item { - id: avatarContainer - - property bool hasImage: profileImageLoader.status === Image.Ready - + DankCircularImage { Layout.preferredWidth: 60 Layout.preferredHeight: 60 - - Rectangle { - anchors.fill: parent - radius: width / 2 - color: "transparent" - border.color: Theme.primary - border.width: 1 - visible: parent.hasImage - } - - Image { - id: profileImageLoader - - source: { - if (PortalService.profileImage === "") { - return "" - } - - if (PortalService.profileImage.startsWith("/")) { - return "file://" + PortalService.profileImage - } - - return PortalService.profileImage + imageSource: { + if (PortalService.profileImage === "") { + return "" } - smooth: true - asynchronous: true - mipmap: true - cache: true - visible: false - } - MultiEffect { - anchors.fill: parent - anchors.margins: 5 - source: profileImageLoader - maskEnabled: true - maskSource: circularMask - visible: avatarContainer.hasImage - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - - Item { - id: circularMask - - width: 60 - 10 - height: 60 - 10 - layer.enabled: true - layer.smooth: true - visible: false - - Rectangle { - anchors.fill: parent - radius: width / 2 - color: "black" - antialiasing: true + if (PortalService.profileImage.startsWith("/")) { + return "file://" + PortalService.profileImage } - } - Rectangle { - anchors.fill: parent - radius: width / 2 - color: Theme.primary - visible: !parent.hasImage - - DankIcon { - anchors.centerIn: parent - name: "person" - size: Theme.iconSize + 4 - color: Theme.primaryText - } - } - - DankIcon { - anchors.centerIn: parent - name: "warning" - size: Theme.iconSize + 4 - color: Theme.primaryText - visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error + return PortalService.profileImage } + fallbackIcon: "person" } Rectangle { diff --git a/Modules/WallpaperBackground.qml b/Modules/WallpaperBackground.qml index 4d534450..8cf1ebf2 100644 --- a/Modules/WallpaperBackground.qml +++ b/Modules/WallpaperBackground.qml @@ -11,7 +11,12 @@ LazyLoader { active: true Variants { - model: SettingsData.getFilteredScreens("wallpaper") + model: { + if (SessionData.isGreeterMode) { + return Quickshell.screens + } + return SettingsData.getFilteredScreens("wallpaper") + } PanelWindow { id: wallpaperWindow diff --git a/README.md b/README.md index 7c2c2504..be5160d6 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,12 @@ bindl = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 "" bind = SUPERSHIFT, N, exec, dms ipc call night toggle ``` +## Greeter + +You can install a matching [greetd](https://github.com/kennylevinsen/greetd) greeter, that will give you a greeter that matches the lock screen. + +It's as simple as running `dms greeter install` in most cases, but more information is in the [Greetd module](Modules/Greetd/README.md) + ## IPC Commands Control everything from the command line, or via keybinds. For comprehensive documentation of all available IPC commands, see [docs/IPC.md](docs/IPC.md). diff --git a/Services/PortalService.qml b/Services/PortalService.qml index 8860d75d..f9081518 100644 --- a/Services/PortalService.qml +++ b/Services/PortalService.qml @@ -21,10 +21,42 @@ Singleton { systemProfileCheckProcess.running = true } + function getUserProfileImage(username) { + if (!username) { + profileImage = "" + return + } + if (Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true") { + profileImage = "" + return + } + 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 + } + + function getGreeterUserProfileImage(username) { + if (!username) { + profileImage = "" + return + } + 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 + } + function setProfileImage(imagePath) { profileImage = imagePath - if (accountsServiceAvailable && imagePath) { - setSystemProfileImage(imagePath) + if (accountsServiceAvailable) { + if (imagePath) { + setSystemProfileImage(imagePath) + } else { + setSystemProfileImage("") + } } } @@ -51,11 +83,12 @@ Singleton { } function setSystemProfileImage(imagePath) { - if (!accountsServiceAvailable || !imagePath) { + if (!accountsServiceAvailable) { return } - const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${imagePath}'` + const path = imagePath || "" + const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${path}'` systemProfileSetProcess.command = ["bash", "-c", script] systemProfileSetProcess.running = true @@ -123,6 +156,29 @@ Singleton { } } + Process { + id: userProfileCheckProcess + command: [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const trimmed = text.trim() + if (trimmed && trimmed !== "" && !trimmed.includes("Error") && trimmed !== "/var/lib/AccountsService/icons/") { + root.profileImage = trimmed + } else { + root.profileImage = "" + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.profileImage = "" + } + } + } + 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"] diff --git a/Widgets/DankCircularImage.qml b/Widgets/DankCircularImage.qml index b53feb75..68f0fc45 100644 --- a/Widgets/DankCircularImage.qml +++ b/Widgets/DankCircularImage.qml @@ -69,7 +69,7 @@ Rectangle { name: root.fallbackIcon size: parent.width * 0.5 color: Theme.surfaceVariantText - visible: internalImage.status !== Image.Ready && root.imageSource === "" && root.fallbackIcon !== "" + visible: (internalImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== "" } @@ -77,8 +77,8 @@ Rectangle { anchors.centerIn: parent visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== "" text: root.fallbackText - font.pixelSize: Math.max(12, parent.width * 0.36) + font.pixelSize: Math.max(12, parent.width * 0.5) font.weight: Font.Bold - color: Theme.primaryText + color: Theme.surfaceVariantText } } \ No newline at end of file diff --git a/Widgets/DankDropdown.qml b/Widgets/DankDropdown.qml index 96b52088..7c5fec38 100644 --- a/Widgets/DankDropdown.qml +++ b/Widgets/DankDropdown.qml @@ -16,6 +16,9 @@ Rectangle { property bool enableFuzzySearch: false property int popupWidthOffset: 0 property int maxPopupHeight: 400 + property bool openUpwards: false + property int popupWidth: 0 + property bool alignPopupRight: false signal valueChanged(string value) @@ -102,10 +105,33 @@ Rectangle { return } - const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4) - popup.x = pos.x - (root.popupWidthOffset / 2) - popup.y = pos.y - popup.open() + if (root.openUpwards || root.alignPopupRight) { + popup.open() + Qt.callLater(() => { + if (root.openUpwards) { + const pos = dropdown.mapToItem(Overlay.overlay, 0, 0) + if (root.alignPopupRight) { + popup.x = pos.x + dropdown.width - popup.width + } else { + popup.x = pos.x - (root.popupWidthOffset / 2) + } + popup.y = pos.y - popup.height - 4 + } else { + const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4) + if (root.alignPopupRight) { + popup.x = pos.x + dropdown.width - popup.width + } else { + popup.x = pos.x - (root.popupWidthOffset / 2) + } + popup.y = pos.y + } + }) + } else { + const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4) + popup.x = pos.x - (root.popupWidthOffset / 2) + popup.y = pos.y + popup.open() + } } } @@ -213,7 +239,7 @@ Rectangle { } parent: Overlay.overlay - width: dropdown.width + root.popupWidthOffset + width: root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset) height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16) padding: 0 modal: true @@ -338,8 +364,9 @@ Rectangle { font.pixelSize: Theme.fontSizeMedium color: isCurrentValue ? Theme.primary : Theme.surfaceText font.weight: isCurrentValue ? Font.Medium : Font.Normal - width: parent.parent.width - parent.x - Theme.spacingS - elide: Text.ElideRight + width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS) + elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight + wrapMode: Text.NoWrap } } diff --git a/shell.qml b/shell.qml index 820f12bc..058098cb 100644 --- a/shell.qml +++ b/shell.qml @@ -19,7 +19,6 @@ import qs.Modules.Dock import qs.Modules.Lock import qs.Modules.Notifications.Center import qs.Widgets -import "./Modules/Notepad" import qs.Modules.Notifications.Popup import qs.Modules.OSD import qs.Modules.ProcessList @@ -31,612 +30,19 @@ import qs.Services ShellRoot { id: root - Component.onCompleted: { - PortalService.init() - // Initialize DisplayService night mode functionality - DisplayService.nightModeEnabled - // Initialize WallpaperCyclingService - WallpaperCyclingService.cyclingActive - } - - WallpaperBackground {} - - Lock { - id: lock - - anchors.fill: parent - } + readonly property bool runGreeter: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" Loader { - id: dankBarLoader + id: dmsShellLoader asynchronous: false - - property var currentPosition: SettingsData.dankBarPosition - - sourceComponent: DankBar { - onColorPickerRequested: colorPickerModal.show() - } - - onCurrentPositionChanged: { - const component = sourceComponent - sourceComponent = null - Qt.callLater(() => { - sourceComponent = component - }) - } + sourceComponent: DMSShell{} + active: !root.runGreeter } Loader { - id: dockLoader - active: true + id: dmsGreeterLoader asynchronous: false - - property var currentPosition: SettingsData.dockPosition - - sourceComponent: Dock { - contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null - } - - onLoaded: { - if (item) { - dockContextMenuLoader.active = true - } - } - - onCurrentPositionChanged: { - const comp = sourceComponent - sourceComponent = null - Qt.callLater(() => { - sourceComponent = comp - }) - } - } - - Loader { - id: dankDashPopoutLoader - - active: false - asynchronous: true - - sourceComponent: Component { - DankDashPopout { - id: dankDashPopout - } - } - } - - LazyLoader { - id: dockContextMenuLoader - - active: false - - DockContextMenu { - id: dockContextMenu - } - } - - LazyLoader { - id: notificationCenterLoader - - active: false - - NotificationCenterPopout { - id: notificationCenter - } - } - - Variants { - model: SettingsData.getFilteredScreens("notifications") - - delegate: NotificationPopupManager { - modelData: item - } - } - - LazyLoader { - id: controlCenterLoader - - active: false - - property var modalRef: colorPickerModal - - ControlCenterPopout { - id: controlCenterPopout - colorPickerModal: controlCenterLoader.modalRef - - onPowerActionRequested: (action, title, message) => { - powerConfirmModalLoader.active = true - if (powerConfirmModalLoader.item) { - powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary - powerConfirmModalLoader.item.show(title, message, function () { - 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 - } - }, function () {}) - } - } - onLockRequested: { - lock.activate() - } - } - } - - LazyLoader { - id: wifiPasswordModalLoader - - active: false - - WifiPasswordModal { - id: wifiPasswordModal - } - } - - LazyLoader { - id: networkInfoModalLoader - - active: false - - NetworkInfoModal { - id: networkInfoModal - } - } - - LazyLoader { - id: batteryPopoutLoader - - active: false - - BatteryPopout { - id: batteryPopout - } - } - - LazyLoader { - id: vpnPopoutLoader - - active: false - - VpnPopout { - id: vpnPopout - } - } - - LazyLoader { - id: powerMenuLoader - - active: false - - PowerMenu { - id: powerMenu - - onPowerActionRequested: (action, title, message) => { - powerConfirmModalLoader.active = true - if (powerConfirmModalLoader.item) { - powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary - powerConfirmModalLoader.item.show(title, message, function () { - 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 - } - }, function () {}) - } - } - } - } - - LazyLoader { - id: powerConfirmModalLoader - - active: false - - ConfirmModal { - id: powerConfirmModal - } - } - - LazyLoader { - id: processListPopoutLoader - - active: false - - ProcessListPopout { - id: processListPopout - } - } - - SettingsModal { - id: settingsModal - } - - LazyLoader { - id: appDrawerLoader - - active: false - - AppDrawerPopout { - id: appDrawerPopout - } - } - - SpotlightModal { - id: spotlightModal - } - - ClipboardHistoryModal { - id: clipboardHistoryModalPopup - } - - NotificationModal { - id: notificationModal - } - ColorPickerModal { - id: colorPickerModal - } - - LazyLoader { - id: processListModalLoader - - active: false - - ProcessListModal { - id: processListModal - } - } - - LazyLoader { - id: systemUpdateLoader - - active: false - - SystemUpdatePopout { - id: systemUpdatePopout - } - } - - Variants { - id: notepadSlideoutVariants - model: SettingsData.getFilteredScreens("notepad") - - delegate: DankSlideout { - id: notepadSlideout - modelData: item - title: qsTr("Notepad") - slideoutWidth: 480 - expandable: true - expandedWidthValue: 960 - customTransparency: SettingsData.notepadTransparencyOverride - - content: Component { - Notepad { - onHideRequested: { - notepadSlideout.hide() - } - } - } - - function toggle() { - if (isVisible) { - hide() - } else { - show() - } - } - } - } - - LazyLoader { - id: powerMenuModalLoader - - active: false - - PowerMenuModal { - id: powerMenuModal - - onPowerActionRequested: (action, title, message) => { - powerConfirmModalLoader.active = true - if (powerConfirmModalLoader.item) { - powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary - powerConfirmModalLoader.item.show(title, message, function () { - 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 - } - }, function () {}) - } - } - } - } - - IpcHandler { - function open() { - powerMenuModalLoader.active = true - if (powerMenuModalLoader.item) - powerMenuModalLoader.item.open() - - return "POWERMENU_OPEN_SUCCESS" - } - - function close() { - if (powerMenuModalLoader.item) - powerMenuModalLoader.item.close() - - return "POWERMENU_CLOSE_SUCCESS" - } - - function toggle() { - powerMenuModalLoader.active = true - if (powerMenuModalLoader.item) - powerMenuModalLoader.item.toggle() - - return "POWERMENU_TOGGLE_SUCCESS" - } - - target: "powermenu" - } - - IpcHandler { - function open(): string { - processListModalLoader.active = true - if (processListModalLoader.item) - processListModalLoader.item.show() - - return "PROCESSLIST_OPEN_SUCCESS" - } - - function close(): string { - if (processListModalLoader.item) - processListModalLoader.item.hide() - - return "PROCESSLIST_CLOSE_SUCCESS" - } - - function toggle(): string { - processListModalLoader.active = true - if (processListModalLoader.item) - processListModalLoader.item.toggle() - - return "PROCESSLIST_TOGGLE_SUCCESS" - } - - target: "processlist" - } - - IpcHandler { - function open(): string { - controlCenterLoader.active = true - if (controlCenterLoader.item) { - controlCenterLoader.item.open() - return "CONTROL_CENTER_OPEN_SUCCESS" - } - return "CONTROL_CENTER_OPEN_FAILED" - } - - function close(): string { - if (controlCenterLoader.item) { - controlCenterLoader.item.close() - return "CONTROL_CENTER_CLOSE_SUCCESS" - } - return "CONTROL_CENTER_CLOSE_FAILED" - } - - function toggle(): string { - controlCenterLoader.active = true - if (controlCenterLoader.item) { - controlCenterLoader.item.toggle() - return "CONTROL_CENTER_TOGGLE_SUCCESS" - } - return "CONTROL_CENTER_TOGGLE_FAILED" - } - - target: "control-center" - } - - IpcHandler { - function open(tab: string): string { - dankDashPopoutLoader.active = true - if (dankDashPopoutLoader.item) { - switch (tab.toLowerCase()) { - case "media": - dankDashPopoutLoader.item.currentTabIndex = 1 - break - case "weather": - dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0 - break - default: - dankDashPopoutLoader.item.currentTabIndex = 0 - break - } - dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) - dankDashPopoutLoader.item.dashVisible = true - return "DASH_OPEN_SUCCESS" - } - return "DASH_OPEN_FAILED" - } - - function close(): string { - if (dankDashPopoutLoader.item) { - dankDashPopoutLoader.item.dashVisible = false - return "DASH_CLOSE_SUCCESS" - } - return "DASH_CLOSE_FAILED" - } - - function toggle(tab: string): string { - dankDashPopoutLoader.active = true - if (dankDashPopoutLoader.item) { - if (dankDashPopoutLoader.item.dashVisible) { - dankDashPopoutLoader.item.dashVisible = false - } else { - switch (tab.toLowerCase()) { - case "media": - dankDashPopoutLoader.item.currentTabIndex = 1 - break - case "weather": - dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0 - break - default: - dankDashPopoutLoader.item.currentTabIndex = 0 - break - } - dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) - dankDashPopoutLoader.item.dashVisible = true - } - return "DASH_TOGGLE_SUCCESS" - } - return "DASH_TOGGLE_FAILED" - } - - target: "dash" - } - - IpcHandler { - function getFocusedScreenName() { - if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) { - return Hyprland.focusedWorkspace.monitor.name - } - if (CompositorService.isNiri && NiriService.currentOutput) { - return NiriService.currentOutput - } - return "" - } - - function getActiveNotepadInstance() { - if (notepadSlideoutVariants.instances.length === 0) { - return null - } - - if (notepadSlideoutVariants.instances.length === 1) { - return notepadSlideoutVariants.instances[0] - } - - var focusedScreen = getFocusedScreenName() - if (focusedScreen && notepadSlideoutVariants.instances.length > 0) { - for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { - var slideout = notepadSlideoutVariants.instances[i] - if (slideout.modelData && slideout.modelData.name === focusedScreen) { - return slideout - } - } - } - - for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { - var slideout = notepadSlideoutVariants.instances[i] - if (slideout.isVisible) { - return slideout - } - } - - return notepadSlideoutVariants.instances[0] - } - - function open(): string { - var instance = getActiveNotepadInstance() - if (instance) { - instance.show() - return "NOTEPAD_OPEN_SUCCESS" - } - return "NOTEPAD_OPEN_FAILED" - } - - function close(): string { - var instance = getActiveNotepadInstance() - if (instance) { - instance.hide() - return "NOTEPAD_CLOSE_SUCCESS" - } - return "NOTEPAD_CLOSE_FAILED" - } - - function toggle(): string { - var instance = getActiveNotepadInstance() - if (instance) { - instance.toggle() - return "NOTEPAD_TOGGLE_SUCCESS" - } - return "NOTEPAD_TOGGLE_FAILED" - } - - target: "notepad" - } - - Variants { - model: SettingsData.getFilteredScreens("toast") - - delegate: Toast { - modelData: item - visible: ToastService.toastVisible - } - } - - Variants { - model: SettingsData.getFilteredScreens("osd") - - delegate: VolumeOSD { - modelData: item - } - } - - Variants { - model: SettingsData.getFilteredScreens("osd") - - delegate: MicMuteOSD { - modelData: item - } - } - - Variants { - model: SettingsData.getFilteredScreens("osd") - - delegate: BrightnessOSD { - modelData: item - } - } - - Variants { - model: SettingsData.getFilteredScreens("osd") - - delegate: IdleInhibitorOSD { - modelData: item - } + sourceComponent: DMSGreeter{} + active: root.runGreeter } }