From 4e65ee0c3fc80eaede494398bd232ec016dfd04b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 16:28:07 +0000 Subject: [PATCH 1/4] Redesign shell theme from Material Design to Neovim TUI style Transform the Quickshell desktop shell from a GNOME-like Material Design 3 interface to a Neovim/LazyVim-inspired TUI aesthetic: - Replace MD3 surface layer tinting with flat Tokyo Night colors - Change from rounded corners to sharp/minimal TUI-style borders - Add lualine-inspired status bar with mode indicator and segmented sections - Create TuiPanel component for vim-style floating windows with title bars - Add which-key style keyboard hint bars to sidebars and switcher - Redesign ApplicationView with vim-style line numbers, command-line search - Update OSD to minimal TUI style with mode badges - Restyle notifications with vim-style bracketed labels - Add vim keybindings (j/k/h/l) to app switcher - Use monospace font throughout for consistent TUI look --- .../quickshell/modules/bar/StatusBar.qml | 787 +++++++++++------- .../quickshell/modules/common/Appearance.qml | 383 +++++---- .../quickshell/modules/common/TuiPanel.qml | 134 +++ dot_files/quickshell/modules/common/qmldir | 1 + .../notifications/NotificationPopup.qml | 149 ++-- dot_files/quickshell/modules/osd/Osd.qml | 204 ++--- .../modules/sidebars/ApplicationView.qml | 360 ++++---- .../modules/sidebars/SidebarLeft.qml | 53 +- .../modules/sidebars/SidebarRight.qml | 182 ++-- .../modules/switcher/AppSwitcher.qml | 366 +++++--- 10 files changed, 1626 insertions(+), 993 deletions(-) create mode 100644 dot_files/quickshell/modules/common/TuiPanel.qml diff --git a/dot_files/quickshell/modules/bar/StatusBar.qml b/dot_files/quickshell/modules/bar/StatusBar.qml index 3fae25a..d858e9a 100644 --- a/dot_files/quickshell/modules/bar/StatusBar.qml +++ b/dot_files/quickshell/modules/bar/StatusBar.qml @@ -8,6 +8,7 @@ import "../common" as Common import "../../services" as Services import "../../" as Root +// Lualine-inspired status bar with vim-style segments PanelWindow { id: root @@ -23,131 +24,236 @@ PanelWindow { implicitHeight: Common.Appearance.sizes.barHeight color: "transparent" - // Bar should be above click catchers so it's always clickable WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "statusbar" - // Bar button component - component BarButton: MouseArea { - id: button - - property string icon: "" - property string buttonText: "" - property string tooltip: "" - property bool highlighted: false - property color textColor: Common.Appearance.m3colors.onSurfaceVariant - - Layout.preferredHeight: 28 - // Icon-only buttons get minimal padding, buttons with text get more - Layout.preferredWidth: button.buttonText === "" - ? 28 - : buttonContent.implicitWidth + Common.Appearance.spacing.small * 2 + // Current "mode" based on shell state + property string currentMode: { + if (Root.GlobalStates.sidebarLeftOpen) { + return Root.GlobalStates.sidebarLeftView === "apps" ? "APPS" : "UPDATES" + } + if (Root.GlobalStates.sidebarRightOpen) { + switch (Root.GlobalStates.sidebarRightView) { + case "audio": return "AUDIO" + case "bluetooth": return "BLUETOOTH" + case "network": return "NETWORK" + case "calendar": return "CALENDAR" + case "notifications": return "NOTIFY" + case "power": return "POWER" + case "weather": return "WEATHER" + default: return "NORMAL" + } + } + return "NORMAL" + } - hoverEnabled: true - cursorShape: Qt.PointingHandCursor + property color modeColor: { + if (currentMode === "NORMAL") return Common.Appearance.colors.modeNormal + if (currentMode === "APPS" || currentMode === "UPDATES") return Common.Appearance.colors.modeInsert + return Common.Appearance.colors.modeVisual + } - Rectangle { - anchors.fill: parent - radius: Common.Appearance.rounding.small - color: button.containsMouse - ? Common.Appearance.m3colors.surfaceVariant - : "transparent" + // Segment component - lualine style section + component Segment: Rectangle { + id: segment + property string segmentText: "" + property string icon: "" + property color segmentColor: Common.Appearance.colors.bgHighlight + property color textColor: Common.Appearance.colors.fg + property bool showSeparator: true + property bool isActive: false + property bool clickable: false + signal clicked() - Behavior on color { - ColorAnimation { duration: 150 } - } - } + color: segmentColor + implicitWidth: segmentContent.implicitWidth + Common.Appearance.spacing.medium * 2 + implicitHeight: parent.height RowLayout { - id: buttonContent + id: segmentContent anchors.centerIn: parent - spacing: Common.Appearance.spacing.tiny + spacing: Common.Appearance.spacing.small Common.Icon { - visible: button.icon !== "" - name: button.icon - size: Common.Appearance.sizes.iconMedium - color: button.highlighted - ? Common.Appearance.m3colors.primary - : button.textColor + visible: segment.icon !== "" + name: segment.icon + size: Common.Appearance.sizes.iconSmall + color: segment.isActive ? Common.Appearance.colors.blue : segment.textColor } Text { - visible: button.buttonText !== "" - text: button.buttonText - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - color: button.highlighted - ? Common.Appearance.m3colors.primary - : button.textColor + visible: segment.segmentText !== "" + text: segment.segmentText + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: segment.isActive + color: segment.isActive ? Common.Appearance.colors.blue : segment.textColor } } + + MouseArea { + anchors.fill: parent + enabled: segment.clickable + cursorShape: segment.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor + hoverEnabled: segment.clickable + onClicked: segment.clicked() + + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.05) : "transparent" + } + } + + // Right separator + Text { + visible: segment.showSeparator + anchors.right: parent.right + anchors.rightMargin: -width / 2 + anchors.verticalCenter: parent.verticalCenter + text: Common.Appearance.separators.right + font.family: Common.Appearance.fonts.mono + font.pixelSize: parent.height + color: segment.segmentColor + z: 1 + } } - // Bar indicator (icon only, no interaction) - component BarIndicator: Item { + // Icon-only segment for tray items + component IconSegment: Rectangle { + id: iconSeg property string icon: "" - property string tooltip: "" - property color iconColor: Common.Appearance.m3colors.onSurfaceVariant + property color iconColor: Common.Appearance.colors.fgDark + property color segmentColor: Common.Appearance.colors.bgHighlight + property bool clickable: false + property bool showBadge: false + property color badgeColor: Common.Appearance.colors.error + signal clicked() - Layout.preferredHeight: 28 - Layout.preferredWidth: 28 + color: segmentColor + implicitWidth: Common.Appearance.sizes.barHeight + implicitHeight: parent.height Common.Icon { anchors.centerIn: parent - name: parent.icon - size: Common.Appearance.sizes.iconMedium - color: parent.iconColor + name: iconSeg.icon + size: Common.Appearance.sizes.iconSmall + color: iconSeg.iconColor } - } - // Bar background - Rectangle { - anchors.fill: parent - color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, - Common.Appearance.panelOpacity - ) + // Badge indicator + Rectangle { + visible: iconSeg.showBadge + width: 6 + height: 6 + radius: 3 + color: iconSeg.badgeColor + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 6 + anchors.rightMargin: 8 + } + + MouseArea { + anchors.fill: parent + enabled: iconSeg.clickable + cursorShape: iconSeg.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor + hoverEnabled: iconSeg.clickable + onClicked: iconSeg.clicked() + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } } - // Helper properties for screen position (reactive to screen changes) + // Helper properties for screen position property bool isLeftmost: { - // Single monitor case if (Quickshell.screens.length === 1) return true - // Multi-monitor: check against leftmost screen return targetScreen === Root.GlobalStates.leftmostScreen } property bool isRightmost: { - // Single monitor case if (Quickshell.screens.length === 1) return true - // Multi-monitor: check against rightmost screen return targetScreen === Root.GlobalStates.rightmostScreen } + // Bar background + Rectangle { + anchors.fill: parent + color: Qt.rgba( + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, + Common.Appearance.panelOpacity + ) + + // Bottom border line + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + color: Common.Appearance.colors.border + } + } + // Bar content RowLayout { anchors.fill: parent - anchors.leftMargin: Common.Appearance.spacing.medium - anchors.rightMargin: Common.Appearance.spacing.medium - spacing: Common.Appearance.spacing.small + spacing: 0 + + // ═══════════════════════════════════════════════════════════════ + // LEFT SECTION - Mode indicator + navigation + // ═══════════════════════════════════════════════════════════════ + + // Mode indicator (vim-style) + Rectangle { + visible: root.isLeftmost + color: root.modeColor + implicitWidth: modeText.implicitWidth + Common.Appearance.spacing.large * 2 + implicitHeight: parent.height + + Text { + id: modeText + anchors.centerIn: parent + text: root.currentMode + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.bg + } - // Left section - Launcher button (only on leftmost screen) - BarButton { + // Powerline separator + Text { + anchors.left: parent.right + anchors.leftMargin: -1 + anchors.verticalCenter: parent.verticalCenter + text: Common.Appearance.separators.left + font.family: Common.Appearance.fonts.mono + font.pixelSize: parent.height + color: root.modeColor + z: 1 + } + } + + // Apps button + IconSegment { visible: root.isLeftmost icon: Common.Icons.icons.apps - tooltip: "Applications" + segmentColor: Common.Appearance.colors.bgHighlight + iconColor: Root.GlobalStates.sidebarLeftView === "apps" && Root.GlobalStates.sidebarLeftOpen + ? Common.Appearance.colors.blue + : Common.Appearance.colors.fgDark + clickable: true onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen, "apps") } - // Updates button (only on leftmost screen, shows indicator when attention needed) + // Updates button MouseArea { id: updatesButton visible: root.isLeftmost - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 + implicitWidth: Common.Appearance.sizes.barHeight + implicitHeight: parent.height hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen, "updates") @@ -157,13 +263,11 @@ PanelWindow { Rectangle { anchors.fill: parent - radius: Common.Appearance.rounding.small - color: updatesButton.containsMouse - ? Common.Appearance.m3colors.surfaceVariant - : "transparent" + color: Common.Appearance.colors.bgHighlight - Behavior on color { - ColorAnimation { duration: 150 } + Rectangle { + anchors.fill: parent + color: updatesButton.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" } } @@ -174,10 +278,10 @@ PanelWindow { : (updatesButton.needsAttention ? Common.Icons.icons.download : Common.Icons.icons.checkCircle) - size: Common.Appearance.sizes.iconMedium + size: Common.Appearance.sizes.iconSmall color: updatesButton.needsAttention - ? Common.Appearance.m3colors.primary - : Common.Appearance.m3colors.onSurfaceVariant + ? Common.Appearance.colors.green + : Common.Appearance.colors.fgDark RotationAnimation on rotation { running: updatesButton.isRunning @@ -189,15 +293,36 @@ PanelWindow { } } - // Spacer + // Separator after left section + Rectangle { + visible: root.isLeftmost + width: 1 + height: parent.height + color: Common.Appearance.colors.border + } + + // ═══════════════════════════════════════════════════════════════ + // CENTER SECTION - Spacer (could show workspace info later) + // ═══════════════════════════════════════════════════════════════ Item { Layout.fillWidth: true } - // Right section - System indicators (only on rightmost screen) + // ═══════════════════════════════════════════════════════════════ + // RIGHT SECTION - System indicators + // ═══════════════════════════════════════════════════════════════ + + // Separator before right section + Rectangle { + visible: root.isRightmost + width: 1 + height: parent.height + color: Common.Appearance.colors.border + } + + // System tray RowLayout { visible: root.isRightmost - spacing: 2 + spacing: 0 - // System tray icons Repeater { model: SystemTray.items @@ -205,84 +330,72 @@ PanelWindow { id: trayItemArea required property var modelData - Layout.preferredHeight: 28 - Layout.preferredWidth: 28 + implicitWidth: Common.Appearance.sizes.barHeight + implicitHeight: Common.Appearance.sizes.barHeight hoverEnabled: true cursorShape: Qt.PointingHandCursor - // Check if icon has unsupported custom path (e.g. "icon_name?path=/some/path") property bool hasCustomPath: modelData.icon && modelData.icon.includes("?path=") - // Resolve icon source - handle paths, icon names, skip unsupported custom paths property string iconSource: { const icon = modelData.icon if (!icon || icon === "") return "" - // Skip icons with custom paths - Quickshell doesn't support them if (icon.includes("?path=")) return "" - // Already a full path or URL if (icon.startsWith("/")) return "file://" + icon if (icon.startsWith("file://") || icon.startsWith("image://")) return icon - // Icon name - try Qt icon provider return "image://icon/" + icon } - // Datacube fallback lookup using app title property string datacubeIcon: Services.IconResolver.getIcon(modelData.title) Rectangle { anchors.fill: parent - radius: Common.Appearance.rounding.small - color: trayItemArea.containsMouse - ? Common.Appearance.m3colors.surfaceVariant - : "transparent" + color: Common.Appearance.colors.bgHighlight - Behavior on color { - ColorAnimation { duration: 150 } + Rectangle { + anchors.fill: parent + color: trayItemArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" } } - // Track if primary icon failed (Error, Null status, or has unsupported custom path) property bool primaryFailed: trayItemArea.hasCustomPath || primaryTrayIcon.status === Image.Error || primaryTrayIcon.status === Image.Null || trayItemArea.iconSource === "" - // Primary icon from tray item Image { id: primaryTrayIcon anchors.centerIn: parent - width: Common.Appearance.sizes.iconMedium - height: Common.Appearance.sizes.iconMedium - sourceSize: Qt.size(Common.Appearance.sizes.iconMedium, Common.Appearance.sizes.iconMedium) + width: Common.Appearance.sizes.iconSmall + height: Common.Appearance.sizes.iconSmall + sourceSize: Qt.size(Common.Appearance.sizes.iconSmall, Common.Appearance.sizes.iconSmall) source: trayItemArea.iconSource smooth: true visible: status === Image.Ready } - // Datacube fallback icon Image { id: fallbackTrayIcon anchors.centerIn: parent - width: Common.Appearance.sizes.iconMedium - height: Common.Appearance.sizes.iconMedium - sourceSize: Qt.size(Common.Appearance.sizes.iconMedium, Common.Appearance.sizes.iconMedium) + width: Common.Appearance.sizes.iconSmall + height: Common.Appearance.sizes.iconSmall + sourceSize: Qt.size(Common.Appearance.sizes.iconSmall, Common.Appearance.sizes.iconSmall) source: trayItemArea.primaryFailed ? trayItemArea.datacubeIcon : "" smooth: true visible: trayItemArea.primaryFailed && status === Image.Ready } - // Last resort: letter icon Rectangle { anchors.centerIn: parent - width: Common.Appearance.sizes.iconMedium - height: Common.Appearance.sizes.iconMedium - radius: Common.Appearance.rounding.small - color: Common.Appearance.m3colors.primaryContainer + width: Common.Appearance.sizes.iconSmall + height: Common.Appearance.sizes.iconSmall + radius: Common.Appearance.rounding.tiny + color: Common.Appearance.colors.bgVisual visible: trayItemArea.primaryFailed && fallbackTrayIcon.status !== Image.Ready Text { anchors.centerIn: parent text: trayItemArea.modelData.title ? trayItemArea.modelData.title.charAt(0).toUpperCase() : "?" - font.pixelSize: 10 + font.pixelSize: 9 font.bold: true - color: Common.Appearance.m3colors.onPrimaryContainer + color: Common.Appearance.colors.fg } } @@ -290,16 +403,13 @@ PanelWindow { onClicked: (mouse) => { if (mouse.button === Qt.RightButton || (trayItemArea.modelData.onlyMenu && trayItemArea.modelData.hasMenu)) { - // Right click or menu-only item: show menu if (trayItemArea.modelData.hasMenu) { - // Map coordinates to window const pos = trayItemArea.mapToItem(null, 0, trayItemArea.height) trayItemArea.modelData.display(root, pos.x, pos.y) } } else if (mouse.button === Qt.MiddleButton) { trayItemArea.modelData.secondaryActivate() } else { - // Left click: activate trayItemArea.modelData.activate() } } @@ -309,211 +419,304 @@ PanelWindow { } } } + } - // Camera Privacy indicator - BarButton { - visible: Services.Privacy.cameraInUse - icon: Common.Icons.icons.camera - textColor: Common.Appearance.m3colors.error - tooltip: "Camera in use" - } + // Separator + Rectangle { + visible: root.isRightmost && SystemTray.items.length > 0 + width: 1 + height: parent.height + color: Common.Appearance.colors.border + } - // Audio (mic + output combined) - MouseArea { - id: audioButton - Layout.preferredHeight: 28 - Layout.preferredWidth: 56 // Two 28px icons - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "audio") + // Camera Privacy indicator + IconSegment { + visible: root.isRightmost && Services.Privacy.cameraInUse + icon: Common.Icons.icons.camera + iconColor: Common.Appearance.colors.error + segmentColor: Common.Appearance.colors.bgHighlight + } + + // Audio segment (mic + speaker) + MouseArea { + visible: root.isRightmost + implicitWidth: audioContent.implicitWidth + Common.Appearance.spacing.medium * 2 + implicitHeight: parent.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "audio") + + Rectangle { + anchors.fill: parent + color: Common.Appearance.colors.bgHighlight Rectangle { anchors.fill: parent - radius: Common.Appearance.rounding.small - color: audioButton.containsMouse - ? Common.Appearance.m3colors.surfaceVariant - : "transparent" + color: parent.parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } - Behavior on color { - ColorAnimation { duration: 150 } - } + RowLayout { + id: audioContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Common.Icon { + name: Services.Audio.micMuted + ? Common.Icons.icons.micOff + : Common.Icons.icons.mic + size: Common.Appearance.sizes.iconSmall + color: Services.Privacy.micInUse + ? Common.Appearance.colors.error + : (Services.Audio.micMuted ? Common.Appearance.colors.comment : Common.Appearance.colors.fgDark) } - RowLayout { - id: audioButtonContent - anchors.fill: parent - spacing: 0 + Common.Icon { + name: Services.Audio.muted + ? Common.Icons.icons.volumeOff + : Common.Icons.volumeIcon(Services.Audio.volume * 100, false) + size: Common.Appearance.sizes.iconSmall + color: Services.Audio.muted ? Common.Appearance.colors.comment : Common.Appearance.colors.fgDark + } + } + } - Item { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 + // Bluetooth + IconSegment { + visible: root.isRightmost && Services.BluetoothStatus.available + icon: Services.BluetoothStatus.powered + ? (Services.BluetoothStatus.connected + ? Common.Icons.icons.bluetoothConnected + : Common.Icons.icons.bluetooth) + : Common.Icons.icons.bluetoothOff + iconColor: Services.BluetoothStatus.connected + ? Common.Appearance.colors.blue + : (Services.BluetoothStatus.powered ? Common.Appearance.colors.fgDark : Common.Appearance.colors.comment) + segmentColor: Common.Appearance.colors.bgHighlight + clickable: true + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "bluetooth") + } - Common.Icon { - anchors.centerIn: parent - name: Services.Audio.micMuted - ? Common.Icons.icons.micOff - : Common.Icons.icons.mic - size: Common.Appearance.sizes.iconMedium - color: Services.Privacy.micInUse - ? Common.Appearance.m3colors.error - : Common.Appearance.m3colors.onSurfaceVariant - } - } + // Network + IconSegment { + visible: root.isRightmost && Common.Config.showNetwork + icon: { + if (!Services.Network.connected) { + return Services.Network.wifiAvailable ? Common.Icons.icons.wifiOff : Common.Icons.icons.ethernetOff + } + if (Services.Network.type === "wifi") { + return Common.Icons.wifiIcon(Services.Network.strength, true) + } + return Common.Icons.icons.ethernet + } + iconColor: Services.Network.connected ? Common.Appearance.colors.fgDark : Common.Appearance.colors.comment + segmentColor: Common.Appearance.colors.bgHighlight + clickable: true + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "network") + } - Item { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 + // Notifications + IconSegment { + visible: root.isRightmost + icon: Root.GlobalStates.doNotDisturb + ? Common.Icons.icons.doNotDisturb + : Common.Icons.icons.notification + iconColor: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb + ? Common.Appearance.colors.orange + : Common.Appearance.colors.fgDark + segmentColor: Common.Appearance.colors.bgHighlight + clickable: true + showBadge: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb + badgeColor: Common.Appearance.colors.orange + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "notifications") + } - Common.Icon { - anchors.centerIn: parent - name: Services.Audio.muted - ? Common.Icons.icons.volumeOff - : Common.Icons.volumeIcon(Services.Audio.volume * 100, false) - size: Common.Appearance.sizes.iconMedium - color: Common.Appearance.m3colors.onSurfaceVariant - } - } + // Separator before clock + Rectangle { + visible: root.isRightmost + width: 1 + height: parent.height + color: Common.Appearance.colors.border + } + + // Clock segment (prominent) + MouseArea { + visible: root.isRightmost + implicitWidth: clockContent.implicitWidth + Common.Appearance.spacing.large * 2 + implicitHeight: parent.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "calendar") + + Rectangle { + anchors.fill: parent + color: Common.Appearance.colors.bgHighlight + + Rectangle { + anchors.fill: parent + color: parent.parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" } } - // Bluetooth - BarButton { - visible: Services.BluetoothStatus.available - icon: Services.BluetoothStatus.powered - ? (Services.BluetoothStatus.connected - ? Common.Icons.icons.bluetoothConnected - : Common.Icons.icons.bluetooth) - : Common.Icons.icons.bluetoothOff - tooltip: Services.BluetoothStatus.powered - ? (Services.BluetoothStatus.connected - ? "Bluetooth: " + Services.BluetoothStatus.connectedDeviceName - : "Bluetooth: On") - : "Bluetooth: Off" - textColor: Services.BluetoothStatus.connected - ? Common.Appearance.m3colors.primary - : Common.Appearance.m3colors.onSurfaceVariant - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "bluetooth") + RowLayout { + id: clockContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Text { + text: Services.DateTime.timeString + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.fg + } + + Text { + text: Common.Appearance.separators.pipe + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.comment + } + + Text { + text: Services.DateTime.shortDateString + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fgDark + } } - // Network - BarButton { - visible: Common.Config.showNetwork - icon: { - if (!Services.Network.connected) { - return Services.Network.wifiAvailable ? Common.Icons.icons.wifiOff : Common.Icons.icons.ethernetOff - } - if (Services.Network.type === "wifi") { - return Common.Icons.wifiIcon(Services.Network.strength, true) + onContainsMouseChanged: { + if (containsMouse) { + Root.GlobalStates.osdType = "tooltip" + Root.GlobalStates.osdTooltipText = Services.DateTime.fullDateTimeString + Root.GlobalStates.osdVisible = true + } else { + if (Root.GlobalStates.osdType === "tooltip") { + Root.GlobalStates.osdVisible = false } - return Common.Icons.icons.ethernet } - tooltip: Services.Network.connected - ? Services.Network.name - : "Disconnected" - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "network") } - // Notifications bell - BarButton { - icon: Root.GlobalStates.doNotDisturb - ? Common.Icons.icons.doNotDisturb - : Common.Icons.icons.notification - tooltip: Root.GlobalStates.doNotDisturb - ? "Do Not Disturb" - : (Root.GlobalStates.unreadNotificationCount > 0 - ? Root.GlobalStates.unreadNotificationCount + " notifications" - : "Notifications") - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "notifications") - textColor: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb - ? Common.Appearance.m3colors.orange - : Common.Appearance.m3colors.onSurfaceVariant + Timer { + interval: 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: Services.DateTime.update() } + } - // Calendar / Date-Time - BarButton { - id: clockButton - icon: Common.Icons.icons.calendar - buttonText: Services.DateTime.shortDateString + " " + Services.DateTime.timeString - tooltip: Services.DateTime.fullDateTimeString - - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "calendar") - - // Show tooltip OSD on hover - onContainsMouseChanged: { - if (containsMouse) { - Root.GlobalStates.osdType = "tooltip" - Root.GlobalStates.osdTooltipText = Services.DateTime.fullDateTimeString - Root.GlobalStates.osdVisible = true - } else { - if (Root.GlobalStates.osdType === "tooltip") { - Root.GlobalStates.osdVisible = false - } - } - } + // Weather segment (if enabled) + MouseArea { + visible: root.isRightmost && Common.Config.showWeather + implicitWidth: weatherContent.implicitWidth + Common.Appearance.spacing.medium * 2 + implicitHeight: parent.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "weather") - Timer { - interval: 1000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: Services.DateTime.update() + Rectangle { + anchors.fill: parent + color: Common.Appearance.colors.bgHighlight + + Rectangle { + anchors.fill: parent + color: parent.parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" } } - // Weather (if enabled) - BarButton { - visible: Common.Config.showWeather - icon: Services.Weather.ready - ? Common.Icons.weatherIcon(Services.Weather.condition, Services.Weather.isNight) - : Common.Icons.icons.cloudy - buttonText: Services.Weather.ready ? Services.Weather.temperature : "--°" - tooltip: Services.Weather.ready ? Services.Weather.description : "Loading weather..." - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "weather") + RowLayout { + id: weatherContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Common.Icon { + name: Services.Weather.ready + ? Common.Icons.weatherIcon(Services.Weather.condition, Services.Weather.isNight) + : Common.Icons.icons.cloudy + size: Common.Appearance.sizes.iconSmall + color: Common.Appearance.colors.cyan + } + + Text { + text: Services.Weather.ready ? Services.Weather.temperature : "--°" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fgDark + } } + } - // Power button (shows battery on laptops, power icon on desktops) - BarButton { - icon: { - if (Services.Battery.present) { - // Laptop with battery - if (Services.Battery.pluggedIn && Services.Battery.percent >= 95) { - // Fully charged and plugged in - return Common.Icons.icons.plug - } else if (Services.Battery.charging) { - return Common.Icons.icons.batteryCharging - } else { - return Common.Icons.batteryIcon(Services.Battery.percent, false) - } + // Separator before power + Rectangle { + visible: root.isRightmost + width: 1 + height: parent.height + color: Common.Appearance.colors.border + } + + // Power/Battery segment (rightmost, colored) + MouseArea { + visible: root.isRightmost + implicitWidth: powerContent.implicitWidth + Common.Appearance.spacing.medium * 2 + implicitHeight: parent.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "power") + + property color powerColor: { + if (Services.Battery.present) { + if (Services.Battery.percent <= 20 && !Services.Battery.charging) { + return Common.Appearance.colors.error } - // Desktop - no battery - return Common.Icons.icons.power - } - buttonText: Services.Battery.present ? Services.Battery.percent + "%" : "" - tooltip: { - if (Services.Battery.present) { - if (Services.Battery.pluggedIn && Services.Battery.percent >= 95) { - return "Fully charged" - } else if (Services.Battery.charging) { - return "Charging: " + Services.Battery.percent + "%" - } else { - const timeStr = Services.Battery.timeRemainingString() - return "Battery: " + Services.Battery.percent + "%" + (timeStr ? " (" + timeStr + " remaining)" : "") - } + if (Services.Battery.pluggedIn) { + return Common.Appearance.colors.green } - return "Power options" } - textColor: { - if (Services.Battery.present) { - if (Services.Battery.percent <= 20 && !Services.Battery.charging) { - return Common.Appearance.m3colors.error - } - if (Services.Battery.pluggedIn) { - return Common.Appearance.m3colors.primary + return Common.Appearance.colors.magenta + } + + Rectangle { + anchors.fill: parent + color: parent.powerColor + + Rectangle { + anchors.fill: parent + color: parent.parent.containsMouse ? Qt.rgba(0, 0, 0, 0.1) : "transparent" + } + } + + RowLayout { + id: powerContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Common.Icon { + name: { + if (Services.Battery.present) { + if (Services.Battery.pluggedIn && Services.Battery.percent >= 95) { + return Common.Icons.icons.plug + } else if (Services.Battery.charging) { + return Common.Icons.icons.batteryCharging + } else { + return Common.Icons.batteryIcon(Services.Battery.percent, false) + } } + return Common.Icons.icons.power } - return Common.Appearance.m3colors.onSurfaceVariant + size: Common.Appearance.sizes.iconSmall + color: Common.Appearance.colors.bg + } + + Text { + visible: Services.Battery.present + text: Services.Battery.percent + "%" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.bg } - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "power") } } } diff --git a/dot_files/quickshell/modules/common/Appearance.qml b/dot_files/quickshell/modules/common/Appearance.qml index aea7848..7d98d74 100644 --- a/dot_files/quickshell/modules/common/Appearance.qml +++ b/dot_files/quickshell/modules/common/Appearance.qml @@ -2,160 +2,135 @@ pragma Singleton import QtQuick -// Material Design 3 theming system with Tokyonight defaults +// Neovim/TUI-inspired theming system with Tokyo Night colors QtObject { id: root // Dark mode toggle property bool darkMode: true - // Tokyonight color palette mapped to Material Design 3 roles - readonly property var m3colors: darkMode ? darkPalette : lightPalette + // Tokyo Night color palette (direct colors, no MD3 abstraction) + readonly property var colors: darkMode ? darkPalette : lightPalette readonly property var darkPalette: ({ - // Primary colors (Tokyonight blue) - primary: "#7aa2f7", - onPrimary: "#1a1b26", - primaryContainer: "#3d59a1", - onPrimaryContainer: "#c0caf5", - - // Secondary colors (Tokyonight green) - secondary: "#9ece6a", - onSecondary: "#1a1b26", - secondaryContainer: "#4a5e3a", - onSecondaryContainer: "#c0caf5", - - // Tertiary colors (Tokyonight purple) - tertiary: "#bb9af7", - onTertiary: "#1a1b26", - tertiaryContainer: "#6a4c93", - onTertiaryContainer: "#c0caf5", - - // Error colors (Tokyonight red) - error: "#f7768e", - onError: "#1a1b26", - errorContainer: "#8c4351", - onErrorContainer: "#ffc0c8", - - // Background and surface - background: "#1a1b26", - onBackground: "#c0caf5", - surface: "#1a1b26", - onSurface: "#c0caf5", - surfaceVariant: "#24283b", - onSurfaceVariant: "#a9b1d6", - - // Outline - outline: "#33467c", - outlineVariant: "#292e42", - - // Inverse - inverseSurface: "#c0caf5", - inverseOnSurface: "#1a1b26", - inversePrimary: "#3d59a1", - - // Additional Tokyonight colors + // Core background colors + bg: "#1a1b26", + bgDark: "#16161e", + bgHighlight: "#292e42", + bgVisual: "#33467c", + + // Foreground colors + fg: "#c0caf5", + fgDark: "#a9b1d6", + fgGutter: "#3b4261", + + // Border colors + border: "#414868", + borderHighlight: "#7aa2f7", + + // Accent colors (matching Neovim Tokyo Night) + blue: "#7aa2f7", cyan: "#7dcfff", + green: "#9ece6a", + magenta: "#bb9af7", orange: "#ff9e64", + red: "#f7768e", yellow: "#e0af68", - magenta: "#ff007c", teal: "#1abc9c", - comment: "#565f89" + + // Semantic colors + comment: "#565f89", + error: "#f7768e", + warning: "#e0af68", + info: "#7dcfff", + hint: "#1abc9c", + + // Git colors + gitAdd: "#449dab", + gitChange: "#6183bb", + gitDelete: "#914c54", + + // Mode colors (like Neovim mode indicators) + modeNormal: "#7aa2f7", + modeInsert: "#9ece6a", + modeVisual: "#bb9af7", + modeReplace: "#f7768e", + modeCommand: "#e0af68" }) readonly property var lightPalette: ({ - // Primary colors - primary: "#3d59a1", - onPrimary: "#ffffff", - primaryContainer: "#d0e4ff", - onPrimaryContainer: "#001d36", - - // Secondary colors - secondary: "#4a5e3a", - onSecondary: "#ffffff", - secondaryContainer: "#cce8b5", - onSecondaryContainer: "#0e2000", - - // Tertiary colors - tertiary: "#6a4c93", - onTertiary: "#ffffff", - tertiaryContainer: "#eddcff", - onTertiaryContainer: "#25005a", - - // Error colors - error: "#ba1a1a", - onError: "#ffffff", - errorContainer: "#ffdad6", - onErrorContainer: "#410002", - - // Background and surface - background: "#d5d6db", - onBackground: "#343338", - surface: "#f8f9ff", - onSurface: "#1a1b26", - surfaceVariant: "#e0e2ec", - onSurfaceVariant: "#44464f", - - // Outline - outline: "#74777f", - outlineVariant: "#c4c6d0", - - // Inverse - inverseSurface: "#2f3033", - inverseOnSurface: "#f1f0f4", - inversePrimary: "#9ecaff", - - // Additional colors - cyan: "#0891b2", - orange: "#c2410c", - yellow: "#a16207", - magenta: "#be185d", - teal: "#0d9488", - comment: "#6b7280" + // Core background colors + bg: "#d5d6db", + bgDark: "#cbccd1", + bgHighlight: "#b7b8bd", + bgVisual: "#99a0b5", + + // Foreground colors + fg: "#343b58", + fgDark: "#4c5372", + fgGutter: "#9699a3", + + // Border colors + border: "#9699a3", + borderHighlight: "#2e7de9", + + // Accent colors + blue: "#2e7de9", + cyan: "#007197", + green: "#587539", + magenta: "#9854f1", + orange: "#b15c00", + red: "#f52a65", + yellow: "#8c6c3e", + teal: "#118c74", + + // Semantic colors + comment: "#848cb5", + error: "#f52a65", + warning: "#8c6c3e", + info: "#007197", + hint: "#118c74", + + // Git colors + gitAdd: "#387068", + gitChange: "#506d9b", + gitDelete: "#c47981", + + // Mode colors + modeNormal: "#2e7de9", + modeInsert: "#587539", + modeVisual: "#9854f1", + modeReplace: "#f52a65", + modeCommand: "#8c6c3e" }) - // Surface layers (MD3 elevation) - function surfaceLayer(level: int): color { - const base = m3colors.surface - const tint = m3colors.primary - const alphas = [0, 0.05, 0.08, 0.11, 0.12, 0.14] - const alpha = alphas[Math.min(level, 5)] - return Qt.tint(base, Qt.rgba( - parseInt(tint.slice(1, 3), 16) / 255, - parseInt(tint.slice(3, 5), 16) / 255, - parseInt(tint.slice(5, 7), 16) / 255, - alpha - )) + // No surface layer tinting - just use flat colors with optional transparency + function surfaceColor(level: int): color { + switch (level) { + case 0: return colors.bg + case 1: return colors.bgDark + case 2: return colors.bgHighlight + case 3: return colors.bgVisual + default: return colors.bg + } } - // Animation durations + // Animation durations (snappier, more vim-like) readonly property var animation: ({ - // Expressive animations (spatial) - expressiveFast: 200, - expressive: 350, - expressiveSlow: 500, - - // Emphasized animations - emphasized: 500, - emphasizedAccel: 200, - emphasizedDecel: 400, - - // Standard animations - standard: 300, - standardAccel: 200, - standardDecel: 300 + instant: 50, + fast: 100, + normal: 150, + slow: 250 }) // Easing curves readonly property var easing: ({ - emphasized: Easing.BezierSpline, - emphasizedParams: [0.2, 0, 0, 1], standard: Easing.OutCubic, decelerate: Easing.OutQuart, accelerate: Easing.InQuart }) - // Typography + // Typography - monospace throughout for TUI aesthetic readonly property var fonts: ({ main: "JetBrains Mono", title: "JetBrains Mono", @@ -163,52 +138,100 @@ QtObject { }) readonly property var fontSize: ({ - smallest: 10, + tiny: 10, small: 12, - normal: 14, - large: 16, - title: 20, - headline: 24, - display: 32 + normal: 13, + large: 14, + title: 14, + headline: 16, + display: 20 }) - // Spacing and rounding + // Spacing (tighter for TUI aesthetic) readonly property var spacing: ({ - tiny: 4, - small: 8, - medium: 12, - large: 16, - xlarge: 24, - xxlarge: 32 - }) - - readonly property var rounding: ({ - none: 0, + tiny: 2, small: 4, medium: 8, large: 12, xlarge: 16, + xxlarge: 24 + }) + + // Rounding - minimal to none for TUI aesthetic + readonly property var rounding: ({ + none: 0, + tiny: 2, + small: 3, + medium: 4, + large: 6, full: 9999 }) + // Border widths + readonly property var borderWidth: ({ + none: 0, + thin: 1, + normal: 1, + thick: 2 + }) + // Component sizes readonly property var sizes: ({ - barHeight: 36, - sidebarWidth: 380, - osdWidth: 300, - osdHeight: 48, + barHeight: 28, + sidebarWidth: 400, + osdWidth: 280, + osdHeight: 40, launcherWidth: 600, launcherHeight: 500, notificationWidth: 380, - iconSmall: 16, - iconMedium: 20, - iconLarge: 24, - iconXLarge: 32 + iconTiny: 12, + iconSmall: 14, + iconMedium: 16, + iconLarge: 20, + iconXLarge: 24 }) // Transparency settings - property real panelOpacity: 0.85 - property real overlayOpacity: 0.95 + property real panelOpacity: 0.92 + property real overlayOpacity: 0.96 + + // Lualine-style separator characters + readonly property var separators: ({ + // Powerline style + left: "", + right: "", + leftThin: "", + rightThin: "", + // Simple style + pipe: "│", + slashForward: "/", + slashBack: "\\", + // Block style + block: "█", + blockHalf: "▌" + }) + + // Box drawing characters for TUI borders + readonly property var box: ({ + topLeft: "┌", + topRight: "┐", + bottomLeft: "└", + bottomRight: "┘", + horizontal: "─", + vertical: "│", + teeLeft: "├", + teeRight: "┤", + teeUp: "┴", + teeDown: "┬", + cross: "┼", + // Double line variants + dTopLeft: "╔", + dTopRight: "╗", + dBottomLeft: "╚", + dBottomRight: "╝", + dHorizontal: "═", + dVertical: "║" + }) // Helper function to get contrasting text color function contrastText(backgroundColor: color): color { @@ -216,6 +239,60 @@ QtObject { const g = backgroundColor.g const b = backgroundColor.b const luminance = 0.299 * r + 0.587 * g + 0.114 * b - return luminance > 0.5 ? m3colors.onBackground : m3colors.background + return luminance > 0.5 ? colors.bg : colors.fg + } + + // Helper for mode-based colors + function modeColor(mode: string): color { + switch (mode) { + case "normal": return colors.modeNormal + case "insert": return colors.modeInsert + case "visual": return colors.modeVisual + case "replace": return colors.modeReplace + case "command": return colors.modeCommand + default: return colors.modeNormal + } + } + + // Legacy compatibility aliases for m3colors (maps to new colors) + readonly property var m3colors: ({ + primary: colors.blue, + onPrimary: colors.bg, + primaryContainer: colors.bgVisual, + onPrimaryContainer: colors.fg, + secondary: colors.green, + onSecondary: colors.bg, + secondaryContainer: colors.bgHighlight, + onSecondaryContainer: colors.fg, + tertiary: colors.magenta, + onTertiary: colors.bg, + tertiaryContainer: colors.bgHighlight, + onTertiaryContainer: colors.fg, + error: colors.error, + onError: colors.bg, + errorContainer: colors.bgHighlight, + onErrorContainer: colors.error, + background: colors.bg, + onBackground: colors.fg, + surface: colors.bg, + onSurface: colors.fg, + surfaceVariant: colors.bgHighlight, + onSurfaceVariant: colors.fgDark, + outline: colors.border, + outlineVariant: colors.fgGutter, + inverseSurface: colors.fg, + inverseOnSurface: colors.bg, + inversePrimary: colors.bgVisual, + cyan: colors.cyan, + orange: colors.orange, + yellow: colors.yellow, + magenta: colors.magenta, + teal: colors.teal, + comment: colors.comment + }) + + // Helper for backward compatibility - surface layer without tinting + function surfaceLayer(level: int): color { + return surfaceColor(level) } } diff --git a/dot_files/quickshell/modules/common/TuiPanel.qml b/dot_files/quickshell/modules/common/TuiPanel.qml new file mode 100644 index 0000000..7af7666 --- /dev/null +++ b/dot_files/quickshell/modules/common/TuiPanel.qml @@ -0,0 +1,134 @@ +import QtQuick +import QtQuick.Layouts + +// Vim-style floating panel with TUI borders and title bar +Rectangle { + id: root + + // Panel title (displayed in title bar, vim-style) + property string title: "" + + // Keyboard hints shown at bottom + property var keyHints: [] // Array of {key: "Esc", action: "close"} + + // Content item + default property alias content: contentContainer.data + + // Colors + color: Qt.rgba( + Appearance.colors.bg.r, + Appearance.colors.bg.g, + Appearance.colors.bg.b, + Appearance.panelOpacity + ) + + // TUI-style border + border.width: Appearance.borderWidth.thin + border.color: Appearance.colors.border + + // Sharp corners for TUI look + radius: Appearance.rounding.tiny + + // Layout + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Title bar (vim-style) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: root.title !== "" ? 24 : 0 + visible: root.title !== "" + color: Appearance.colors.bgHighlight + + // Bottom border + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + color: Appearance.colors.border + } + + // Title text (centered, vim-style) + Text { + anchors.centerIn: parent + text: root.title + font.family: Appearance.fonts.mono + font.pixelSize: Appearance.fontSize.small + font.bold: true + color: Appearance.colors.fg + } + } + + // Content area + Item { + id: contentContainer + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Appearance.spacing.medium + } + + // Keyboard hints bar (like which-key) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: root.keyHints.length > 0 ? 22 : 0 + visible: root.keyHints.length > 0 + color: Appearance.colors.bgDark + + // Top border + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: 1 + color: Appearance.colors.border + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Appearance.spacing.medium + anchors.rightMargin: Appearance.spacing.medium + spacing: Appearance.spacing.large + + Repeater { + model: root.keyHints + + Row { + spacing: Appearance.spacing.tiny + + // Key badge + Rectangle { + width: keyText.implicitWidth + Appearance.spacing.small * 2 + height: 16 + radius: Appearance.rounding.tiny + color: Appearance.colors.bgVisual + anchors.verticalCenter: parent.verticalCenter + + Text { + id: keyText + anchors.centerIn: parent + text: modelData.key + font.family: Appearance.fonts.mono + font.pixelSize: Appearance.fontSize.tiny + font.bold: true + color: Appearance.colors.cyan + } + } + + // Action text + Text { + anchors.verticalCenter: parent.verticalCenter + text: modelData.action + font.family: Appearance.fonts.mono + font.pixelSize: Appearance.fontSize.tiny + color: Appearance.colors.fgDark + } + } + } + + Item { Layout.fillWidth: true } + } + } + } +} diff --git a/dot_files/quickshell/modules/common/qmldir b/dot_files/quickshell/modules/common/qmldir index 50f30ce..596e883 100644 --- a/dot_files/quickshell/modules/common/qmldir +++ b/dot_files/quickshell/modules/common/qmldir @@ -6,3 +6,4 @@ singleton Directories 1.0 Directories.qml singleton Icons 1.0 Icons.qml ClickCatcher 1.0 ClickCatcher.qml Icon 1.0 Icon.qml +TuiPanel 1.0 TuiPanel.qml diff --git a/dot_files/quickshell/modules/notifications/NotificationPopup.qml b/dot_files/quickshell/modules/notifications/NotificationPopup.qml index d6e330e..0c21c55 100644 --- a/dot_files/quickshell/modules/notifications/NotificationPopup.qml +++ b/dot_files/quickshell/modules/notifications/NotificationPopup.qml @@ -8,7 +8,7 @@ import "../common" as Common import "../../" as Root import "../../services" as Services -// Notification popup that appears in the corner +// Vim-style notification popup with TUI aesthetics PanelWindow { id: root @@ -29,16 +29,11 @@ PanelWindow { visible: notifications.length > 0 && !Root.GlobalStates.doNotDisturb - // Notification list property var notifications: [] property int maxVisible: 5 - // Background - Rectangle { - anchors.fill: parent - radius: Common.Appearance.rounding.large - color: "transparent" - } + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "notification" ColumnLayout { id: notificationColumn @@ -56,36 +51,38 @@ PanelWindow { } } - // "More notifications" indicator + // "More notifications" indicator (TUI style) Rectangle { visible: notifications.length > maxVisible Layout.fillWidth: true - Layout.preferredHeight: 32 - radius: Common.Appearance.rounding.medium + Layout.preferredHeight: 24 + radius: Common.Appearance.rounding.tiny color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, Common.Appearance.overlayOpacity ) + border.width: Common.Appearance.borderWidth.thin + border.color: Common.Appearance.colors.border Text { anchors.centerIn: parent - text: "+" + (notifications.length - maxVisible) + " more notifications" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.m3colors.onSurfaceVariant + text: "-- +" + (notifications.length - maxVisible) + " more --" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.comment } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.sidebarRightOpen = true + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "notifications") } } } - // Notification item component + // Notification item component (TUI style) component NotificationItem: Rectangle { id: notifItem @@ -94,16 +91,16 @@ PanelWindow { signal actionInvoked(string actionId) implicitHeight: contentLayout.implicitHeight + Common.Appearance.spacing.medium * 2 - radius: Common.Appearance.rounding.large + radius: Common.Appearance.rounding.tiny color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, Common.Appearance.overlayOpacity ) - border.width: 1 - border.color: Common.Appearance.m3colors.outlineVariant + border.width: Common.Appearance.borderWidth.thin + border.color: Common.Appearance.colors.border // Auto-dismiss timer Timer { @@ -113,7 +110,6 @@ PanelWindow { onTriggered: dismissed() } - // Click to invoke default action, hover to pause timer MouseArea { anchors.fill: parent hoverEnabled: true @@ -126,7 +122,6 @@ PanelWindow { } } onClicked: { - // If there's exactly one action, clicking the notification invokes it if (notification.actions && notification.actions.length === 1) { actionInvoked(notification.actions[0].identifier || "") } @@ -139,18 +134,17 @@ PanelWindow { anchors.margins: Common.Appearance.spacing.medium spacing: Common.Appearance.spacing.small - // Header row + // Header row (vim-style) RowLayout { Layout.fillWidth: true spacing: Common.Appearance.spacing.small - // App icon via IconResolver + // App icon Item { id: popupIconContainer - Layout.preferredWidth: 20 - Layout.preferredHeight: 20 + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 - // Get icon from IconResolver (triggers async lookup if not cached) property string resolvedIcon: notification.appName ? Services.IconResolver.getIcon(notification.appName) : "" property string fallbackIcon: notification.appIcon || "" property string iconSource: resolvedIcon || (fallbackIcon ? "image://icon/" + fallbackIcon : "") @@ -161,41 +155,50 @@ PanelWindow { id: appIcon anchors.fill: parent source: popupIconContainer.iconSource - sourceSize: Qt.size(20, 20) + sourceSize: Qt.size(16, 16) smooth: true } } - // App name + // App name (bracketed, vim-style) Text { - Layout.fillWidth: true - text: notification.appName || "Notification" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.m3colors.onSurfaceVariant - elide: Text.ElideRight + text: "[" + (notification.appName || "notify") + "]" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + font.bold: true + color: Common.Appearance.colors.cyan } + Item { Layout.fillWidth: true } + // Time Text { text: formatTime(notification.time) - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.smallest - color: Common.Appearance.m3colors.onSurfaceVariant + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.comment } // Close button MouseArea { - Layout.preferredWidth: 20 - Layout.preferredHeight: 20 + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 cursorShape: Qt.PointingHandCursor onClicked: dismissed() - Common.Icon { + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.tiny + color: parent.containsMouse ? Common.Appearance.colors.bgVisual : "transparent" + } + + Text { anchors.centerIn: parent - name: Common.Icons.icons.close - size: Common.Appearance.sizes.iconSmall - color: Common.Appearance.m3colors.onSurfaceVariant + text: "×" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: parent.containsMouse ? Common.Appearance.colors.error : Common.Appearance.colors.comment } } } @@ -208,10 +211,10 @@ PanelWindow { // Notification image Image { visible: notification.image && status === Image.Ready - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 source: notification.image || "" - sourceSize: Qt.size(48, 48) + sourceSize: Qt.size(40, 40) fillMode: Image.PreserveAspectCrop } @@ -223,10 +226,10 @@ PanelWindow { Text { Layout.fillWidth: true text: notification.summary || "" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - font.weight: Font.Medium - color: Common.Appearance.m3colors.onSurface + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.fg elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 2 @@ -237,9 +240,9 @@ PanelWindow { Layout.fillWidth: true visible: text !== "" text: notification.body || "" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.m3colors.onSurfaceVariant + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.fgDark elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 3 @@ -247,7 +250,7 @@ PanelWindow { } } - // Actions row - only show if multiple actions (single action is triggered by clicking notification) + // Actions row (TUI style buttons) RowLayout { visible: notification.actions && notification.actions.length > 1 Layout.fillWidth: true @@ -258,26 +261,32 @@ PanelWindow { delegate: MouseArea { required property var modelData - Layout.preferredHeight: 28 + Layout.preferredHeight: 22 Layout.preferredWidth: actionText.implicitWidth + Common.Appearance.spacing.medium * 2 cursorShape: Qt.PointingHandCursor onClicked: actionInvoked(modelData.identifier || "") Rectangle { anchors.fill: parent - radius: Common.Appearance.rounding.small + radius: Common.Appearance.rounding.tiny color: parent.containsMouse - ? Common.Appearance.m3colors.primaryContainer - : Common.Appearance.m3colors.surfaceVariant + ? Common.Appearance.colors.bgVisual + : Common.Appearance.colors.bgHighlight + border.width: Common.Appearance.borderWidth.thin + border.color: parent.containsMouse + ? Common.Appearance.colors.blue + : Common.Appearance.colors.border } Text { id: actionText anchors.centerIn: parent - text: modelData.text || "Action" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.m3colors.primary + text: "[" + (modelData.text || "Action") + "]" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: parent.containsMouse + ? Common.Appearance.colors.blue + : Common.Appearance.colors.fgDark } } } @@ -298,7 +307,6 @@ PanelWindow { } function addNotification(notification: var) { - // Add to front of list notifications = [notification, ...notifications] Root.GlobalStates.unreadNotificationCount++ } @@ -308,7 +316,6 @@ PanelWindow { } function invokeAction(notification: var, actionId: string) { - // Invoke action through the notification service Services.Notifications.invokeAction(notification.id, actionId) removeNotification(notification.id) } diff --git a/dot_files/quickshell/modules/osd/Osd.qml b/dot_files/quickshell/modules/osd/Osd.qml index 332adf1..410ba2b 100644 --- a/dot_files/quickshell/modules/osd/Osd.qml +++ b/dot_files/quickshell/modules/osd/Osd.qml @@ -6,6 +6,7 @@ import Quickshell.Wayland import "../common" as Common import "../../" as Root +// Vim-style minimal OSD with TUI aesthetics PanelWindow { id: root @@ -19,163 +20,174 @@ PanelWindow { margins.top: Common.Appearance.sizes.barHeight + Common.Appearance.spacing.medium margins.right: Common.Appearance.spacing.medium - // Tooltip mode uses auto-sizing, progress mode uses fixed width property bool isTooltip: Root.GlobalStates.osdType === "tooltip" - implicitWidth: isTooltip ? tooltipContent.implicitWidth + Common.Appearance.spacing.large * 2 : Common.Appearance.sizes.osdWidth - implicitHeight: isTooltip ? tooltipContent.implicitHeight + Common.Appearance.spacing.medium * 2 : Common.Appearance.sizes.osdHeight + Common.Appearance.spacing.large + implicitWidth: isTooltip + ? tooltipContent.implicitWidth + Common.Appearance.spacing.large * 2 + : Common.Appearance.sizes.osdWidth + implicitHeight: isTooltip + ? tooltipContent.implicitHeight + Common.Appearance.spacing.medium * 2 + : Common.Appearance.sizes.osdHeight + color: "transparent" - // Float on top of windows without reserving space exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "osd" visible: Root.GlobalStates.osdVisible - // OSD background + // OSD background - TUI style with border Rectangle { id: osdBackground anchors.fill: parent - // Animation for showing/hiding opacity: Root.GlobalStates.osdVisible ? 1 : 0 Behavior on opacity { NumberAnimation { - duration: Common.Appearance.animation.standard + duration: Common.Appearance.animation.fast easing.type: Easing.OutCubic } } - radius: Common.Appearance.rounding.large + + // Sharp corners, thin border - TUI style + radius: Common.Appearance.rounding.tiny color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, Common.Appearance.overlayOpacity ) - // Border - border.width: 1 - border.color: Common.Appearance.m3colors.outlineVariant + border.width: Common.Appearance.borderWidth.thin + border.color: Common.Appearance.colors.border } - // Tooltip content (for tooltip mode) + // Tooltip content Text { id: tooltipContent visible: root.isTooltip anchors.centerIn: parent text: Root.GlobalStates.osdTooltipText - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - color: Common.Appearance.m3colors.onSurface + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fg } - // Progress OSD content (for volume/brightness/mic) + // Progress OSD content (vim-style minimal) RowLayout { visible: !root.isTooltip anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium + anchors.leftMargin: Common.Appearance.spacing.medium + anchors.rightMargin: Common.Appearance.spacing.medium spacing: Common.Appearance.spacing.medium - // Icon - Common.Icon { - id: osdIcon - name: getIcon() - size: Common.Appearance.sizes.iconXLarge - color: getIconColor() + // Type indicator (like vim mode indicator) + Rectangle { + Layout.preferredWidth: typeLabel.implicitWidth + Common.Appearance.spacing.medium * 2 + Layout.preferredHeight: parent.height - Common.Appearance.spacing.small * 2 + color: getTypeColor() + radius: Common.Appearance.rounding.tiny - function getIcon() { + function getTypeColor() { + if (Root.GlobalStates.osdMuted) { + return Common.Appearance.colors.error + } switch (Root.GlobalStates.osdType) { - case "volume": - return Root.GlobalStates.osdMuted - ? Common.Icons.icons.volumeMute - : Common.Icons.volumeIcon(Root.GlobalStates.osdValue * 100, false) - case "brightness": - return Common.Icons.brightnessIcon(Root.GlobalStates.osdValue * 100) - case "mic": - return Root.GlobalStates.osdMuted - ? Common.Icons.icons.micOff - : Common.Icons.icons.mic - default: - return Common.Icons.icons.volumeHigh + case "volume": return Common.Appearance.colors.blue + case "brightness": return Common.Appearance.colors.yellow + case "mic": return Common.Appearance.colors.green + default: return Common.Appearance.colors.magenta } } - function getIconColor() { - if (Root.GlobalStates.osdMuted) { - return Common.Appearance.m3colors.error + Text { + id: typeLabel + anchors.centerIn: parent + text: getTypeText() + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + font.bold: true + color: Common.Appearance.colors.bg + + function getTypeText() { + if (Root.GlobalStates.osdMuted) { + return Root.GlobalStates.osdType === "mic" ? "MIC OFF" : "MUTED" + } + switch (Root.GlobalStates.osdType) { + case "volume": return "VOL" + case "brightness": return "BRI" + case "mic": return "MIC" + default: return "OSD" + } } - return Common.Appearance.m3colors.primary } } - // Progress bar and value - ColumnLayout { + // Progress bar (vim-style minimal) + Item { Layout.fillWidth: true - spacing: Common.Appearance.spacing.tiny + Layout.preferredHeight: 4 - // Progress bar + // Background track Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 6 - radius: 3 - color: Common.Appearance.m3colors.surfaceVariant - - Rectangle { - width: parent.width * Math.min(Root.GlobalStates.osdValue, 1.0) - height: parent.height - radius: parent.radius - color: Root.GlobalStates.osdMuted - ? Common.Appearance.m3colors.error - : Common.Appearance.m3colors.primary - - Behavior on width { - NumberAnimation { - duration: 100 - easing.type: Easing.OutCubic - } - } - } + anchors.fill: parent + radius: 2 + color: Common.Appearance.colors.bgVisual + } - // Overshoot indicator (for volume > 100%) - Rectangle { - visible: Root.GlobalStates.osdValue > 1.0 && Root.GlobalStates.osdType === "volume" - x: parent.width - width: Math.min((Root.GlobalStates.osdValue - 1.0) * parent.width, parent.width * 0.5) - height: parent.height - radius: parent.radius - color: Common.Appearance.m3colors.error - opacity: 0.7 - - Behavior on width { - NumberAnimation { - duration: 100 - easing.type: Easing.OutCubic - } + // Progress fill + Rectangle { + width: parent.width * Math.min(Root.GlobalStates.osdValue, 1.0) + height: parent.height + radius: 2 + color: Root.GlobalStates.osdMuted + ? Common.Appearance.colors.error + : Common.Appearance.colors.fg + + Behavior on width { + NumberAnimation { + duration: Common.Appearance.animation.fast + easing.type: Easing.OutCubic } } } - // Value text (if enabled) - Text { - visible: Common.Config.osdShowValue - Layout.alignment: Qt.AlignRight - text: getValueText() - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.m3colors.onSurfaceVariant - - function getValueText() { - if (Root.GlobalStates.osdMuted) { - return Root.GlobalStates.osdType === "mic" ? "Muted" : "Muted" + // Overshoot indicator + Rectangle { + visible: Root.GlobalStates.osdValue > 1.0 && Root.GlobalStates.osdType === "volume" + x: parent.width + width: Math.min((Root.GlobalStates.osdValue - 1.0) * parent.width, parent.width * 0.5) + height: parent.height + radius: 2 + color: Common.Appearance.colors.error + opacity: 0.8 + + Behavior on width { + NumberAnimation { + duration: Common.Appearance.animation.fast + easing.type: Easing.OutCubic } - return Math.round(Root.GlobalStates.osdValue * 100) + "%" } } } + + // Value text (monospace, right-aligned) + Text { + Layout.preferredWidth: 40 + horizontalAlignment: Text.AlignRight + text: Root.GlobalStates.osdMuted + ? "---" + : Math.round(Root.GlobalStates.osdValue * 100) + "%" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Root.GlobalStates.osdMuted + ? Common.Appearance.colors.error + : Common.Appearance.colors.fgDark + } } - // Mouse area to dismiss on click + // Click to dismiss MouseArea { anchors.fill: parent onClicked: Root.GlobalStates.osdVisible = false diff --git a/dot_files/quickshell/modules/sidebars/ApplicationView.qml b/dot_files/quickshell/modules/sidebars/ApplicationView.qml index 47463b1..c0ebca2 100644 --- a/dot_files/quickshell/modules/sidebars/ApplicationView.qml +++ b/dot_files/quickshell/modules/sidebars/ApplicationView.qml @@ -8,24 +8,21 @@ import "../common" as Common import "../../" as Root import "../../services" as Services -// Application launcher view for the left sidebar +// Vim-style application launcher with TUI aesthetics ColumnLayout { id: root - spacing: Common.Appearance.spacing.medium + spacing: 0 - // State for search results property var searchResults: [] property var allApps: [] property string currentQuery: "" property bool isSearching: false - // Track the query we're waiting for results from property string allAppsQueryId: "" property string searchQueryId: "" property int retryCount: 0 property int maxRetries: 5 - // Load all apps on startup Component.onCompleted: { loadAllApps() } @@ -34,183 +31,133 @@ ColumnLayout { allAppsQueryId = Services.Datacube.queryAll("", 500) } - // Search bar - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 44 - radius: Common.Appearance.rounding.large - color: Common.Appearance.m3colors.surfaceVariant - - RowLayout { - anchors.fill: parent - anchors.leftMargin: Common.Appearance.spacing.medium - anchors.rightMargin: Common.Appearance.spacing.medium - spacing: Common.Appearance.spacing.small - - Common.Icon { - name: Common.Icons.icons.search - size: Common.Appearance.sizes.iconMedium - color: Common.Appearance.m3colors.onSurfaceVariant - } - - TextInput { - id: searchInput - Layout.fillWidth: true - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - color: Common.Appearance.m3colors.onSurface - clip: true - - property string placeholderText: "Search applications..." - - Text { - anchors.fill: parent - verticalAlignment: Text.AlignVCenter - text: searchInput.placeholderText - font: searchInput.font - color: Common.Appearance.m3colors.onSurfaceVariant - visible: !searchInput.text && !searchInput.activeFocus - } - - onTextChanged: { - root.currentQuery = text - root.isSearching = text.trim() !== "" - if (root.isSearching) { - queryDebounceTimer.restart() - } else { - searchResults = [] - } - } - - Keys.onEscapePressed: { - if (text !== "") { - text = "" - } else { - Root.GlobalStates.sidebarLeftOpen = false - } - } - - Keys.onDownPressed: appListView.incrementCurrentIndex() - Keys.onUpPressed: appListView.decrementCurrentIndex() - Keys.onReturnPressed: { - if (appListView.currentIndex >= 0 && appListView.currentIndex < appListView.count) { - const apps = root.isSearching ? searchResults : allApps - launchApp(apps[appListView.currentIndex]) - } - } - } - - // Clear button - MouseArea { - visible: searchInput.text !== "" - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - cursorShape: Qt.PointingHandCursor - onClicked: searchInput.text = "" - - Common.Icon { - anchors.centerIn: parent - name: Common.Icons.icons.close - size: Common.Appearance.sizes.iconSmall - color: Common.Appearance.m3colors.onSurfaceVariant - } - } - } - } - - // App grid/list + // App list (vim-style) ListView { id: appListView Layout.fillWidth: true Layout.fillHeight: true clip: true - spacing: 2 + spacing: 0 model: root.isSearching ? searchResults : allApps + // Line numbers like vim + property int lineNumberWidth: 36 + delegate: MouseArea { id: appDelegate required property var modelData required property int index width: appListView.width - height: 48 + height: 28 hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: launchApp(modelData) + property bool isSelected: appListView.currentIndex === index + + // Selection highlight (vim-style visual line) Rectangle { anchors.fill: parent - radius: Common.Appearance.rounding.medium - color: appDelegate.containsMouse || appListView.currentIndex === appDelegate.index - ? Common.Appearance.m3colors.surfaceVariant - : "transparent" + color: appDelegate.isSelected + ? Common.Appearance.colors.bgVisual + : (appDelegate.containsMouse + ? Common.Appearance.colors.bgHighlight + : "transparent") } RowLayout { anchors.fill: parent - anchors.leftMargin: Common.Appearance.spacing.medium - anchors.rightMargin: Common.Appearance.spacing.medium - spacing: Common.Appearance.spacing.medium + spacing: 0 - // App icon with letter fallback + // Line number (vim-style gutter) + Rectangle { + Layout.preferredWidth: appListView.lineNumberWidth + Layout.fillHeight: true + color: Common.Appearance.colors.bgDark + + Text { + anchors.right: parent.right + anchors.rightMargin: Common.Appearance.spacing.small + anchors.verticalCenter: parent.verticalCenter + text: (index + 1).toString() + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: appDelegate.isSelected + ? Common.Appearance.colors.yellow + : Common.Appearance.colors.comment + } + } + + // Separator line + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + color: Common.Appearance.colors.fgGutter + } + + // App icon Item { - id: iconContainer - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + Layout.leftMargin: Common.Appearance.spacing.small property string iconSource: modelData.icon || "" Image { id: appIcon - anchors.fill: parent - source: iconContainer.iconSource - sourceSize: Qt.size(32, 32) + anchors.centerIn: parent + width: 18 + height: 18 + source: parent.iconSource + sourceSize: Qt.size(18, 18) smooth: true visible: status === Image.Ready } - // Fallback: Letter icon + // Fallback: colored letter Rectangle { - anchors.fill: parent + anchors.centerIn: parent + width: 18 + height: 18 visible: appIcon.status !== Image.Ready - radius: Common.Appearance.rounding.small - color: Common.Appearance.m3colors.primaryContainer + radius: Common.Appearance.rounding.tiny + color: Common.Appearance.colors.bgVisual Text { anchors.centerIn: parent text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - font.pixelSize: 16 + font.pixelSize: 10 font.bold: true - color: Common.Appearance.m3colors.onPrimaryContainer + color: Common.Appearance.colors.cyan } } } - // App info - ColumnLayout { + // App name + Text { Layout.fillWidth: true - spacing: 2 - - Text { - Layout.fillWidth: true - text: modelData.name || "Unknown" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - color: Common.Appearance.m3colors.onSurface - elide: Text.ElideRight - } + Layout.leftMargin: Common.Appearance.spacing.small + text: modelData.name || "Unknown" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: appDelegate.isSelected + ? Common.Appearance.colors.fg + : Common.Appearance.colors.fgDark + elide: Text.ElideRight + } - Text { - Layout.fillWidth: true - visible: text !== "" && text !== modelData.name - text: modelData.description || modelData.genericName || "" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.m3colors.onSurfaceVariant - elide: Text.ElideRight - } + // Category/description (dimmed) + Text { + Layout.rightMargin: Common.Appearance.spacing.medium + visible: modelData.genericName && modelData.genericName !== modelData.name + text: modelData.genericName || "" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.comment + elide: Text.ElideRight } } } @@ -219,26 +166,143 @@ ColumnLayout { Text { anchors.centerIn: parent visible: appListView.count === 0 - text: root.isSearching ? "No applications found" : "Loading applications..." - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - color: Common.Appearance.m3colors.onSurfaceVariant + text: root.isSearching ? "-- No matches --" : "-- Loading... --" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.comment } ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded + width: 8 + + contentItem: Rectangle { + implicitWidth: 6 + radius: 3 + color: Common.Appearance.colors.bgVisual + } + } + } + + // Separator line + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Common.Appearance.colors.border + } + + // Command line search (vim-style at bottom) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 28 + color: Common.Appearance.colors.bgDark + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.small + anchors.rightMargin: Common.Appearance.spacing.small + spacing: 0 + + // Command prompt indicator + Text { + text: root.isSearching ? "/" : ":" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.cyan + } + + // Search input + TextInput { + id: searchInput + Layout.fillWidth: true + Layout.leftMargin: Common.Appearance.spacing.tiny + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fg + clip: true + selectByMouse: true + selectionColor: Common.Appearance.colors.bgVisual + selectedTextColor: Common.Appearance.colors.fg + + property string placeholderText: "Type to search..." + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: searchInput.placeholderText + font: searchInput.font + color: Common.Appearance.colors.comment + visible: !searchInput.text && !searchInput.activeFocus + } + + onTextChanged: { + root.currentQuery = text + root.isSearching = text.trim() !== "" + if (root.isSearching) { + queryDebounceTimer.restart() + } else { + searchResults = [] + } + } + + Keys.onEscapePressed: { + if (text !== "") { + text = "" + } else { + Root.GlobalStates.sidebarLeftOpen = false + } + } + + Keys.onDownPressed: appListView.incrementCurrentIndex() + Keys.onUpPressed: appListView.decrementCurrentIndex() + Keys.onReturnPressed: { + if (appListView.currentIndex >= 0 && appListView.currentIndex < appListView.count) { + const apps = root.isSearching ? searchResults : allApps + launchApp(apps[appListView.currentIndex]) + } + } + + // Vim-style j/k navigation when not typing + Keys.onPressed: (event) => { + if (event.key === Qt.Key_J && !event.modifiers) { + appListView.incrementCurrentIndex() + event.accepted = true + } else if (event.key === Qt.Key_K && !event.modifiers) { + appListView.decrementCurrentIndex() + event.accepted = true + } + } + } + + // Results count (vim-style) + Text { + visible: appListView.count > 0 + text: "[" + (appListView.currentIndex + 1) + "/" + appListView.count + "]" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.comment + } + } + + // Cursor blink simulation + Rectangle { + visible: searchInput.activeFocus && searchInput.cursorVisible + x: searchInput.x + searchInput.cursorRectangle.x + Common.Appearance.spacing.small + 8 + y: (parent.height - height) / 2 + width: 2 + height: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fg } } - // Handle Datacube query results + // Datacube query handling Connections { target: Services.Datacube function onQueryCompleted(queryId, results) { if (queryId === root.allAppsQueryId) { - // Reset retry count on success root.retryCount = 0 - // Sort alphabetically by name results.sort((a, b) => { const nameA = (a.name || "").toLowerCase() const nameB = (b.name || "").toLowerCase() @@ -254,7 +318,6 @@ ColumnLayout { function onQueryFailed(queryId, error) { console.log("Datacube query failed:", queryId, error) - // If the allApps query failed, retry after a delay (service may have restarted) if (queryId === root.allAppsQueryId && root.retryCount < root.maxRetries) { root.retryCount++ console.log("Datacube: retrying allApps query (attempt", root.retryCount, "of", root.maxRetries, ")") @@ -263,17 +326,15 @@ ColumnLayout { } } - // Retry timer for failed queries (datacube service may have restarted) Timer { id: retryTimer - interval: 1000 * root.retryCount // Exponential backoff: 1s, 2s, 3s, etc. + interval: 1000 * root.retryCount repeat: false onTriggered: { root.loadAllApps() } } - // Debounce timer for search queries Timer { id: queryDebounceTimer interval: 150 @@ -284,7 +345,6 @@ ColumnLayout { root.searchQueryId = "" return } - // Cancel previous search if still running if (root.searchQueryId) { Services.Datacube.cancelQuery(root.searchQueryId) } @@ -297,52 +357,42 @@ ColumnLayout { const desktopId = metadata.desktop_id || app?.id || "" if (!desktopId) return - // Check if terminal app - handle boolean or string "true"/"false" const isTerminal = metadata.terminal === true || metadata.terminal === "true" const source = app?.source || "native" if (isTerminal) { - // Terminal apps: launch with ghostty appLaunchProcess.command = ["ghostty", "-e", desktopId] } else if (source === "flatpak") { - // Flatpak apps: use flatpak run appLaunchProcess.command = ["flatpak", "run", desktopId] } else { - // Native apps: use gtk4-launch with desktop_id appLaunchProcess.command = ["gtk4-launch", desktopId] } - // Launch detached so apps survive shell restart and run independently appLaunchProcess.startDetached() Root.GlobalStates.sidebarLeftOpen = false searchInput.text = "" } - // App launcher process - used with startDetached() for independent execution Process { id: appLaunchProcess command: ["true"] } - // Handle sidebar state changes Connections { target: Root.GlobalStates function onSidebarLeftOpenChanged() { if (Root.GlobalStates.sidebarLeftOpen) { - // Refresh apps if list is empty (datacube may have restarted) if (root.allApps.length === 0) { root.retryCount = 0 root.loadAllApps() } } else { - // Reset state when sidebar closes searchInput.text = "" searchResults = [] } } } - // Focus search input - called externally by Loader function focusSearch() { searchInput.forceActiveFocus() appListView.currentIndex = 0 diff --git a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml index b3e8ce5..a500ae3 100644 --- a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml +++ b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml @@ -21,51 +21,50 @@ PanelWindow { left: true } - margins.top: Common.Appearance.sizes.barHeight + margins.top: Common.Appearance.sizes.barHeight + Common.Appearance.spacing.small + margins.bottom: Common.Appearance.spacing.small + margins.left: Common.Appearance.spacing.small implicitWidth: Common.Appearance.sizes.sidebarWidth color: "transparent" visible: Root.GlobalStates.sidebarLeftOpen - // Focus app search when sidebar opens onVisibleChanged: { if (visible && appViewLoader.item) { appViewLoader.item.focusSearch() } } - // Request keyboard focus from compositor WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "sidebar" - // Background - Rectangle { + // TUI Panel container + Common.TuiPanel { anchors.fill: parent - color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, - Common.Appearance.panelOpacity - ) - } + title: Root.GlobalStates.sidebarLeftView === "apps" ? "[ Applications ]" : "[ System Updates ]" + keyHints: [ + { key: "Esc", action: "close" }, + { key: "/", action: "search" }, + { key: "j/k", action: "navigate" }, + { key: "Enter", action: "select" } + ] - // Application View (shown when sidebarLeftView === "apps") - Loader { - id: appViewLoader - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarLeftView === "apps" - source: "ApplicationView.qml" - onLoaded: item.focusSearch() - } + // Application View + Loader { + id: appViewLoader + anchors.fill: parent + active: Root.GlobalStates.sidebarLeftView === "apps" + source: "ApplicationView.qml" + onLoaded: item.focusSearch() + } - // Update View (shown when sidebarLeftView === "updates") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarLeftView === "updates" - source: "UpdateView.qml" + // Update View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarLeftView === "updates" + source: "UpdateView.qml" + } } } diff --git a/dot_files/quickshell/modules/sidebars/SidebarRight.qml b/dot_files/quickshell/modules/sidebars/SidebarRight.qml index b5c3605..8cdd676 100644 --- a/dot_files/quickshell/modules/sidebars/SidebarRight.qml +++ b/dot_files/quickshell/modules/sidebars/SidebarRight.qml @@ -21,90 +21,132 @@ PanelWindow { right: true } - margins.top: Common.Appearance.sizes.barHeight + margins.top: Common.Appearance.sizes.barHeight + Common.Appearance.spacing.small + margins.bottom: Common.Appearance.spacing.small + margins.right: Common.Appearance.spacing.small implicitWidth: Common.Appearance.sizes.sidebarWidth color: "transparent" visible: Root.GlobalStates.sidebarRightOpen - // Allow keyboard focus for text input in sidebars WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "sidebar" - // Background - Rectangle { - anchors.fill: parent - color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, - Common.Appearance.panelOpacity - ) - } - - // Network View (shown when sidebarRightView === "network") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "network" - source: "NetworkView.qml" - } - - // Bluetooth View (shown when sidebarRightView === "bluetooth") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "bluetooth" - source: "BluetoothView.qml" - } - - // Audio View (shown when sidebarRightView === "audio") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "audio" - source: "AudioView.qml" + // Get title based on current view + property string viewTitle: { + switch (Root.GlobalStates.sidebarRightView) { + case "network": return "[ Network ]" + case "bluetooth": return "[ Bluetooth ]" + case "audio": return "[ Audio ]" + case "calendar": return "[ Calendar ]" + case "notifications": return "[ Notifications ]" + case "power": return "[ Power ]" + case "weather": return "[ Weather ]" + default: return "[ Settings ]" + } } - // Calendar View (shown when sidebarRightView === "calendar") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "calendar" - source: "CalendarView.qml" - } - - // Notifications View (shown when sidebarRightView === "notifications") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "notifications" - source: "NotificationsView.qml" - } - - // Power View (shown when sidebarRightView === "power") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "power" - source: "PowerView.qml" - } - - // Weather View (shown when sidebarRightView === "weather") - Loader { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "weather" - source: "WeatherView.qml" + // Get keyboard hints based on current view + property var viewHints: { + switch (Root.GlobalStates.sidebarRightView) { + case "network": + return [ + { key: "Esc", action: "close" }, + { key: "j/k", action: "navigate" }, + { key: "Enter", action: "connect" } + ] + case "bluetooth": + return [ + { key: "Esc", action: "close" }, + { key: "j/k", action: "navigate" }, + { key: "Enter", action: "pair" } + ] + case "audio": + return [ + { key: "Esc", action: "close" }, + { key: "m", action: "mute" }, + { key: "+/-", action: "volume" } + ] + case "notifications": + return [ + { key: "Esc", action: "close" }, + { key: "d", action: "dismiss" }, + { key: "D", action: "clear all" } + ] + case "power": + return [ + { key: "Esc", action: "close" }, + { key: "s", action: "suspend" }, + { key: "r", action: "reboot" }, + { key: "p", action: "poweroff" } + ] + default: + return [{ key: "Esc", action: "close" }] + } } - // Default Content (shown when sidebarRightView === "default") - Loader { + // TUI Panel container + Common.TuiPanel { anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - active: Root.GlobalStates.sidebarRightView === "default" - source: "DefaultView.qml" + title: root.viewTitle + keyHints: root.viewHints + + // Network View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "network" + source: "NetworkView.qml" + } + + // Bluetooth View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "bluetooth" + source: "BluetoothView.qml" + } + + // Audio View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "audio" + source: "AudioView.qml" + } + + // Calendar View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "calendar" + source: "CalendarView.qml" + } + + // Notifications View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "notifications" + source: "NotificationsView.qml" + } + + // Power View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "power" + source: "PowerView.qml" + } + + // Weather View + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "weather" + source: "WeatherView.qml" + } + + // Default Content + Loader { + anchors.fill: parent + active: Root.GlobalStates.sidebarRightView === "default" + source: "DefaultView.qml" + } } } diff --git a/dot_files/quickshell/modules/switcher/AppSwitcher.qml b/dot_files/quickshell/modules/switcher/AppSwitcher.qml index 03efbd0..59ddb1f 100644 --- a/dot_files/quickshell/modules/switcher/AppSwitcher.qml +++ b/dot_files/quickshell/modules/switcher/AppSwitcher.qml @@ -6,14 +6,13 @@ import Quickshell.Wayland import "../common" as Common import "../../services" as Services -// App switcher overlay - centered on screen +// Vim-style app switcher overlay with TUI aesthetics PanelWindow { id: root required property var targetScreen screen: targetScreen - // Center on screen anchors { top: true bottom: true @@ -29,13 +28,11 @@ PanelWindow { WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.namespace: "appswitcher" - // Focus scope for keyboard handling FocusScope { id: focusRoot anchors.fill: parent focus: true - // Keyboard handling Keys.onPressed: (event) => { if (event.key === Qt.Key_Tab) { if (event.modifiers & Qt.ShiftModifier) { @@ -50,27 +47,32 @@ PanelWindow { } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { Services.Windows.selectWindow() event.accepted = true - } else if (event.key === Qt.Key_Left) { + } else if (event.key === Qt.Key_Left || event.key === Qt.Key_H) { Services.Windows.prevWindow() event.accepted = true - } else if (event.key === Qt.Key_Right) { + } else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { Services.Windows.nextWindow() event.accepted = true + } else if (event.key === Qt.Key_J) { + Services.Windows.nextWindow() + event.accepted = true + } else if (event.key === Qt.Key_K) { + Services.Windows.prevWindow() + event.accepted = true } } Keys.onReleased: (event) => { - // When Super (Meta) is released, select the current window if (event.key === Qt.Key_Super_L || event.key === Qt.Key_Super_R || event.key === Qt.Key_Meta) { Services.Windows.selectWindow() event.accepted = true } } - // Dark overlay background + // Dark overlay Rectangle { anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.5) + color: Qt.rgba(0, 0, 0, 0.6) MouseArea { anchors.fill: parent @@ -79,165 +81,271 @@ PanelWindow { } } - // Switcher panel - centered + // Switcher panel (TUI style - like a floating vim window) Rectangle { id: switcherPanel anchors.centerIn: parent - // Calculate width based on number of windows - readonly property int itemWidth: 120 - readonly property int itemSpacing: Common.Appearance.spacing.medium + readonly property int itemWidth: 100 + readonly property int itemSpacing: 2 readonly property int windowCount: Services.Windows.windows.length readonly property int contentWidth: windowCount > 0 ? (windowCount * itemWidth) + ((windowCount - 1) * itemSpacing) - : 200 // Minimum width when no windows + : 200 - width: Math.min(parent.width * 0.8, contentWidth + Common.Appearance.spacing.large * 2) - height: 160 - radius: Common.Appearance.rounding.large + width: Math.min(parent.width * 0.85, contentWidth + Common.Appearance.spacing.medium * 2 + 4) + height: 140 + radius: Common.Appearance.rounding.tiny color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, - 0.95 + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, + 0.96 ) + border.width: Common.Appearance.borderWidth.thin + border.color: Common.Appearance.colors.border - // Window list - ListView { - id: switcherRow + ColumnLayout { anchors.fill: parent - anchors.margins: Common.Appearance.spacing.medium - orientation: ListView.Horizontal - spacing: switcherPanel.itemSpacing - clip: true + spacing: 0 + + // Title bar (vim-style) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 22 + color: Common.Appearance.colors.bgHighlight - model: Services.Windows.windows - currentIndex: Services.Windows.currentIndex + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: Common.Appearance.colors.border + } - highlightFollowsCurrentItem: true - highlightMoveDuration: 150 + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.small + anchors.rightMargin: Common.Appearance.spacing.small - delegate: Item { - id: windowDelegate - required property var modelData - required property int index + Text { + text: "[ Switch Window ]" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + font.bold: true + color: Common.Appearance.colors.fg + } - width: switcherPanel.itemWidth - height: switcherRow.height + Item { Layout.fillWidth: true } - Rectangle { - anchors.fill: parent - radius: Common.Appearance.rounding.medium - color: switcherRow.currentIndex === windowDelegate.index - ? Common.Appearance.m3colors.primaryContainer - : (delegateMouse.containsMouse - ? Common.Appearance.m3colors.surfaceVariant - : "transparent") - - Behavior on color { - ColorAnimation { duration: 100 } + Text { + text: "[" + (Services.Windows.currentIndex + 1) + "/" + Services.Windows.windows.length + "]" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.comment } } + } - MouseArea { - id: delegateMouse - anchors.fill: parent - hoverEnabled: true - onClicked: { - Services.Windows.currentIndex = windowDelegate.index - Services.Windows.selectWindow() + // Window list + ListView { + id: switcherRow + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Common.Appearance.spacing.small + orientation: ListView.Horizontal + spacing: switcherPanel.itemSpacing + clip: true + + model: Services.Windows.windows + currentIndex: Services.Windows.currentIndex + + highlightFollowsCurrentItem: true + highlightMoveDuration: Common.Appearance.animation.fast + + delegate: Item { + id: windowDelegate + required property var modelData + required property int index + + width: switcherPanel.itemWidth + height: switcherRow.height + + // Selection highlight (vim visual style) + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.tiny + color: switcherRow.currentIndex === windowDelegate.index + ? Common.Appearance.colors.bgVisual + : (delegateMouse.containsMouse + ? Common.Appearance.colors.bgHighlight + : "transparent") + border.width: switcherRow.currentIndex === windowDelegate.index ? 1 : 0 + border.color: Common.Appearance.colors.blue } - } - ColumnLayout { - anchors.fill: parent - anchors.margins: Common.Appearance.spacing.small - spacing: Common.Appearance.spacing.tiny - - // App icon - Item { - Layout.fillWidth: true - Layout.preferredHeight: 48 - Layout.alignment: Qt.AlignHCenter - - // Get icon from IconResolver service - property string cachedIcon: modelData.class ? Services.IconResolver.getIcon(modelData.class) : "" - - // Primary: datacube cached icon - Image { - id: appIcon - anchors.centerIn: parent - width: 48 - height: 48 - source: parent.cachedIcon - sourceSize: Qt.size(48, 48) - smooth: true - visible: status === Image.Ready + MouseArea { + id: delegateMouse + anchors.fill: parent + hoverEnabled: true + onClicked: { + Services.Windows.currentIndex = windowDelegate.index + Services.Windows.selectWindow() } + } - // Fallback letter icon - Rectangle { - anchors.centerIn: parent - width: 48 - height: 48 - visible: appIcon.status !== Image.Ready - radius: Common.Appearance.rounding.medium - color: Common.Appearance.m3colors.secondaryContainer + ColumnLayout { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.tiny + spacing: Common.Appearance.spacing.tiny - Text { + // App icon + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + Layout.alignment: Qt.AlignHCenter + + property string cachedIcon: modelData.class ? Services.IconResolver.getIcon(modelData.class) : "" + + Image { + id: appIcon anchors.centerIn: parent - text: modelData.class ? modelData.class.charAt(0).toUpperCase() : "?" - font.pixelSize: 20 - font.bold: true - color: Common.Appearance.m3colors.onSecondaryContainer + width: 36 + height: 36 + source: parent.cachedIcon + sourceSize: Qt.size(36, 36) + smooth: true + visible: status === Image.Ready + } + + // Fallback: colored letter (TUI style) + Rectangle { + anchors.centerIn: parent + width: 36 + height: 36 + visible: appIcon.status !== Image.Ready + radius: Common.Appearance.rounding.tiny + color: Common.Appearance.colors.bgVisual + border.width: 1 + border.color: Common.Appearance.colors.border + + Text { + anchors.centerIn: parent + text: modelData.class ? modelData.class.charAt(0).toUpperCase() : "?" + font.family: Common.Appearance.fonts.mono + font.pixelSize: 16 + font.bold: true + color: Common.Appearance.colors.cyan + } } } - } - // Window title - Text { - Layout.fillWidth: true - Layout.fillHeight: true - text: modelData.title || modelData.class || "Window" - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - color: switcherRow.currentIndex === windowDelegate.index - ? Common.Appearance.m3colors.onPrimaryContainer - : Common.Appearance.m3colors.onSurface - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.Wrap - maximumLineCount: 2 - elide: Text.ElideRight + // Window title + Text { + Layout.fillWidth: true + text: modelData.class || "Window" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: switcherRow.currentIndex === windowDelegate.index + ? Common.Appearance.colors.fg + : Common.Appearance.colors.fgDark + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + + // Workspace indicator + Text { + Layout.fillWidth: true + text: "ws:" + (modelData.workspace ? modelData.workspace.id : "?") + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny - 2 + color: Common.Appearance.colors.comment + horizontalAlignment: Text.AlignHCenter + } } + } + } - // Workspace indicator - Text { - Layout.fillWidth: true - text: "Workspace " + (modelData.workspace ? modelData.workspace.id : "?") - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.small - 2 - color: Common.Appearance.m3colors.onSurfaceVariant - horizontalAlignment: Text.AlignHCenter + // Keyboard hints bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 20 + color: Common.Appearance.colors.bgDark + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: Common.Appearance.colors.border + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.small + anchors.rightMargin: Common.Appearance.spacing.small + spacing: Common.Appearance.spacing.large + + Repeater { + model: [ + { key: "Tab/h/l", action: "navigate" }, + { key: "Enter", action: "select" }, + { key: "Esc", action: "cancel" } + ] + + Row { + spacing: Common.Appearance.spacing.tiny + + Rectangle { + width: keyText.implicitWidth + Common.Appearance.spacing.small * 2 + height: 14 + radius: Common.Appearance.rounding.tiny + color: Common.Appearance.colors.bgVisual + anchors.verticalCenter: parent.verticalCenter + + Text { + id: keyText + anchors.centerIn: parent + text: modelData.key + font.family: Common.Appearance.fonts.mono + font.pixelSize: 9 + font.bold: true + color: Common.Appearance.colors.cyan + } + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: modelData.action + font.family: Common.Appearance.fonts.mono + font.pixelSize: 9 + color: Common.Appearance.colors.fgDark + } + } } + + Item { Layout.fillWidth: true } } } } } - // Current window title at bottom + // Current window title (vim-style message line) Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: switcherPanel.bottom - anchors.topMargin: Common.Appearance.spacing.medium + anchors.topMargin: Common.Appearance.spacing.small width: titleText.implicitWidth + Common.Appearance.spacing.large * 2 - height: titleText.implicitHeight + Common.Appearance.spacing.medium - radius: Common.Appearance.rounding.medium + height: 22 + radius: Common.Appearance.rounding.tiny color: Qt.rgba( - Common.Appearance.m3colors.surface.r, - Common.Appearance.m3colors.surface.g, - Common.Appearance.m3colors.surface.b, - 0.95 + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, + 0.96 ) + border.width: Common.Appearance.borderWidth.thin + border.color: Common.Appearance.colors.border visible: Services.Windows.windows.length > 0 Text { @@ -247,13 +355,13 @@ PanelWindow { const windows = Services.Windows.windows const idx = Services.Windows.currentIndex if (windows.length > 0 && idx < windows.length) { - return windows[idx].title || windows[idx].class || "" + return "\"" + (windows[idx].title || windows[idx].class || "") + "\"" } return "" } - font.family: Common.Appearance.fonts.main - font.pixelSize: Common.Appearance.fontSize.normal - color: Common.Appearance.m3colors.onSurface + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.tiny + color: Common.Appearance.colors.fgDark maximumLineCount: 1 elide: Text.ElideMiddle } From 16a1485b7d5ab10e40a3d12c1f18a3515feb5f76 Mon Sep 17 00:00:00 2001 From: binarypie Date: Mon, 26 Jan 2026 08:42:48 -0800 Subject: [PATCH 2/4] Block style UX --- .../quickshell/modules/bar/StatusBar.qml | 17 ++-- .../modules/sidebars/SidebarLeft.qml | 9 -- .../modules/sidebars/SidebarRight.qml | 57 ----------- dot_files/quickshell/services/Hyprland.qml | 98 +++++++++++++++++++ dot_files/quickshell/services/qmldir | 1 + 5 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 dot_files/quickshell/services/Hyprland.qml diff --git a/dot_files/quickshell/modules/bar/StatusBar.qml b/dot_files/quickshell/modules/bar/StatusBar.qml index d858e9a..4776590 100644 --- a/dot_files/quickshell/modules/bar/StatusBar.qml +++ b/dot_files/quickshell/modules/bar/StatusBar.qml @@ -47,6 +47,9 @@ PanelWindow { return "NORMAL" } + // Display text for mode indicator (shows all active workspaces when in NORMAL mode) + property string modeDisplayText: currentMode === "NORMAL" ? Services.Hyprland.allWorkspaces : currentMode + property color modeColor: { if (currentMode === "NORMAL") return Common.Appearance.colors.modeNormal if (currentMode === "APPS" || currentMode === "UPDATES") return Common.Appearance.colors.modeInsert @@ -192,7 +195,7 @@ PanelWindow { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - height: 1 + height: 0 color: Common.Appearance.colors.border } } @@ -216,7 +219,7 @@ PanelWindow { Text { id: modeText anchors.centerIn: parent - text: root.currentMode + text: root.modeDisplayText font.family: Common.Appearance.fonts.mono font.pixelSize: Common.Appearance.fontSize.small font.bold: true @@ -296,7 +299,7 @@ PanelWindow { // Separator after left section Rectangle { visible: root.isLeftmost - width: 1 + width: 0 height: parent.height color: Common.Appearance.colors.border } @@ -313,7 +316,7 @@ PanelWindow { // Separator before right section Rectangle { visible: root.isRightmost - width: 1 + width: 0 height: parent.height color: Common.Appearance.colors.border } @@ -424,7 +427,7 @@ PanelWindow { // Separator Rectangle { visible: root.isRightmost && SystemTray.items.length > 0 - width: 1 + width: 0 height: parent.height color: Common.Appearance.colors.border } @@ -534,7 +537,7 @@ PanelWindow { // Separator before clock Rectangle { visible: root.isRightmost - width: 1 + width: 0 height: parent.height color: Common.Appearance.colors.border } @@ -651,7 +654,7 @@ PanelWindow { // Separator before power Rectangle { visible: root.isRightmost - width: 1 + width: 0 height: parent.height color: Common.Appearance.colors.border } diff --git a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml index a500ae3..417a24e 100644 --- a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml +++ b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml @@ -21,9 +21,7 @@ PanelWindow { left: true } - margins.top: Common.Appearance.sizes.barHeight + Common.Appearance.spacing.small margins.bottom: Common.Appearance.spacing.small - margins.left: Common.Appearance.spacing.small implicitWidth: Common.Appearance.sizes.sidebarWidth color: "transparent" @@ -43,13 +41,6 @@ PanelWindow { // TUI Panel container Common.TuiPanel { anchors.fill: parent - title: Root.GlobalStates.sidebarLeftView === "apps" ? "[ Applications ]" : "[ System Updates ]" - keyHints: [ - { key: "Esc", action: "close" }, - { key: "/", action: "search" }, - { key: "j/k", action: "navigate" }, - { key: "Enter", action: "select" } - ] // Application View Loader { diff --git a/dot_files/quickshell/modules/sidebars/SidebarRight.qml b/dot_files/quickshell/modules/sidebars/SidebarRight.qml index 8cdd676..5b19e43 100644 --- a/dot_files/quickshell/modules/sidebars/SidebarRight.qml +++ b/dot_files/quickshell/modules/sidebars/SidebarRight.qml @@ -21,9 +21,7 @@ PanelWindow { right: true } - margins.top: Common.Appearance.sizes.barHeight + Common.Appearance.spacing.small margins.bottom: Common.Appearance.spacing.small - margins.right: Common.Appearance.spacing.small implicitWidth: Common.Appearance.sizes.sidebarWidth color: "transparent" @@ -34,64 +32,9 @@ PanelWindow { WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "sidebar" - // Get title based on current view - property string viewTitle: { - switch (Root.GlobalStates.sidebarRightView) { - case "network": return "[ Network ]" - case "bluetooth": return "[ Bluetooth ]" - case "audio": return "[ Audio ]" - case "calendar": return "[ Calendar ]" - case "notifications": return "[ Notifications ]" - case "power": return "[ Power ]" - case "weather": return "[ Weather ]" - default: return "[ Settings ]" - } - } - - // Get keyboard hints based on current view - property var viewHints: { - switch (Root.GlobalStates.sidebarRightView) { - case "network": - return [ - { key: "Esc", action: "close" }, - { key: "j/k", action: "navigate" }, - { key: "Enter", action: "connect" } - ] - case "bluetooth": - return [ - { key: "Esc", action: "close" }, - { key: "j/k", action: "navigate" }, - { key: "Enter", action: "pair" } - ] - case "audio": - return [ - { key: "Esc", action: "close" }, - { key: "m", action: "mute" }, - { key: "+/-", action: "volume" } - ] - case "notifications": - return [ - { key: "Esc", action: "close" }, - { key: "d", action: "dismiss" }, - { key: "D", action: "clear all" } - ] - case "power": - return [ - { key: "Esc", action: "close" }, - { key: "s", action: "suspend" }, - { key: "r", action: "reboot" }, - { key: "p", action: "poweroff" } - ] - default: - return [{ key: "Esc", action: "close" }] - } - } - // TUI Panel container Common.TuiPanel { anchors.fill: parent - title: root.viewTitle - keyHints: root.viewHints // Network View Loader { diff --git a/dot_files/quickshell/services/Hyprland.qml b/dot_files/quickshell/services/Hyprland.qml new file mode 100644 index 0000000..d03a51a --- /dev/null +++ b/dot_files/quickshell/services/Hyprland.qml @@ -0,0 +1,98 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Hyprland workspace tracking service +Singleton { + id: root + + // Map of monitor name -> active workspace info + property var workspaces: ({}) + + // Revision counter to force binding updates + property int revision: 0 + + // All active workspaces as a formatted string (e.g., "1 | 3") + property string allWorkspaces: { + void revision // Force re-evaluation + let ids = [] + for (const mon in workspaces) { + if (workspaces[mon] && workspaces[mon].id) { + ids.push(workspaces[mon].id) + } + } + ids.sort((a, b) => a - b) + return ids.length > 0 ? ids.join(" | ") : "1" + } + + // Get workspace ID as string for a specific monitor + // Note: Include revision in bindings to ensure updates: Services.Hyprland.revision, Services.Hyprland.getWorkspace(...) + function getWorkspace(monitorName: string): string { + if (workspaces[monitorName]) { + // Prefer numeric ID over name (names can be things like "DP-1" in some configs) + return String(workspaces[monitorName].id) + } + return "1" + } + + // Get workspace ID for a specific monitor + function getWorkspaceId(monitorName: string): int { + if (workspaces[monitorName]) { + return workspaces[monitorName].id || 1 + } + return 1 + } + + // Query monitors to get active workspaces + Process { + id: monitorProcess + command: ["hyprctl", "monitors", "-j"] + + property string output: "" + + stdout: SplitParser { + splitMarker: "" + onRead: data => { + monitorProcess.output += data + } + } + + onExited: { + try { + const monitors = JSON.parse(monitorProcess.output) + let newWorkspaces = {} + + for (const mon of monitors) { + if (mon.activeWorkspace) { + newWorkspaces[mon.name] = { + id: mon.activeWorkspace.id, + name: mon.activeWorkspace.name + } + } + } + + root.workspaces = newWorkspaces + root.revision++ + } catch (e) { + console.log("Failed to parse monitor workspace data:", e) + } + monitorProcess.output = "" + } + } + + // Refresh workspace data + function refresh() { + monitorProcess.running = true + } + + // Poll for workspace changes + Timer { + interval: 250 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refresh() + } +} diff --git a/dot_files/quickshell/services/qmldir b/dot_files/quickshell/services/qmldir index 1e49837..e22879d 100644 --- a/dot_files/quickshell/services/qmldir +++ b/dot_files/quickshell/services/qmldir @@ -12,5 +12,6 @@ singleton Notifications 1.0 Notifications.qml singleton Power 1.0 Power.qml singleton Privacy 1.0 Privacy.qml singleton Updates 1.0 Updates.qml +singleton Hyprland 1.0 Hyprland.qml singleton Weather 1.0 Weather.qml singleton Windows 1.0 Windows.qml From 3b4716d1a8245599b0d3602f10e15497fb0cc019 Mon Sep 17 00:00:00 2001 From: binarypie Date: Mon, 26 Jan 2026 08:48:23 -0800 Subject: [PATCH 3/4] round hyprland corners --- dot_files/quickshell/modules/common/Appearance.qml | 1 + dot_files/quickshell/modules/common/TuiPanel.qml | 4 ++-- dot_files/quickshell/modules/sidebars/SidebarLeft.qml | 5 ++++- dot_files/quickshell/modules/sidebars/SidebarRight.qml | 5 ++++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dot_files/quickshell/modules/common/Appearance.qml b/dot_files/quickshell/modules/common/Appearance.qml index 7d98d74..41c83ed 100644 --- a/dot_files/quickshell/modules/common/Appearance.qml +++ b/dot_files/quickshell/modules/common/Appearance.qml @@ -164,6 +164,7 @@ QtObject { small: 3, medium: 4, large: 6, + window: 10, // Match Hyprland window rounding full: 9999 }) diff --git a/dot_files/quickshell/modules/common/TuiPanel.qml b/dot_files/quickshell/modules/common/TuiPanel.qml index 7af7666..0ca2ed2 100644 --- a/dot_files/quickshell/modules/common/TuiPanel.qml +++ b/dot_files/quickshell/modules/common/TuiPanel.qml @@ -26,8 +26,8 @@ Rectangle { border.width: Appearance.borderWidth.thin border.color: Appearance.colors.border - // Sharp corners for TUI look - radius: Appearance.rounding.tiny + // Match Hyprland window rounding + radius: Appearance.rounding.window // Layout ColumnLayout { diff --git a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml index 417a24e..1247b2c 100644 --- a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml +++ b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml @@ -21,7 +21,10 @@ PanelWindow { left: true } - margins.bottom: Common.Appearance.spacing.small + // Match Hyprland gaps_out (20) for floating window look + margins.top: 20 + margins.bottom: 20 + margins.left: 20 implicitWidth: Common.Appearance.sizes.sidebarWidth color: "transparent" diff --git a/dot_files/quickshell/modules/sidebars/SidebarRight.qml b/dot_files/quickshell/modules/sidebars/SidebarRight.qml index 5b19e43..15b9627 100644 --- a/dot_files/quickshell/modules/sidebars/SidebarRight.qml +++ b/dot_files/quickshell/modules/sidebars/SidebarRight.qml @@ -21,7 +21,10 @@ PanelWindow { right: true } - margins.bottom: Common.Appearance.spacing.small + // Match Hyprland gaps_out (20) for floating window look + margins.top: 20 + margins.bottom: 20 + margins.right: 20 implicitWidth: Common.Appearance.sizes.sidebarWidth color: "transparent" From b212e6bae23e6eec4f7c66e20ccf8941bd274d39 Mon Sep 17 00:00:00 2001 From: binarypie Date: Mon, 26 Jan 2026 09:09:00 -0800 Subject: [PATCH 4/4] round status bar --- .../quickshell/modules/bar/StatusBar.qml | 1125 ++++++++--------- .../quickshell/modules/common/Appearance.qml | 2 +- 2 files changed, 561 insertions(+), 566 deletions(-) diff --git a/dot_files/quickshell/modules/bar/StatusBar.qml b/dot_files/quickshell/modules/bar/StatusBar.qml index 4776590..eb8f314 100644 --- a/dot_files/quickshell/modules/bar/StatusBar.qml +++ b/dot_files/quickshell/modules/bar/StatusBar.qml @@ -8,7 +8,7 @@ import "../common" as Common import "../../services" as Services import "../../" as Root -// Lualine-inspired status bar with vim-style segments +// Status bar with two rounded pill sections PanelWindow { id: root @@ -56,120 +56,6 @@ PanelWindow { return Common.Appearance.colors.modeVisual } - // Segment component - lualine style section - component Segment: Rectangle { - id: segment - property string segmentText: "" - property string icon: "" - property color segmentColor: Common.Appearance.colors.bgHighlight - property color textColor: Common.Appearance.colors.fg - property bool showSeparator: true - property bool isActive: false - property bool clickable: false - signal clicked() - - color: segmentColor - implicitWidth: segmentContent.implicitWidth + Common.Appearance.spacing.medium * 2 - implicitHeight: parent.height - - RowLayout { - id: segmentContent - anchors.centerIn: parent - spacing: Common.Appearance.spacing.small - - Common.Icon { - visible: segment.icon !== "" - name: segment.icon - size: Common.Appearance.sizes.iconSmall - color: segment.isActive ? Common.Appearance.colors.blue : segment.textColor - } - - Text { - visible: segment.segmentText !== "" - text: segment.segmentText - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - font.bold: segment.isActive - color: segment.isActive ? Common.Appearance.colors.blue : segment.textColor - } - } - - MouseArea { - anchors.fill: parent - enabled: segment.clickable - cursorShape: segment.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor - hoverEnabled: segment.clickable - onClicked: segment.clicked() - - Rectangle { - anchors.fill: parent - color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.05) : "transparent" - } - } - - // Right separator - Text { - visible: segment.showSeparator - anchors.right: parent.right - anchors.rightMargin: -width / 2 - anchors.verticalCenter: parent.verticalCenter - text: Common.Appearance.separators.right - font.family: Common.Appearance.fonts.mono - font.pixelSize: parent.height - color: segment.segmentColor - z: 1 - } - } - - // Icon-only segment for tray items - component IconSegment: Rectangle { - id: iconSeg - property string icon: "" - property color iconColor: Common.Appearance.colors.fgDark - property color segmentColor: Common.Appearance.colors.bgHighlight - property bool clickable: false - property bool showBadge: false - property color badgeColor: Common.Appearance.colors.error - signal clicked() - - color: segmentColor - implicitWidth: Common.Appearance.sizes.barHeight - implicitHeight: parent.height - - Common.Icon { - anchors.centerIn: parent - name: iconSeg.icon - size: Common.Appearance.sizes.iconSmall - color: iconSeg.iconColor - } - - // Badge indicator - Rectangle { - visible: iconSeg.showBadge - width: 6 - height: 6 - radius: 3 - color: iconSeg.badgeColor - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 6 - anchors.rightMargin: 8 - } - - MouseArea { - anchors.fill: parent - enabled: iconSeg.clickable - cursorShape: iconSeg.clickable ? Qt.PointingHandCursor : Qt.ArrowCursor - hoverEnabled: iconSeg.clickable - onClicked: iconSeg.clicked() - - Rectangle { - anchors.fill: parent - color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - } - } - } - // Helper properties for screen position property bool isLeftmost: { if (Quickshell.screens.length === 1) return true @@ -180,545 +66,654 @@ PanelWindow { return targetScreen === Root.GlobalStates.rightmostScreen } - // Bar background - Rectangle { - anchors.fill: parent - color: Qt.rgba( - Common.Appearance.colors.bgDark.r, - Common.Appearance.colors.bgDark.g, - Common.Appearance.colors.bgDark.b, - Common.Appearance.panelOpacity - ) - - // Bottom border line - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 0 - color: Common.Appearance.colors.border - } - } + // Pill dimensions - use window rounding to match Hyprland + property int pillHeight: Common.Appearance.sizes.barHeight + property int pillRadius: Common.Appearance.rounding.window - // Bar content - RowLayout { + // Bar content - two pills at edges (margins match Hyprland gaps_out) + Item { anchors.fill: parent - spacing: 0 + anchors.topMargin: 2 + anchors.bottomMargin: 0 + anchors.leftMargin: 20 + anchors.rightMargin: 20 // ═══════════════════════════════════════════════════════════════ - // LEFT SECTION - Mode indicator + navigation + // LEFT PILL - Mode indicator + apps + updates // ═══════════════════════════════════════════════════════════════ - - // Mode indicator (vim-style) Rectangle { + id: leftPill visible: root.isLeftmost - color: root.modeColor - implicitWidth: modeText.implicitWidth + Common.Appearance.spacing.large * 2 - implicitHeight: parent.height - - Text { - id: modeText - anchors.centerIn: parent - text: root.modeDisplayText - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - font.bold: true - color: Common.Appearance.colors.bg - } - - // Powerline separator - Text { - anchors.left: parent.right - anchors.leftMargin: -1 - anchors.verticalCenter: parent.verticalCenter - text: Common.Appearance.separators.left - font.family: Common.Appearance.fonts.mono - font.pixelSize: parent.height - color: root.modeColor - z: 1 - } - } + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: root.pillHeight + width: leftPillContent.implicitWidth + radius: root.pillRadius + color: Qt.rgba( + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, + Common.Appearance.panelOpacity + ) + border.width: 1 + border.color: Common.Appearance.colors.border + clip: true - // Apps button - IconSegment { - visible: root.isLeftmost - icon: Common.Icons.icons.apps - segmentColor: Common.Appearance.colors.bgHighlight - iconColor: Root.GlobalStates.sidebarLeftView === "apps" && Root.GlobalStates.sidebarLeftOpen - ? Common.Appearance.colors.blue - : Common.Appearance.colors.fgDark - clickable: true - onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen, "apps") - } + RowLayout { + id: leftPillContent + anchors.fill: parent + spacing: 0 - // Updates button - MouseArea { - id: updatesButton - visible: root.isLeftmost - implicitWidth: Common.Appearance.sizes.barHeight - implicitHeight: parent.height - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen, "updates") + // Mode indicator + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: modeText.implicitWidth + Common.Appearance.spacing.medium * 2 + color: root.modeColor + radius: root.pillRadius - property bool isRunning: Services.Updates.preinstallRunning - property bool needsAttention: Services.Updates.needsAttention + // Square off the right side + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: root.pillRadius + color: parent.color + } - Rectangle { - anchors.fill: parent - color: Common.Appearance.colors.bgHighlight + Text { + id: modeText + anchors.centerIn: parent + text: root.modeDisplayText + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.bg + } + } + // Apps button Rectangle { - anchors.fill: parent - color: updatesButton.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" + + Common.Icon { + anchors.centerIn: parent + name: Common.Icons.icons.apps + size: Common.Appearance.sizes.iconSmall + color: Root.GlobalStates.sidebarLeftView === "apps" && Root.GlobalStates.sidebarLeftOpen + ? Common.Appearance.colors.blue + : Common.Appearance.colors.fgDark + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen, "apps") + + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } } - } - Common.Icon { - anchors.centerIn: parent - name: updatesButton.isRunning - ? Common.Icons.icons.refresh - : (updatesButton.needsAttention - ? Common.Icons.icons.download - : Common.Icons.icons.checkCircle) - size: Common.Appearance.sizes.iconSmall - color: updatesButton.needsAttention - ? Common.Appearance.colors.green - : Common.Appearance.colors.fgDark - - RotationAnimation on rotation { - running: updatesButton.isRunning - from: 0 - to: 360 - duration: 1000 - loops: Animation.Infinite + // Updates button + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" + radius: root.pillRadius + + // Square off the left side + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: root.pillRadius + color: parent.color + } + + property bool isRunning: Services.Updates.preinstallRunning ?? false + property bool needsAttention: Services.Updates.needsAttention ?? false + + Common.Icon { + anchors.centerIn: parent + name: parent.isRunning + ? Common.Icons.icons.refresh + : (parent.needsAttention + ? Common.Icons.icons.download + : Common.Icons.icons.checkCircle) + size: Common.Appearance.sizes.iconSmall + color: parent.needsAttention + ? Common.Appearance.colors.green + : Common.Appearance.colors.fgDark + + RotationAnimation on rotation { + running: Services.Updates.preinstallRunning ?? false + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen, "updates") + + Rectangle { + anchors.fill: parent + radius: root.pillRadius + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: root.pillRadius + color: parent.color + } + } + } } } } - // Separator after left section - Rectangle { - visible: root.isLeftmost - width: 0 - height: parent.height - color: Common.Appearance.colors.border - } - - // ═══════════════════════════════════════════════════════════════ - // CENTER SECTION - Spacer (could show workspace info later) - // ═══════════════════════════════════════════════════════════════ - Item { Layout.fillWidth: true } - // ═══════════════════════════════════════════════════════════════ - // RIGHT SECTION - System indicators + // RIGHT PILL - System indicators // ═══════════════════════════════════════════════════════════════ - - // Separator before right section Rectangle { + id: rightPill visible: root.isRightmost - width: 0 - height: parent.height - color: Common.Appearance.colors.border - } - - // System tray - RowLayout { - visible: root.isRightmost - spacing: 0 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: root.pillHeight + width: rightPillContent.implicitWidth + radius: root.pillRadius + color: Qt.rgba( + Common.Appearance.colors.bgDark.r, + Common.Appearance.colors.bgDark.g, + Common.Appearance.colors.bgDark.b, + Common.Appearance.panelOpacity + ) + border.width: 1 + border.color: Common.Appearance.colors.border + clip: true - Repeater { - model: SystemTray.items + RowLayout { + id: rightPillContent + anchors.fill: parent + spacing: 0 + + // System tray + Repeater { + model: SystemTray.items + + delegate: Rectangle { + id: trayItemRect + required property var modelData + required property int index + + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" + radius: index === 0 ? root.pillRadius : 0 + + property bool hasCustomPath: modelData.icon && modelData.icon.includes("?path=") + + property string iconSource: { + const icon = modelData.icon + if (!icon || icon === "") return "" + if (icon.includes("?path=")) return "" + if (icon.startsWith("/")) return "file://" + icon + if (icon.startsWith("file://") || icon.startsWith("image://")) return icon + return "image://icon/" + icon + } - delegate: MouseArea { - id: trayItemArea - required property var modelData + property string datacubeIcon: Services.IconResolver.getIcon(modelData.title) + property bool primaryFailed: hasCustomPath || primaryTrayIcon.status === Image.Error || primaryTrayIcon.status === Image.Null || iconSource === "" - implicitWidth: Common.Appearance.sizes.barHeight - implicitHeight: Common.Appearance.sizes.barHeight - hoverEnabled: true - cursorShape: Qt.PointingHandCursor + Image { + id: primaryTrayIcon + anchors.centerIn: parent + width: Common.Appearance.sizes.iconSmall + height: Common.Appearance.sizes.iconSmall + sourceSize: Qt.size(Common.Appearance.sizes.iconSmall, Common.Appearance.sizes.iconSmall) + source: trayItemRect.iconSource + smooth: true + visible: status === Image.Ready + } - property bool hasCustomPath: modelData.icon && modelData.icon.includes("?path=") + Image { + id: fallbackTrayIcon + anchors.centerIn: parent + width: Common.Appearance.sizes.iconSmall + height: Common.Appearance.sizes.iconSmall + sourceSize: Qt.size(Common.Appearance.sizes.iconSmall, Common.Appearance.sizes.iconSmall) + source: trayItemRect.primaryFailed ? trayItemRect.datacubeIcon : "" + smooth: true + visible: trayItemRect.primaryFailed && status === Image.Ready + } - property string iconSource: { - const icon = modelData.icon - if (!icon || icon === "") return "" - if (icon.includes("?path=")) return "" - if (icon.startsWith("/")) return "file://" + icon - if (icon.startsWith("file://") || icon.startsWith("image://")) return icon - return "image://icon/" + icon - } + Rectangle { + anchors.centerIn: parent + width: Common.Appearance.sizes.iconSmall + height: Common.Appearance.sizes.iconSmall + radius: Common.Appearance.rounding.tiny + color: Common.Appearance.colors.bgVisual + visible: trayItemRect.primaryFailed && fallbackTrayIcon.status !== Image.Ready + + Text { + anchors.centerIn: parent + text: trayItemRect.modelData.title ? trayItemRect.modelData.title.charAt(0).toUpperCase() : "?" + font.pixelSize: 9 + font.bold: true + color: Common.Appearance.colors.fg + } + } - property string datacubeIcon: Services.IconResolver.getIcon(modelData.title) + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton || (trayItemRect.modelData.onlyMenu && trayItemRect.modelData.hasMenu)) { + if (trayItemRect.modelData.hasMenu) { + const pos = trayItemRect.mapToItem(null, 0, trayItemRect.height) + trayItemRect.modelData.display(root, pos.x, pos.y) + } + } else if (mouse.button === Qt.MiddleButton) { + trayItemRect.modelData.secondaryActivate() + } else { + trayItemRect.modelData.activate() + } + } - Rectangle { - anchors.fill: parent - color: Common.Appearance.colors.bgHighlight + onWheel: (wheel) => { + trayItemRect.modelData.scroll(wheel.angleDelta.y, false) + } - Rectangle { - anchors.fill: parent - color: trayItemArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } } } + } - property bool primaryFailed: trayItemArea.hasCustomPath || primaryTrayIcon.status === Image.Error || primaryTrayIcon.status === Image.Null || trayItemArea.iconSource === "" + // Separator after tray + Rectangle { + visible: SystemTray.items.length > 0 + Layout.fillHeight: true + Layout.preferredWidth: 1 + Layout.topMargin: 6 + Layout.bottomMargin: 6 + color: Common.Appearance.colors.border + } - Image { - id: primaryTrayIcon - anchors.centerIn: parent - width: Common.Appearance.sizes.iconSmall - height: Common.Appearance.sizes.iconSmall - sourceSize: Qt.size(Common.Appearance.sizes.iconSmall, Common.Appearance.sizes.iconSmall) - source: trayItemArea.iconSource - smooth: true - visible: status === Image.Ready - } + // Camera Privacy indicator + Rectangle { + visible: Services.Privacy.cameraInUse + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" - Image { - id: fallbackTrayIcon + Common.Icon { anchors.centerIn: parent - width: Common.Appearance.sizes.iconSmall - height: Common.Appearance.sizes.iconSmall - sourceSize: Qt.size(Common.Appearance.sizes.iconSmall, Common.Appearance.sizes.iconSmall) - source: trayItemArea.primaryFailed ? trayItemArea.datacubeIcon : "" - smooth: true - visible: trayItemArea.primaryFailed && status === Image.Ready + name: Common.Icons.icons.camera + size: Common.Appearance.sizes.iconSmall + color: Common.Appearance.colors.error } + } - Rectangle { + // Audio segment + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: audioContent.implicitWidth + Common.Appearance.spacing.medium * 2 + color: "transparent" + + RowLayout { + id: audioContent anchors.centerIn: parent - width: Common.Appearance.sizes.iconSmall - height: Common.Appearance.sizes.iconSmall - radius: Common.Appearance.rounding.tiny - color: Common.Appearance.colors.bgVisual - visible: trayItemArea.primaryFailed && fallbackTrayIcon.status !== Image.Ready + spacing: Common.Appearance.spacing.small + + Common.Icon { + name: Services.Audio.micMuted + ? Common.Icons.icons.micOff + : Common.Icons.icons.mic + size: Common.Appearance.sizes.iconSmall + color: Services.Privacy.micInUse + ? Common.Appearance.colors.error + : (Services.Audio.micMuted ? Common.Appearance.colors.comment : Common.Appearance.colors.fgDark) + } - Text { - anchors.centerIn: parent - text: trayItemArea.modelData.title ? trayItemArea.modelData.title.charAt(0).toUpperCase() : "?" - font.pixelSize: 9 - font.bold: true - color: Common.Appearance.colors.fg + Common.Icon { + name: Services.Audio.muted + ? Common.Icons.icons.volumeOff + : Common.Icons.volumeIcon(Services.Audio.volume * 100, false) + size: Common.Appearance.sizes.iconSmall + color: Services.Audio.muted ? Common.Appearance.colors.comment : Common.Appearance.colors.fgDark } } - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "audio") - onClicked: (mouse) => { - if (mouse.button === Qt.RightButton || (trayItemArea.modelData.onlyMenu && trayItemArea.modelData.hasMenu)) { - if (trayItemArea.modelData.hasMenu) { - const pos = trayItemArea.mapToItem(null, 0, trayItemArea.height) - trayItemArea.modelData.display(root, pos.x, pos.y) - } - } else if (mouse.button === Qt.MiddleButton) { - trayItemArea.modelData.secondaryActivate() - } else { - trayItemArea.modelData.activate() + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" } } - - onWheel: (wheel) => { - trayItemArea.modelData.scroll(wheel.angleDelta.y, false) - } } - } - } - // Separator - Rectangle { - visible: root.isRightmost && SystemTray.items.length > 0 - width: 0 - height: parent.height - color: Common.Appearance.colors.border - } - - // Camera Privacy indicator - IconSegment { - visible: root.isRightmost && Services.Privacy.cameraInUse - icon: Common.Icons.icons.camera - iconColor: Common.Appearance.colors.error - segmentColor: Common.Appearance.colors.bgHighlight - } + // Bluetooth + Rectangle { + visible: Services.BluetoothStatus.available + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" - // Audio segment (mic + speaker) - MouseArea { - visible: root.isRightmost - implicitWidth: audioContent.implicitWidth + Common.Appearance.spacing.medium * 2 - implicitHeight: parent.height - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "audio") + Common.Icon { + anchors.centerIn: parent + name: Services.BluetoothStatus.powered + ? (Services.BluetoothStatus.connected + ? Common.Icons.icons.bluetoothConnected + : Common.Icons.icons.bluetooth) + : Common.Icons.icons.bluetoothOff + size: Common.Appearance.sizes.iconSmall + color: Services.BluetoothStatus.connected + ? Common.Appearance.colors.blue + : (Services.BluetoothStatus.powered ? Common.Appearance.colors.fgDark : Common.Appearance.colors.comment) + } - Rectangle { - anchors.fill: parent - color: Common.Appearance.colors.bgHighlight + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "bluetooth") - Rectangle { - anchors.fill: parent - color: parent.parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } } - } - RowLayout { - id: audioContent - anchors.centerIn: parent - spacing: Common.Appearance.spacing.small - - Common.Icon { - name: Services.Audio.micMuted - ? Common.Icons.icons.micOff - : Common.Icons.icons.mic - size: Common.Appearance.sizes.iconSmall - color: Services.Privacy.micInUse - ? Common.Appearance.colors.error - : (Services.Audio.micMuted ? Common.Appearance.colors.comment : Common.Appearance.colors.fgDark) - } + // Network + Rectangle { + visible: Common.Config.showNetwork + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" - Common.Icon { - name: Services.Audio.muted - ? Common.Icons.icons.volumeOff - : Common.Icons.volumeIcon(Services.Audio.volume * 100, false) - size: Common.Appearance.sizes.iconSmall - color: Services.Audio.muted ? Common.Appearance.colors.comment : Common.Appearance.colors.fgDark - } - } - } + Common.Icon { + anchors.centerIn: parent + name: { + if (!Services.Network.connected) { + return Services.Network.wifiAvailable ? Common.Icons.icons.wifiOff : Common.Icons.icons.ethernetOff + } + if (Services.Network.type === "wifi") { + return Common.Icons.wifiIcon(Services.Network.strength, true) + } + return Common.Icons.icons.ethernet + } + size: Common.Appearance.sizes.iconSmall + color: Services.Network.connected ? Common.Appearance.colors.fgDark : Common.Appearance.colors.comment + } - // Bluetooth - IconSegment { - visible: root.isRightmost && Services.BluetoothStatus.available - icon: Services.BluetoothStatus.powered - ? (Services.BluetoothStatus.connected - ? Common.Icons.icons.bluetoothConnected - : Common.Icons.icons.bluetooth) - : Common.Icons.icons.bluetoothOff - iconColor: Services.BluetoothStatus.connected - ? Common.Appearance.colors.blue - : (Services.BluetoothStatus.powered ? Common.Appearance.colors.fgDark : Common.Appearance.colors.comment) - segmentColor: Common.Appearance.colors.bgHighlight - clickable: true - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "bluetooth") - } + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "network") - // Network - IconSegment { - visible: root.isRightmost && Common.Config.showNetwork - icon: { - if (!Services.Network.connected) { - return Services.Network.wifiAvailable ? Common.Icons.icons.wifiOff : Common.Icons.icons.ethernetOff - } - if (Services.Network.type === "wifi") { - return Common.Icons.wifiIcon(Services.Network.strength, true) + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } } - return Common.Icons.icons.ethernet - } - iconColor: Services.Network.connected ? Common.Appearance.colors.fgDark : Common.Appearance.colors.comment - segmentColor: Common.Appearance.colors.bgHighlight - clickable: true - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "network") - } - // Notifications - IconSegment { - visible: root.isRightmost - icon: Root.GlobalStates.doNotDisturb - ? Common.Icons.icons.doNotDisturb - : Common.Icons.icons.notification - iconColor: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb - ? Common.Appearance.colors.orange - : Common.Appearance.colors.fgDark - segmentColor: Common.Appearance.colors.bgHighlight - clickable: true - showBadge: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb - badgeColor: Common.Appearance.colors.orange - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "notifications") - } + // Notifications + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: root.pillHeight + color: "transparent" - // Separator before clock - Rectangle { - visible: root.isRightmost - width: 0 - height: parent.height - color: Common.Appearance.colors.border - } + Common.Icon { + anchors.centerIn: parent + name: Root.GlobalStates.doNotDisturb + ? Common.Icons.icons.doNotDisturb + : Common.Icons.icons.notification + size: Common.Appearance.sizes.iconSmall + color: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb + ? Common.Appearance.colors.orange + : Common.Appearance.colors.fgDark + } - // Clock segment (prominent) - MouseArea { - visible: root.isRightmost - implicitWidth: clockContent.implicitWidth + Common.Appearance.spacing.large * 2 - implicitHeight: parent.height - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "calendar") + // Badge + Rectangle { + visible: Root.GlobalStates.unreadNotificationCount > 0 && !Root.GlobalStates.doNotDisturb + width: 6 + height: 6 + radius: 3 + color: Common.Appearance.colors.orange + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 4 + anchors.rightMargin: 6 + } - Rectangle { - anchors.fill: parent - color: Common.Appearance.colors.bgHighlight + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "notifications") - Rectangle { - anchors.fill: parent - color: parent.parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } } - } - RowLayout { - id: clockContent - anchors.centerIn: parent - spacing: Common.Appearance.spacing.small - - Text { - text: Services.DateTime.timeString - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - font.bold: true - color: Common.Appearance.colors.fg - } + // Clock segment + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: clockContent.implicitWidth + Common.Appearance.spacing.medium * 2 + color: "transparent" - Text { - text: Common.Appearance.separators.pipe - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.colors.comment - } + RowLayout { + id: clockContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small - Text { - text: Services.DateTime.shortDateString - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.colors.fgDark - } - } + Text { + text: Services.DateTime.shortDateString + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.fg + } - onContainsMouseChanged: { - if (containsMouse) { - Root.GlobalStates.osdType = "tooltip" - Root.GlobalStates.osdTooltipText = Services.DateTime.fullDateTimeString - Root.GlobalStates.osdVisible = true - } else { - if (Root.GlobalStates.osdType === "tooltip") { - Root.GlobalStates.osdVisible = false + Text { + text: Common.Appearance.separators.pipe + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.bgHighlight + } + + Text { + text: Services.DateTime.timeString + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fgDark + } } - } - } - Timer { - interval: 1000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: Services.DateTime.update() - } - } + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "calendar") + + onContainsMouseChanged: { + if (containsMouse) { + Root.GlobalStates.osdType = "tooltip" + Root.GlobalStates.osdTooltipText = Services.DateTime.fullDateTimeString + Root.GlobalStates.osdVisible = true + } else { + if (Root.GlobalStates.osdType === "tooltip") { + Root.GlobalStates.osdVisible = false + } + } + } - // Weather segment (if enabled) - MouseArea { - visible: root.isRightmost && Common.Config.showWeather - implicitWidth: weatherContent.implicitWidth + Common.Appearance.spacing.medium * 2 - implicitHeight: parent.height - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "weather") + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + } - Rectangle { - anchors.fill: parent - color: Common.Appearance.colors.bgHighlight + Timer { + interval: 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: Services.DateTime.update() + } + } + // Weather segment Rectangle { - anchors.fill: parent - color: parent.parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - } - } + visible: Common.Config.showWeather + Layout.fillHeight: true + Layout.preferredWidth: weatherContent.implicitWidth + Common.Appearance.spacing.medium * 2 + color: "transparent" - RowLayout { - id: weatherContent - anchors.centerIn: parent - spacing: Common.Appearance.spacing.small - - Common.Icon { - name: Services.Weather.ready - ? Common.Icons.weatherIcon(Services.Weather.condition, Services.Weather.isNight) - : Common.Icons.icons.cloudy - size: Common.Appearance.sizes.iconSmall - color: Common.Appearance.colors.cyan - } + RowLayout { + id: weatherContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Common.Icon { + name: Services.Weather.ready + ? Common.Icons.weatherIcon(Services.Weather.condition, Services.Weather.isNight) + : Common.Icons.icons.cloudy + size: Common.Appearance.sizes.iconSmall + color: Common.Appearance.colors.cyan + } - Text { - text: Services.Weather.ready ? Services.Weather.temperature : "--°" - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - color: Common.Appearance.colors.fgDark - } - } - } + Text { + text: Services.Weather.ready ? Services.Weather.temperature : "--°" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.colors.fgDark + } + } - // Separator before power - Rectangle { - visible: root.isRightmost - width: 0 - height: parent.height - color: Common.Appearance.colors.border - } + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "weather") - // Power/Battery segment (rightmost, colored) - MouseArea { - visible: root.isRightmost - implicitWidth: powerContent.implicitWidth + Common.Appearance.spacing.medium * 2 - implicitHeight: parent.height - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "power") - - property color powerColor: { - if (Services.Battery.present) { - if (Services.Battery.percent <= 20 && !Services.Battery.charging) { - return Common.Appearance.colors.error - } - if (Services.Battery.pluggedIn) { - return Common.Appearance.colors.green + Rectangle { + anchors.fill: parent + color: parent.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } } } - return Common.Appearance.colors.magenta - } - - Rectangle { - anchors.fill: parent - color: parent.powerColor + // Power/Battery segment (rightmost, colored) Rectangle { - anchors.fill: parent - color: parent.parent.containsMouse ? Qt.rgba(0, 0, 0, 0.1) : "transparent" - } - } + Layout.fillHeight: true + Layout.preferredWidth: powerContent.implicitWidth + Common.Appearance.spacing.medium * 2 + radius: root.pillRadius + color: powerColor - RowLayout { - id: powerContent - anchors.centerIn: parent - spacing: Common.Appearance.spacing.small + // Square off the left side + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: root.pillRadius + color: parent.color + } - Common.Icon { - name: { + property color powerColor: { if (Services.Battery.present) { - if (Services.Battery.pluggedIn && Services.Battery.percent >= 95) { - return Common.Icons.icons.plug - } else if (Services.Battery.charging) { - return Common.Icons.icons.batteryCharging - } else { - return Common.Icons.batteryIcon(Services.Battery.percent, false) + if (Services.Battery.percent <= 20 && !Services.Battery.charging) { + return Common.Appearance.colors.error + } + if (Services.Battery.pluggedIn) { + return Common.Appearance.colors.green } } - return Common.Icons.icons.power + return Common.Appearance.colors.magenta } - size: Common.Appearance.sizes.iconSmall - color: Common.Appearance.colors.bg - } - Text { - visible: Services.Battery.present - text: Services.Battery.percent + "%" - font.family: Common.Appearance.fonts.mono - font.pixelSize: Common.Appearance.fontSize.small - font.bold: true - color: Common.Appearance.colors.bg + RowLayout { + id: powerContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Common.Icon { + name: { + if (Services.Battery.present) { + if (Services.Battery.pluggedIn && Services.Battery.percent >= 95) { + return Common.Icons.icons.plug + } else if (Services.Battery.charging) { + return Common.Icons.icons.batteryCharging + } else { + return Common.Icons.batteryIcon(Services.Battery.percent, false) + } + } + return Common.Icons.icons.power + } + size: Common.Appearance.sizes.iconSmall + color: Common.Appearance.colors.bg + } + + Text { + visible: Services.Battery.present + text: Services.Battery.percent + "%" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + font.bold: true + color: Common.Appearance.colors.bg + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "power") + + Rectangle { + anchors.fill: parent + radius: root.pillRadius + color: parent.containsMouse ? Qt.rgba(0, 0, 0, 0.1) : "transparent" + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: root.pillRadius + color: parent.color + } + } + } } } } diff --git a/dot_files/quickshell/modules/common/Appearance.qml b/dot_files/quickshell/modules/common/Appearance.qml index 41c83ed..e541289 100644 --- a/dot_files/quickshell/modules/common/Appearance.qml +++ b/dot_files/quickshell/modules/common/Appearance.qml @@ -186,7 +186,7 @@ QtObject { launcherHeight: 500, notificationWidth: 380, iconTiny: 12, - iconSmall: 14, + iconSmall: 16, iconMedium: 16, iconLarge: 20, iconXLarge: 24