diff --git a/Greeter/Center.qml b/Greeter/Center.qml index d76dc8d..ad836af 100644 --- a/Greeter/Center.qml +++ b/Greeter/Center.qml @@ -201,13 +201,74 @@ ColumnLayout { Item { Layout.fillWidth: true Layout.topMargin: -Appearance.spacing.large - implicitHeight: message.implicitHeight + implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) Behavior on implicitHeight { Anim { } } + CustomText { + id: stateMessage + + readonly property string msg: { + if (Hypr.kbLayout !== Hypr.defaultKbLayout) { + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.capsLock) + return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.numLock) + return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); + } + + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON."); + if (Hypr.capsLock) + return qsTr("Caps lock is ON."); + if (Hypr.numLock) + return qsTr("Num lock is ON."); + + return ""; + } + property bool shouldBeVisible + + anchors.left: parent.left + anchors.right: parent.right + animateProp: "opacity" + color: DynamicColors.palette.m3onSurfaceVariant + font.family: Appearance.font.family.mono + horizontalAlignment: Qt.AlignHCenter + lineHeight: 1.2 + opacity: shouldBeVisible && !message.msg ? 1 : 0 + scale: shouldBeVisible && !message.msg ? 1 : 0.7 + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + + onMsgChanged: { + if (msg) { + if (opacity > 0) { + animate = true; + text = msg; + animate = false; + } else { + text = msg; + } + shouldBeVisible = true; + } else { + shouldBeVisible = false; + } + } + } + CustomText { id: message diff --git a/Greeter/Helpers/Hypr.qml b/Greeter/Helpers/Hypr.qml new file mode 100644 index 0000000..2d3eb31 --- /dev/null +++ b/Greeter/Helpers/Hypr.qml @@ -0,0 +1,163 @@ +pragma Singleton + +import ZShell +import ZShell.Internal +import Quickshell +import Quickshell.Hyprland +import Quickshell.Io +import QtQuick +import qs.Components + +Singleton { + id: root + + property string activeName + readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel + readonly property int activeWsId: focusedWorkspace?.id ?? 1 + property string applicationDir: "/usr/share/applications/" + readonly property bool capsLock: keyboard?.capsLock ?? false + readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??" + property string desktopName: "" + readonly property alias devices: extras.devices + readonly property alias extras: extras + readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor + readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace + property bool hadKeyboard + readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??" + readonly property string kbLayoutFull: keyboard?.activeKeymap ?? "Unknown" + readonly property var kbMap: new Map() + readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null + readonly property var monitors: Hyprland.monitors + readonly property bool numLock: keyboard?.numLock ?? false + readonly property alias options: extras.options + readonly property var toplevels: Hyprland.toplevels + readonly property var workspaces: Hyprland.workspaces + + signal configReloaded + + function dispatch(request: string): void { + Hyprland.dispatch(request); + } + + function getActiveScreen(): ShellScreen { + return Quickshell.screens.find(screen => root.monitorFor(screen) === root.focusedMonitor); + } + + function monitorFor(screen: ShellScreen): HyprlandMonitor { + return Hyprland.monitorFor(screen); + } + + function reloadDynamicConfs(): void { + extras.batchMessage(["keyword bindlni ,Caps_Lock,global,zshell:refreshDevices", "keyword bindlni ,Num_Lock,global,zshell:refreshDevices"]); + } + + Component.onCompleted: reloadDynamicConfs() + + // function updateActiveWindow(): void { + // root.desktopName = root.applicationDir + root.activeToplevel?.lastIpcObject.class + ".desktop"; + // } + + Connections { + function onRawEvent(event: HyprlandEvent): void { + const n = event.name; + if (n.endsWith("v2")) + return; + + if (n === "configreloaded") { + root.configReloaded(); + root.reloadDynamicConfs(); + } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { + Hyprland.refreshWorkspaces(); + Hyprland.refreshMonitors(); + // Qt.callLater( root.updateActiveWindow ); + } else if (["openwindow", "closewindow", "movewindow"].includes(n)) { + Hyprland.refreshToplevels(); + Hyprland.refreshWorkspaces(); + // Qt.callLater( root.updateActiveWindow ); + } else if (n.includes("mon")) { + Hyprland.refreshMonitors(); + // Qt.callLater( root.updateActiveWindow ); + } else if (n.includes("workspace")) { + Hyprland.refreshWorkspaces(); + // Qt.callLater( root.updateActiveWindow ); + } else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) { + Hyprland.refreshToplevels(); + // Qt.callLater( root.updateActiveWindow ); + } + } + + target: Hyprland + } + + FileView { + id: desktopEntryName + + path: root.desktopName + + onLoaded: { + const lines = text().split("\n"); + for (const line of lines) { + if (line.startsWith("Name=")) { + let name = line.replace("Name=", ""); + let caseFix = name[0].toUpperCase() + name.slice(1); + root.activeName = caseFix; + break; + } + } + } + } + + FileView { + id: kbLayoutFile + + path: Quickshell.env("ZSHELL_XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst" + + onLoaded: { + const layoutMatch = text().match(/! layout\n([\s\S]*?)\n\n/); + if (layoutMatch) { + const lines = layoutMatch[1].split("\n"); + for (const line of lines) { + if (!line.trim() || line.trim().startsWith("!")) + continue; + + const match = line.match(/^\s*([a-z]{2,})\s+([a-zA-Z() ]+)$/); + if (match) + root.kbMap.set(match[2], match[1]); + } + } + + const variantMatch = text().match(/! variant\n([\s\S]*?)\n\n/); + if (variantMatch) { + const lines = variantMatch[1].split("\n"); + for (const line of lines) { + if (!line.trim() || line.trim().startsWith("!")) + continue; + + const match = line.match(/^\s*([a-zA-Z0-9_-]+)\s+([a-z]{2,}): (.+)$/); + if (match) + root.kbMap.set(match[3], match[2]); + } + } + } + } + + IpcHandler { + function refreshDevices(): void { + extras.refreshDevices(); + } + + target: "hypr" + } + + CustomShortcut { + name: "refreshDevices" + + onPressed: extras.refreshDevices() + onReleased: extras.refreshDevices() + } + + HyprExtras { + id: extras + + } +} diff --git a/Greeter/Helpers/WallpaperPath.qml b/Greeter/Helpers/WallpaperPath.qml index 89e9a06..1ccb9f9 100644 --- a/Greeter/Helpers/WallpaperPath.qml +++ b/Greeter/Helpers/WallpaperPath.qml @@ -2,28 +2,9 @@ pragma Singleton import Quickshell import Quickshell.Io -import qs.Paths Singleton { id: root - property alias currentWallpaperPath: adapter.currentWallpaperPath - property alias lockscreenBg: adapter.lockscreenBg - - FileView { - id: fileView - - path: `${Paths.state}/wallpaper_path.json` - watchChanges: true - - onAdapterUpdated: writeAdapter() - onFileChanged: reload() - - JsonAdapter { - id: adapter - - property string currentWallpaperPath: "" - property string lockscreenBg: `${Paths.state}/lockscreen_bg.png` - } - } + property string lockscreenBg: `${Quickshell.shellDir}/images/greeter_bg.png` } diff --git a/Greeter/SessionDock.qml b/Greeter/SessionDock.qml index af97f17..54eaff4 100644 --- a/Greeter/SessionDock.qml +++ b/Greeter/SessionDock.qml @@ -62,28 +62,30 @@ ColumnLayout { } ListView { + id: sessions + anchors.fill: parent clip: true currentIndex: root.greeter.sessionIndex + highlightFollowsCurrentItem: false model: root.greeter.sessions spacing: Appearance.spacing.small delegate: CustomRect { + id: session + required property int index required property var modelData anchors.left: parent?.left anchors.right: parent?.right - color: ListView.isCurrentItem ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) implicitHeight: row.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Appearance.rounding.normal - Appearance.padding.smaller StateLayer { function onClicked(): void { root.greeter.sessionIndex = index; } - - color: ListView.isCurrentItem ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface } RowLayout { @@ -94,7 +96,8 @@ ColumnLayout { spacing: Appearance.spacing.normal MaterialIcon { - color: ListView.isCurrentItem ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant + color: session.index === sessions.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.extraLarge text: modelData.kind === "x11" ? "tv" : "desktop_windows" } @@ -104,7 +107,7 @@ ColumnLayout { CustomText { Layout.fillWidth: true - color: ListView.isCurrentItem ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + color: session.index === sessions.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface elide: Text.ElideRight font.pointSize: Appearance.font.size.normal font.weight: 600 @@ -113,18 +116,26 @@ ColumnLayout { CustomText { Layout.fillWidth: true - color: DynamicColors.palette.m3outline + color: session.index === sessions.currentIndex ? DynamicColors.palette.m3onPrimaryFixedVariant : DynamicColors.palette.m3onSurfaceVariant elide: Text.ElideRight font.family: Appearance.font.family.mono font.pointSize: Appearance.font.size.small text: modelData.kind } } + } + } + highlight: CustomRect { + color: DynamicColors.palette.m3primary + implicitHeight: sessions.currentItem?.implicitHeight ?? 0 + implicitWidth: sessions.width + radius: Appearance.rounding.normal - Appearance.padding.smaller + y: sessions.currentItem?.y ?? 0 - MaterialIcon { - color: DynamicColors.palette.m3primary - opacity: ListView.isCurrentItem ? 1 : 0 - text: "check_circle" + Behavior on y { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects } } } diff --git a/Greeter/UserImage.qml b/Greeter/UserImage.qml index 60fcfb0..d433e3d 100644 --- a/Greeter/UserImage.qml +++ b/Greeter/UserImage.qml @@ -1,7 +1,6 @@ import Quickshell import Quickshell.Widgets import QtQuick -import qs.Paths Item { id: root @@ -16,7 +15,7 @@ Item { anchors.fill: parent asynchronous: true fillMode: Image.PreserveAspectCrop - source: `${Paths.home}/.face` + source: `${Quickshell.shellDir}/images/.face` sourceSize.height: parent.height sourceSize.width: parent.width }