// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Widgets import QtQuick import ".." import "../modules/vm" import "calendar" import "Logic.js" as Logic // Date and time at rest, with one dot per VM that is not plainly shut off. // During a flick: the workspace's desktop icon, the app's icon and its name. // A click opens the calendar panel in this capsule. Capsule { id: pill property bool flicking: false property string flickWs: "" property string flickApp: "" property string panel: "" signal openCalendar() signal closeCalendar() expanded: panel === "calendar" panelWidth: 620 panelHeight: calendarPanel.implicitHeight + 28 // The island calls these: focus moves into the panel when it opens, and // Escape asks the panel before closing it. function focusPanel() { calendarPanel.reset(); calendarPanel.forceActiveFocus(); } function back() { return calendarPanel.back(); } readonly property var entry: flickApp ? DesktopEntries.heuristicLookup(flickApp) : null readonly property string wsIcon: Workspaces.icon(flickWs) // Resolved paths, not names, decide what shows: a name the icon theme // lacks resolves to "", and testing the name left a blank slot where the // workspace number should have been. readonly property string wsPath: wsIcon ? Quickshell.iconPath(wsIcon, true) : "" readonly property string appPath: flickApp ? Quickshell.iconPath(entry?.icon || flickApp, true) : "" restWidth: (flicking ? flickRow.implicitWidth : restRow.implicitWidth) + 28 SystemClock { id: clock; precision: SystemClock.Minutes } Row { id: restRow anchors.centerIn: parent spacing: 10 opacity: pill.flicking || pill.expanded ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 150 } } Text { anchors.verticalCenter: parent.verticalCenter text: Qt.formatDateTime(clock.date, "ddd dd/MM") font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Text { anchors.verticalCenter: parent.verticalCenter text: Qt.formatDateTime(clock.date, "HH:mm") font { family: Theme.fontFamily; pixelSize: Theme.fontSize; weight: Font.DemiBold } color: Theme.text } Repeater { model: Virsh.names.filter(n => Logic.vmRole(Virsh.vms[n]) !== "") delegate: Rectangle { required property string modelData anchors.verticalCenter: parent.verticalCenter width: 8 height: 8 radius: 4 color: Theme[Logic.vmRole(Virsh.vms[modelData])] } } } Row { id: flickRow anchors.centerIn: parent spacing: 8 opacity: pill.flicking && !pill.expanded ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 150 } } // The desktop sits in a badge so it does not read as a second app // icon next to the real one. Rectangle { anchors.verticalCenter: parent.verticalCenter implicitWidth: Math.max(26, wsContent.implicitWidth + 10) implicitHeight: 24 radius: 6 color: Qt.alpha(Theme.surface, 0.9) border.width: 1 border.color: Qt.alpha(Theme.accent, 0.6) Item { id: wsContent anchors.centerIn: parent implicitWidth: pill.wsPath !== "" ? wsIconImage.implicitWidth : wsNumber.implicitWidth implicitHeight: 18 IconImage { id: wsIconImage anchors.centerIn: parent visible: pill.wsPath !== "" implicitSize: 16 source: pill.wsPath } // A named workspace, or one past eight, has no icon: its number. Text { id: wsNumber anchors.centerIn: parent visible: pill.wsPath === "" text: pill.flickWs font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; weight: Font.DemiBold } color: Theme.accent } } } IconImage { anchors.verticalCenter: parent.verticalCenter visible: pill.appPath !== "" implicitSize: 18 source: pill.appPath } Text { anchors.verticalCenter: parent.verticalCenter visible: pill.flickApp !== "" text: pill.entry?.name || pill.flickApp font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 } color: Theme.text } } // At rest the whole pill is the button; open, the panel takes the clicks. MouseArea { anchors.fill: parent enabled: !pill.expanded cursorShape: Qt.PointingHandCursor onClicked: pill.openCalendar() } CalendarPanel { id: calendarPanel x: 14 y: 14 width: pill.panelWidth - 28 opacity: pill.expanded ? 1 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation { duration: 160 } } onCloseRequested: pill.closeCalendar() } }