// 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.Wayland import QtQuick Scope { id: root // Waybar runs on this screen, so the drawer belongs here too. property string monitor: "DP-1" property bool open: false readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] function show() { // Mail has almost certainly arrived since this was last opened, and // for an autostarted shell that is the whole session. Accounts.refresh(); root.open = true; } function close() { root.open = false; } function toggle() { root.open ? close() : show(); } // Quickshell exits once no window is visible, and this drawer is closed // most of the time. See AGENTS.md. PanelWindow { visible: true implicitWidth: 1 implicitHeight: 1 color: "transparent" exclusionMode: ExclusionMode.Ignore mask: Region {} WlrLayershell.keyboardFocus: WlrKeyboardFocus.None } LazyLoader { active: root.open PanelWindow { id: win screen: root.screenObj anchors { top: true; left: true; right: true; bottom: true } color: "transparent" // Normal, not Ignore: waybar claims an exclusive zone at the top // of this screen, so respecting it puts the drawer below the bar // without this file knowing the bar's height. The backdrop then // starts below waybar too, which is why waybar stays un-dimmed. exclusionMode: ExclusionMode.Normal WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-mail" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive Rectangle { anchors.fill: parent color: "#000000" opacity: 0.5 MouseArea { anchors.fill: parent; onClicked: root.close() } } // Keys reach a focused item, never the window itself: setting // keyboardFocus above is necessary but not sufficient, and // Keys.onEscapePressed on a PanelWindow never fires. Item { anchors.fill: parent focus: true Keys.onEscapePressed: root.close() } // Top right, under the waybar icon, which sits in modules-right. Rectangle { id: drawer anchors { top: parent.top; right: parent.right; topMargin: 8; rightMargin: 8 } width: 460 height: Math.min(content.implicitHeight + 32, win.height - 24) radius: 14 color: Qt.alpha(Theme.base, 0.72) border.width: 1 border.color: Qt.alpha(Theme.text, 0.12) // Swallows clicks so they do not reach the backdrop and close // the drawer. MouseArea { anchors.fill: parent } Column { id: content anchors { left: parent.left; right: parent.right; top: parent.top; margins: 16 } spacing: 10 Item { width: parent.width implicitHeight: Math.max(title.implicitHeight, totalText.implicitHeight) Text { id: title anchors.verticalCenter: parent.verticalCenter text: "Mail" color: Theme.text font.family: Theme.fontFamily font.pixelSize: Theme.fontSize + 2 font.bold: true } Text { id: totalText anchors { right: parent.right; verticalCenter: parent.verticalCenter } // A dash rather than a possibly-wrong number while // any account is still unknown. text: Accounts.anyUnknown ? "—" : `${Accounts.total} unread` color: Theme.subtext font.family: Theme.fontFamily font.pixelSize: Theme.fontSize } } Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } Text { visible: Accounts.error !== "" width: parent.width text: Accounts.error color: Theme.red wrapMode: Text.WordWrap font.family: Theme.fontFamily font.pixelSize: Theme.fontSize - 2 } Repeater { model: Accounts.accounts Column { required property var modelData width: content.width spacing: 4 Item { width: parent.width implicitHeight: 26 Rectangle { id: dot anchors.verticalCenter: parent.verticalCenter width: 8; height: 8; radius: 4 // The account's own colour from the config. // Per-account identity, not a palette. color: modelData.color || Theme.accent } Text { anchors { left: dot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } text: modelData.label color: Theme.text font.family: Theme.fontFamily font.pixelSize: Theme.fontSize } Text { anchors { right: parent.right; verticalCenter: parent.verticalCenter } text: modelData.count < 0 ? "—" : String(modelData.count) color: modelData.count > 0 ? Theme.text : Theme.subtext font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.bold: modelData.count > 0 } } // The newest three unread threads. Read-only: // qtmaildir takes no arguments, so there is no way // to ask it for a particular thread. Repeater { model: modelData.threads Column { required property var modelData width: content.width - 18 x: 18 spacing: 1 bottomPadding: 4 Item { width: parent.width implicitHeight: who.implicitHeight Text { id: who anchors.left: parent.left width: parent.width - when.implicitWidth - 10 text: modelData.authors elide: Text.ElideRight color: Theme.subtext font.family: Theme.fontFamily font.pixelSize: Theme.fontSize - 3 } Text { id: when anchors.right: parent.right text: modelData.date color: Theme.overlay font.family: Theme.fontFamily font.pixelSize: Theme.fontSize - 3 } } Text { width: parent.width text: modelData.subject elide: Text.ElideRight color: Theme.text font.family: Theme.fontFamily font.pixelSize: Theme.fontSize - 2 } } } } } Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } Row { anchors.right: parent.right spacing: 10 Button { text: Accounts.syncing ? "Syncing..." : "Sync now" enabled: !Accounts.syncing onClicked: Accounts.sync() } Button { text: "Open qtmaildir" onClicked: { Accounts.openClient(); root.close(); } } } } } } } }