From 6e9d588f006c8d3efa0b4f70d2784c5fdb5075a6 Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Thu, 23 Jul 2026 23:22:03 +0200 Subject: [PATCH] terra: migrate quickshell config into repo, add quickshell + opencode packages Config was symlinked from ~/.dots/quickshell (separate dotfiles repo); now tracked here and applied via home-manager xdg.configFile. Co-Authored-By: Claude Sonnet 5 --- dotfiles/quickshell/CLAUDE.md | 40 + dotfiles/quickshell/assets/volume-active.svg | 4 + dotfiles/quickshell/assets/volume.svg | 4 + dotfiles/quickshell/shell.qml | 28 + dotfiles/quickshell/widgets/bar/BarBottom.qml | 80 ++ dotfiles/quickshell/widgets/bar/BarTop.qml | 80 ++ .../quickshell/widgets/decoration/Dummy.qml | 7 + .../quickshell/widgets/decoration/Slash.qml | 24 + .../widgets/decoration/TopLeftPanelShape.qml | 43 + .../quickshell/widgets/input/TextField.qml | 111 +++ .../quickshell/widgets/launcher/AppModel.qml | 44 + .../widgets/launcher/LauncherConsole.qml | 282 +++++++ .../widgets/launcher/LauncherCorner.qml | 797 ++++++++++++++++++ .../widgets/launcher/LauncherDock.qml | 288 +++++++ .../widgets/launcher/LauncherGrid.qml | 283 +++++++ .../widgets/launcher/LauncherSlant.qml | 677 +++++++++++++++ .../widgets/launcher/LauncherSpotlight.qml | 262 ++++++ .../widgets/launcher/LauncherStack.qml | 268 ++++++ .../widgets/launcher/LauncherState.qml | 14 + .../widgets/layout/HorizontalStack.qml | 16 + .../widgets/layout/VerticalStack.qml | 16 + .../notifications/NotificationPopup.qml | 119 +++ .../widgets/notifications/Notifications.qml | 79 ++ dotfiles/quickshell/widgets/osd/VolumeOsd.qml | 111 +++ .../quickshell/widgets/sidebar/SideBar.qml | 476 +++++++++++ .../quickshell/widgets/systray/SysTray.qml | 220 +++++ .../widgets/systray/SysTrayItem.qml | 65 ++ hosts/terra/home.nix | 3 + 28 files changed, 4441 insertions(+) create mode 100644 dotfiles/quickshell/CLAUDE.md create mode 100644 dotfiles/quickshell/assets/volume-active.svg create mode 100644 dotfiles/quickshell/assets/volume.svg create mode 100644 dotfiles/quickshell/shell.qml create mode 100644 dotfiles/quickshell/widgets/bar/BarBottom.qml create mode 100644 dotfiles/quickshell/widgets/bar/BarTop.qml create mode 100644 dotfiles/quickshell/widgets/decoration/Dummy.qml create mode 100644 dotfiles/quickshell/widgets/decoration/Slash.qml create mode 100644 dotfiles/quickshell/widgets/decoration/TopLeftPanelShape.qml create mode 100644 dotfiles/quickshell/widgets/input/TextField.qml create mode 100644 dotfiles/quickshell/widgets/launcher/AppModel.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherConsole.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherCorner.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherDock.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherGrid.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherSlant.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherSpotlight.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherStack.qml create mode 100644 dotfiles/quickshell/widgets/launcher/LauncherState.qml create mode 100644 dotfiles/quickshell/widgets/layout/HorizontalStack.qml create mode 100644 dotfiles/quickshell/widgets/layout/VerticalStack.qml create mode 100644 dotfiles/quickshell/widgets/notifications/NotificationPopup.qml create mode 100644 dotfiles/quickshell/widgets/notifications/Notifications.qml create mode 100644 dotfiles/quickshell/widgets/osd/VolumeOsd.qml create mode 100644 dotfiles/quickshell/widgets/sidebar/SideBar.qml create mode 100644 dotfiles/quickshell/widgets/systray/SysTray.qml create mode 100644 dotfiles/quickshell/widgets/systray/SysTrayItem.qml diff --git a/dotfiles/quickshell/CLAUDE.md b/dotfiles/quickshell/CLAUDE.md new file mode 100644 index 0000000..1e561d0 --- /dev/null +++ b/dotfiles/quickshell/CLAUDE.md @@ -0,0 +1,40 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A [Quickshell](https://quickshell.org/) configuration — a QML-based Wayland desktop shell (bar, launcher, tray, decorations) for a Hyprland/wlroots setup. `~/.config/quickshell` is a symlink to this repo, so Quickshell loads `shell.qml` here as the "default" config. + +## Running / testing changes + +```sh +qs # runs ~/.config/quickshell/shell.qml (this repo, since it's the symlinked default config) +qs -p . # run this directory explicitly regardless of symlink +qs -n # exit immediately if another instance is already running (use to avoid duplicate shells while iterating) +``` + +Quickshell hot-reloads QML on file save when already running, so for most edits just save and check the running instance rather than restarting `qs`. There is no separate build/lint/test tooling in this repo — verification is visual/behavioral via the running shell. `qmlls` (QML language server) is configured via `.qmlls.ini` for editor diagnostics. + +## Architecture + +`shell.qml` is the entry point: a `Scope` that instantiates the top-level pieces — `Bar`, `BarBottom`, `BarTop`, and a hidden `Launcher` — as siblings. Each top-level widget manages its own `PanelWindow`(s); there's no central layout manager. + +**Import convention**: QML modules are imported by their path under the repo root using the `qs.` namespace, e.g. `import qs.widgets.launcher`, `import qs.widgets.decoration`. Sibling files in the same directory are imported with a relative string import instead (e.g. `Bar.qml` does `import "modules"`). + +**Multi-monitor**: Bar/BarTop/BarBottom each wrap their `PanelWindow` in `Variants { model: Quickshell.screens }`, so one window instance is created per connected screen. `pragma ComponentBehavior: Bound` + `required property var modelData` is the standard pattern for these per-screen delegates. + +**Directory layout**: +- `widgets/bar/` — `Bar.qml` is the main sidebar (right-anchored, full height) hosting the module stack (date, clock, tray, decorative dividers); `BarTop.qml`/`BarBottom.qml` are thin accent-colored strips anchored to the top/bottom edges. +- `widgets/bar/modules/` — individual bar widgets (`Clock`, `Date`, `Tray`/`TrayItem`, `Volume`) built on the shared `BarWidget` base component. +- `widgets/launcher/` — app launcher panel (`Launcher` → `LauncherPanel` → `SearchBar`/`TextField`), currently a WIP skeleton (search box has no backing logic yet, `Launcher.qml` is instantiated with `visible: false`). +- `widgets/decoration/` — reusable QtQuick `Shape`-based visual accents (angled panel edges, slashes) used to give bar panels their non-rectangular look. `Dummy.qml` is a placeholder/test rectangle. +- `widgets/input/` — thin wrappers around `QtQuick.Controls` inputs (currently just `TextField`). +- `widgets/layout/` — `HorizontalStack`/`VerticalStack`: `RowLayout`/`ColumnLayout` wrappers that expose `default property alias content` for terser call sites, with a trailing filler `Item` that soaks up remaining space. +- `assets/` — SVG icons referenced via `file://${Quickshell.shellDir}/assets/...`. + +**Styling**: No shared theme/tokens file yet — colors (`#FFD063` accent, `#0F1012`/`#292C30` backgrounds, `#EEEEEE` text) and metrics are hardcoded per-component. When touching visuals, grep for the existing hex color across `widgets/` to keep new elements consistent rather than introducing new values. + +**Component base pattern**: `BarWidget.qml` (`widgets/bar/modules/BarWidget.qml`) is a `WrapperMouseArea` + `WrapperRectangle` combo providing hover-triggered border highlight (`Behavior on border.color` animation) and `Layout.fillWidth`. Bar modules extend it via `default property alias content` rather than duplicating the hover/border chrome. + +**Fonts**: Clock/Date use the `Digital-7 Mono` font family — expected to be installed system-wide (see sibling `~/.dots/fonts/digital_7`), not bundled in this repo. diff --git a/dotfiles/quickshell/assets/volume-active.svg b/dotfiles/quickshell/assets/volume-active.svg new file mode 100644 index 0000000..1418c1c --- /dev/null +++ b/dotfiles/quickshell/assets/volume-active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dotfiles/quickshell/assets/volume.svg b/dotfiles/quickshell/assets/volume.svg new file mode 100644 index 0000000..e09faff --- /dev/null +++ b/dotfiles/quickshell/assets/volume.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dotfiles/quickshell/shell.qml b/dotfiles/quickshell/shell.qml new file mode 100644 index 0000000..5a225a0 --- /dev/null +++ b/dotfiles/quickshell/shell.qml @@ -0,0 +1,28 @@ +//@ pragma UseQApplication + +import Quickshell +import qs.widgets.bar +import qs.widgets.launcher +import qs.widgets.notifications +import qs.widgets.osd +import qs.widgets.sidebar +import qs.widgets.systray + +Scope { + // Left sidebar in the Slant (V6) style — toggle with SUPER CTRL S. + SideBar {} + BarBottom {} + + // App launcher variants — toggled via Hyprland global shortcuts + // (SUPER CTRL 1/2/3). Try each and keep the one you like. + LauncherStack {} // 1 — left vertical list + LauncherGrid {} // 2 — centered icon grid + LauncherSpotlight {} // 3 — top-center command bar + LauncherConsole {} // 4 — full-width deck unfolding from the top bar + LauncherDock {} // 5 — deck rising from the bottom bar + LauncherSlant {} // 6 — angular / sheared panel + LauncherCorner {} // 7 — Slant (V6) copy + floating power panel (shutdown/reboot) + + Notifications {} + VolumeOsd {} +} diff --git a/dotfiles/quickshell/widgets/bar/BarBottom.qml b/dotfiles/quickshell/widgets/bar/BarBottom.qml new file mode 100644 index 0000000..414d52b --- /dev/null +++ b/dotfiles/quickshell/widgets/bar/BarBottom.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +import qs.widgets.launcher + +Scope { + id: root + + Variants { + model: Quickshell.screens + + PanelWindow { + required property var modelData + screen: modelData + + color: "transparent" + + anchors { + bottom: true + left: true + right: true + } + + implicitHeight: wrapper.implicitHeight + + WrapperRectangle { + id: wrapper + + color: "transparent" + + anchors.fill: parent + + leftMargin: 12 + rightMargin: 12 + bottomMargin: 12 + + Rectangle { + implicitHeight: 12 + + // Brightens when the Dock launcher (variant 5) opens. + color: LauncherState.dockOpen ? "#FFF3C0" : "#FFD063" + + Behavior on color { + ColorAnimation { + duration: 250 + } + } + + // Gentle "listening" pulse while the dock is open. + Rectangle { + anchors.fill: parent + color: "#FFFFFF" + opacity: 0 + visible: LauncherState.dockOpen + + SequentialAnimation on opacity { + running: LauncherState.dockOpen + loops: Animation.Infinite + + NumberAnimation { + to: 0.4 + duration: 700 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.0 + duration: 700 + easing.type: Easing.InOutSine + } + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/bar/BarTop.qml b/dotfiles/quickshell/widgets/bar/BarTop.qml new file mode 100644 index 0000000..6b8c3d6 --- /dev/null +++ b/dotfiles/quickshell/widgets/bar/BarTop.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Widgets +import QtQuick + +import qs.widgets.launcher + +Scope { + id: root + + Variants { + model: Quickshell.screens + + PanelWindow { + id: topBar + + required property var modelData + screen: modelData + + color: "transparent" + + anchors { + top: true + left: true + right: true + } + + implicitHeight: wrapper.implicitHeight + + WrapperRectangle { + id: wrapper + + color: "transparent" + + anchors.fill: parent + + margin: 12 + bottomMargin: 0 + + Rectangle { + implicitHeight: 6 + + // Brightens when the Console launcher (variant 4) opens. + color: LauncherState.consoleOpen ? "#FFF3C0" : "#FFD063" + + Behavior on color { + ColorAnimation { + duration: 250 + } + } + + // Gentle "listening" pulse while the console is open. + Rectangle { + anchors.fill: parent + color: "#FFFFFF" + opacity: 0 + visible: LauncherState.consoleOpen + + SequentialAnimation on opacity { + running: LauncherState.consoleOpen + loops: Animation.Infinite + + NumberAnimation { + to: 0.4 + duration: 700 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.0 + duration: 700 + easing.type: Easing.InOutSine + } + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/decoration/Dummy.qml b/dotfiles/quickshell/widgets/decoration/Dummy.qml new file mode 100644 index 0000000..e5fab85 --- /dev/null +++ b/dotfiles/quickshell/widgets/decoration/Dummy.qml @@ -0,0 +1,7 @@ +import QtQuick + +Rectangle { + width: 10 + height: 10 + color: "red" +} diff --git a/dotfiles/quickshell/widgets/decoration/Slash.qml b/dotfiles/quickshell/widgets/decoration/Slash.qml new file mode 100644 index 0000000..49412bf --- /dev/null +++ b/dotfiles/quickshell/widgets/decoration/Slash.qml @@ -0,0 +1,24 @@ +import QtQuick +import QtQuick.Shapes + +Item { + Shape { + id: shape + + anchors.fill: parent + + preferredRendererType: Shape.CurveRenderer + ShapePath { + strokeWidth: 4 + strokeColor: "black" + + startX: shape.width + startY: 0 + + PathLine { + x: 0 + y: shape.height + } + } + } +} diff --git a/dotfiles/quickshell/widgets/decoration/TopLeftPanelShape.qml b/dotfiles/quickshell/widgets/decoration/TopLeftPanelShape.qml new file mode 100644 index 0000000..0d991ab --- /dev/null +++ b/dotfiles/quickshell/widgets/decoration/TopLeftPanelShape.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Shapes + +Shape { + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "#FFD063" + strokeWidth: 0 + + startX: 0 + startY: 0 + + PathLine { + x: parent.width + y: 0 + } + + PathLine { + x: parent.width - 15 + y: 38 + } + + PathLine { + x: parent.width - 15 + y: parent.height - 15 + } + + PathLine { + x: parent.width - 30 + y: parent.height + } + + PathLine { + x: 0 + y: parent.height + } + + PathLine { + x: 0 + y: 0 + } + } +} diff --git a/dotfiles/quickshell/widgets/input/TextField.qml b/dotfiles/quickshell/widgets/input/TextField.qml new file mode 100644 index 0000000..629d018 --- /dev/null +++ b/dotfiles/quickshell/widgets/input/TextField.qml @@ -0,0 +1,111 @@ +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +import qs.widgets.decoration + +WrapperItem { + RowLayout { + spacing: 6 + + Item { + id: container + + Layout.fillWidth: true + + implicitHeight: 22 + + Shape { + id: shape + anchors.fill: container + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + fillColor: "#0F1012" + + startX: 8 + startY: 0 + + PathLine { + x: 12 + y: 0 + } + + PathLine { + x: 4 + y: shape.height + } + + PathLine { + x: 0 + y: shape.height + } + + PathLine { + x: 8 + y: 0 + } + } + + ShapePath { + strokeWidth: 0 + fillColor: "#0F1012" + + startX: 16 + startY: 0 + + PathLine { + x: 20 + y: 0 + } + + PathLine { + x: 12 + y: shape.height + } + + PathLine { + x: 8 + y: shape.height + } + + PathLine { + x: 16 + y: 0 + } + } + + ShapePath { + startX: 24 + startY: 0 + + strokeWidth: 0 + fillColor: "#0F1012" + + PathLine { + x: shape.width + y: 0 + } + + PathLine { + x: shape.width - 8 + y: shape.height + } + + PathLine { + x: 16 + y: shape.height + } + + PathLine { + x: 24 + y: 0 + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/AppModel.qml b/dotfiles/quickshell/widgets/launcher/AppModel.qml new file mode 100644 index 0000000..2acbdfc --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/AppModel.qml @@ -0,0 +1,44 @@ +import Quickshell +import QtQuick + +// Non-visual, reusable app-search model shared by every launcher variant. +// Set `search`; read `apps` (a ranked, filtered list of DesktopEntry). +QtObject { + id: root + + property string search: "" + + // `keywords`/`categories` come through as string lists, so coerce every + // field to a string before matching (String([]) joins with commas). + function haystack(a) { + return (String(a.name || "") + " " + String(a.genericName || "") + " " + String(a.comment || "") + " " + String(a.keywords || "")).toLowerCase(); + } + + readonly property var apps: { + const all = DesktopEntries.applications.values.filter(a => !a.noDisplay); + const q = root.search.trim().toLowerCase(); + + if (q.length === 0) + return all.slice().sort((x, y) => String(x.name).localeCompare(String(y.name))); + + const matches = all.filter(a => root.haystack(a).includes(q)); + + // Prefix matches on the visible name rank first, then alphabetical. + return matches.slice().sort((x, y) => { + const xs = String(x.name).toLowerCase().startsWith(q) ? 0 : 1; + const ys = String(y.name).toLowerCase().startsWith(q) ? 0 : 1; + if (xs !== ys) + return xs - ys; + return String(x.name).localeCompare(String(y.name)); + }); + } + + function launch(index) { + const list = root.apps; + if (index >= 0 && index < list.length) { + list[index].execute(); + return true; + } + return false; + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherConsole.qml b/dotfiles/quickshell/widgets/launcher/LauncherConsole.qml new file mode 100644 index 0000000..ccd113c --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherConsole.qml @@ -0,0 +1,282 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +// Variant 4 — "Console": a full-width command deck that unfolds down out of +// the top bar, with a horizontal carousel of app cards. While open it drives +// LauncherState.consoleOpen, which the top bar reacts to (brighten + pulse). +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + // The top bar observes this to energize itself while the console is open. + onActiveChanged: LauncherState.consoleOpen = root.active + + GlobalShortcut { + name: "launcher4" + description: "Toggle app launcher (Console)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + // Stay mapped while the collapse animation plays out. + visible: root.active || panel.height > 1 + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: root.active ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + + color: "transparent" + + anchors { + top: true + left: true + right: true + } + + // Sit just below the top bar strip (12px margin + 6px strip + gap). + margins { + top: 22 + left: 12 + right: 12 + } + + implicitHeight: 210 + + readonly property int openHeight: 200 + + property int sel: 0 + onSelChanged: carousel.positionViewAtIndex(sel, ListView.Contain) + + onVisibleChanged: { + if (visible && root.active) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + sel = Math.max(0, Math.min(n - 1, sel + delta)); + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + // The console body, unfolding downward from the top bar. + Rectangle { + id: panel + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + clip: true + color: "#0F1012" + + height: root.active ? win.openHeight : 0 + + Behavior on height { + NumberAnimation { + duration: 220 + easing.type: root.active ? Easing.OutCubic : Easing.InCubic + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Accent lid — mirrors the top bar strip, reads as its extension. + Rectangle { + color: "#FFD063" + implicitHeight: 4 + Layout.fillWidth: true + } + + // Search / command line + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 18 + Layout.rightMargin: 18 + Layout.topMargin: 12 + Layout.bottomMargin: 10 + spacing: 14 + + Text { + text: ">_" + color: "#FFD063" + font.family: "Digital-7 Mono" + font.pointSize: 22 + font.bold: true + } + + Item { + Layout.fillWidth: true + implicitHeight: 30 + + TextInput { + id: input + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.pointSize: 16 + clip: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Right: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Left: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + onTextChanged: win.sel = 0 + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: input.text.length === 0 + text: "launch application…" + color: "#7A7B7D" + font: input.font + } + } + } + + Text { + text: model.apps.length + " apps" + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 15 + } + } + + Rectangle { + color: "#292C30" + implicitHeight: 1 + Layout.fillWidth: true + } + + // Horizontal carousel of app cards. + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: carousel + anchors.fill: parent + anchors.margins: 12 + + orientation: ListView.Horizontal + clip: true + model: model.apps + currentIndex: win.sel + spacing: 10 + boundsBehavior: Flickable.StopAtBounds + + delegate: MouseArea { + id: card + required property int index + required property var modelData + + width: 108 + height: carousel.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + Rectangle { + anchors.fill: parent + color: card.selected ? "#292C30" : "transparent" + border.width: 1 + border.color: card.selected ? "#FFD063" : "#292C30" + + Behavior on border.color { + ColorAnimation { + duration: 120 + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + IconImage { + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + implicitSize: 46 + source: Quickshell.iconPath(card.modelData.icon, "application-x-executable") + } + + Text { + Layout.fillWidth: true + text: card.modelData.name + color: card.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 9 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.Wrap + } + } + } + } + } + + Text { + anchors.centerIn: parent + visible: model.apps.length === 0 + text: "no matches" + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 16 + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherCorner.qml b/dotfiles/quickshell/widgets/launcher/LauncherCorner.qml new file mode 100644 index 0000000..b53ca8d --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherCorner.qml @@ -0,0 +1,797 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes + +// Variant 7 — a copy of the "Slant" (V6) launcher, plus a small floating +// power panel docked to its right with Shutdown / Reboot buttons. +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + GlobalShortcut { + name: "launcher7" + description: "Toggle app launcher (Slant copy + power panel)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + // Ignore other panels' exclusive zones so the full-screen dim + // backdrop below isn't shrunk away from the bar strips. + exclusionMode: ExclusionMode.Ignore + + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + property int sel: 0 + onSelChanged: list.positionViewAtIndex(sel, ListView.Contain) + + onVisibleChanged: { + if (visible) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + sel = (sel + delta + n) % n; + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + // Dim backdrop — click to dismiss. + Rectangle { + anchors.fill: parent + color: "#0A0A0C" + opacity: 0.55 + + MouseArea { + anchors.fill: parent + onClicked: root.active = false + } + } + + Item { + id: frame + anchors.centerIn: parent + width: 620 + height: 470 + + readonly property int chamfer: 18 + readonly property int bevel: 4 // inward thickness of the bevel borders + readonly property int chunkThick: 8 // outward thickness of the heavy border chunks + readonly property int chunkSlant: 12 // slant of their end pieces + readonly property int capSize: 10 // floating triangle cap in the bottom-right notch + readonly property int smallChamfer: 6 // small bevel on the top-right & bottom-left corners + + // Chamfered panel: rectangle with the top-left and bottom-right + // corners cut off, filled dark with an accent edge. + Shape { + id: panelShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "#0F1012" + strokeColor: "#FFD063" + strokeWidth: 2 + + startX: frame.chamfer + startY: 0 + // top edge to the small top-right bevel + PathLine { + x: panelShape.width - frame.smallChamfer + y: 0 + } + PathLine { + x: panelShape.width + y: frame.smallChamfer + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer + } + PathLine { + x: panelShape.width - frame.chamfer + y: panelShape.height + } + // bottom edge to the small bottom-left bevel + PathLine { + x: frame.smallChamfer + y: panelShape.height + } + PathLine { + x: 0 + y: panelShape.height - frame.smallChamfer + } + PathLine { + x: 0 + y: frame.chamfer + } + PathLine { + x: frame.chamfer + y: 0 + } + } + + // Thick top-left bevel border — a trapezoid whose inner ends + // run out to the top and left edges to meet the straight borders. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: 0 + startY: frame.chamfer + PathLine { + x: frame.chamfer + y: 0 + } + PathLine { + x: frame.chamfer + 2 * frame.bevel + y: 0 + } + PathLine { + x: 0 + y: frame.chamfer + 2 * frame.bevel + } + PathLine { + x: 0 + y: frame.chamfer + } + } + + // Thick bottom-right bevel border — trapezoid running out to the + // bottom and right edges. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: panelShape.width + startY: panelShape.height - frame.chamfer + PathLine { + x: panelShape.width - frame.chamfer + y: panelShape.height + } + PathLine { + x: panelShape.width - frame.chamfer - 2 * frame.bevel + y: panelShape.height + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer - 2 * frame.bevel + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer + } + } + + // Floating triangle cap sitting in the bottom-right bevel notch, + // detached from the panel by the width of the cut. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: panelShape.width + startY: panelShape.height + PathLine { + x: panelShape.width + y: panelShape.height - frame.capSize + } + PathLine { + x: panelShape.width - frame.capSize + y: panelShape.height + } + PathLine { + x: panelShape.width + y: panelShape.height + } + } + + // Heavy outward chunk wrapping the bottom-left corner: a third of + // the way across the bottom edge and 40px up the left edge, with + // slanted end pieces on both arms. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + // inner, right end of the bottom arm + startX: panelShape.width / 3 + startY: panelShape.height + // outer, slanted right end of the bottom arm + PathLine { + x: panelShape.width / 3 - frame.chunkSlant + y: panelShape.height + frame.chunkThick + } + // outer bevel across the corner + PathLine { + x: frame.smallChamfer + y: panelShape.height + frame.chunkThick + } + PathLine { + x: -frame.chunkThick + y: panelShape.height - frame.smallChamfer + } + // outer, slanted top end of the left arm + PathLine { + x: -frame.chunkThick + y: panelShape.height - panelShape.height / 7 + frame.chunkSlant + } + // inner, top end of the left arm + PathLine { + x: 0 + y: panelShape.height - panelShape.height / 7 + } + // inner bevel across the corner, then back along the bottom edge + PathLine { + x: 0 + y: panelShape.height - frame.smallChamfer + } + PathLine { + x: frame.smallChamfer + y: panelShape.height + } + PathLine { + x: panelShape.width / 3 + y: panelShape.height + } + } + + // Heavy outward chunk wrapping the top-right corner: down a third + // of the right edge and a fifth back along the top edge, with + // slanted end pieces on both arms. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + // inner, bottom of the right arm + startX: panelShape.width + startY: panelShape.height / 3 + // outer, slanted bottom end of the right arm + PathLine { + x: panelShape.width + frame.chunkThick + y: panelShape.height / 3 - frame.chunkSlant + } + // outer bevel across the corner + PathLine { + x: panelShape.width + frame.chunkThick + y: frame.smallChamfer + } + PathLine { + x: panelShape.width - frame.smallChamfer + y: -frame.chunkThick + } + // outer, slanted left end of the top arm + PathLine { + x: panelShape.width - panelShape.width / 5 + frame.chunkSlant + y: -frame.chunkThick + } + // inner, left end of the top arm + PathLine { + x: panelShape.width - panelShape.width / 5 + y: 0 + } + // inner bevel across the corner, then back down the right edge + PathLine { + x: panelShape.width - frame.smallChamfer + y: 0 + } + PathLine { + x: panelShape.width + y: frame.smallChamfer + } + PathLine { + x: panelShape.width + y: panelShape.height / 3 + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: 28 + anchors.bottomMargin: 28 + anchors.leftMargin: 30 + anchors.rightMargin: 26 + spacing: 14 + + // Header: slash-trio deco + prompt + count + RowLayout { + Layout.fillWidth: true + spacing: 12 + + // Slanted slash trio, echoing the SearchBar motif. + Shape { + id: slashes + implicitWidth: 26 + implicitHeight: 22 + Layout.alignment: Qt.AlignVCenter + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 6 + startY: 0 + PathLine { + x: 10 + y: 0 + } + PathLine { + x: 4 + y: slashes.height + } + PathLine { + x: 0 + y: slashes.height + } + PathLine { + x: 6 + y: 0 + } + } + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 15 + startY: 0 + PathLine { + x: 19 + y: 0 + } + PathLine { + x: 13 + y: slashes.height + } + PathLine { + x: 9 + y: slashes.height + } + PathLine { + x: 15 + y: 0 + } + } + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 24 + startY: 0 + PathLine { + x: 28 + y: 0 + } + PathLine { + x: 22 + y: slashes.height + } + PathLine { + x: 18 + y: slashes.height + } + PathLine { + x: 24 + y: 0 + } + } + } + + Item { + Layout.fillWidth: true + implicitHeight: 30 + + TextInput { + id: input + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.family: "Digital-7 Mono" + font.pointSize: 20 + font.letterSpacing: 1 + clip: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Up: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + onTextChanged: win.sel = 0 + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: input.text.length === 0 + text: "run" + color: "#7A7B7D" + font: input.font + } + } + } + + Text { + text: model.apps.length + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 18 + } + } + + // Slanted divider. + Item { + Layout.fillWidth: true + implicitHeight: 4 + + Shape { + id: divider + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + readonly property int slant: 2 + readonly property int chunk: 6 + readonly property int chunkSlant: 6 + readonly property int leftChunkLength: 2 + readonly property int rightChunkLength: 7 + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: divider.slant + startY: 0 + PathLine { + x: divider.width + y: 0 + } + PathLine { + x: divider.width - divider.slant + y: divider.height + } + PathLine { + x: 0 + y: divider.height + } + PathLine { + x: divider.slant + y: 0 + } + } + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 0 + startY: divider.height + PathLine { + x: divider.width / divider.leftChunkLength + y: divider.height + } + PathLine { + x: divider.width / divider.leftChunkLength - divider.chunkSlant + y: divider.height + divider.chunk + } + PathLine { + x: 0 + y: divider.height + divider.chunk + } + PathLine { + x: 0 + y: divider.height + } + } + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: divider.width + startY: 0 + PathLine { + x: divider.width - divider.width / divider.rightChunkLength + y: 0 + } + PathLine { + x: divider.width - divider.width / divider.rightChunkLength + divider.chunkSlant + y: -divider.chunk + } + PathLine { + x: divider.width + y: -divider.chunk + } + PathLine { + x: divider.width + y: 0 + } + } + } + } + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + model: model.apps + currentIndex: win.sel + spacing: 3 + boundsBehavior: Flickable.StopAtBounds + + delegate: MouseArea { + id: appRow + required property int index + required property var modelData + + readonly property int shear: 14 + + width: ListView.view.width + implicitHeight: 42 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + // Sheared (parallelogram) selection highlight — the + // rows are anything but rectangular when active. + Shape { + id: rowShape + anchors.fill: parent + visible: appRow.selected + preferredRendererType: Shape.CurveRenderer + + // Body + ShapePath { + strokeWidth: 0 + fillColor: "#22262C" + startX: appRow.shear + startY: 0 + PathLine { + x: rowShape.width + y: 0 + } + PathLine { + x: rowShape.width - appRow.shear + y: rowShape.height + } + PathLine { + x: 0 + y: rowShape.height + } + PathLine { + x: appRow.shear + y: 0 + } + } + // Left accent edge + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: appRow.shear + startY: 0 + PathLine { + x: appRow.shear + 5 + y: 0 + } + PathLine { + x: 5 + y: rowShape.height + } + PathLine { + x: 0 + y: rowShape.height + } + PathLine { + x: appRow.shear + y: 0 + } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: appRow.shear + 16 + anchors.rightMargin: 12 + spacing: 12 + + IconImage { + implicitSize: 28 + source: Quickshell.iconPath(appRow.modelData.icon, "application-x-executable") + } + + Text { + text: appRow.modelData.name + color: appRow.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: appRow.modelData.genericName || "" + color: "#7A7B7D" + font.pointSize: 10 + elide: Text.ElideRight + Layout.maximumWidth: 220 + } + } + } + } + } + } + + // ── Power panel ──────────────────────────────────────────────────── + // Small floating panel docked to the right of the main frame, with + // Shutdown / Reboot buttons. Same chamfered-panel idiom as the main + // frame, kept plain since it's a small secondary surface. + Item { + id: powerPanel + anchors.left: frame.right + anchors.leftMargin: 18 + anchors.top: frame.top + anchors.topMargin: frame.chamfer / 2 + width: 132 + height: powerColumn.implicitHeight + 32 + + readonly property int chamfer: 18 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "#FFD063" + strokeColor: "#FFD063" + strokeWidth: 2 + + startX: 0 + startY: 0 + PathLine { + x: powerPanel.width + y: 0 + } + PathLine { + x: powerPanel.width + y: powerPanel.height - powerPanel.chamfer + } + PathLine { + x: powerPanel.width - powerPanel.chamfer + y: powerPanel.height + } + PathLine { + x: 0 + y: powerPanel.height + } + PathLine { + x: 0 + y: 0 + } + } + } + + ColumnLayout { + id: powerColumn + anchors.centerIn: parent + width: parent.width - 24 + spacing: 10 + + Text { + Layout.alignment: Qt.AlignHCenter + text: "POWER" + font.family: "Digital-7 Mono" + font.pointSize: 11 + color: "#0F1012" + } + + Repeater { + model: [ + { label: "SHUTDOWN", command: ["systemctl", "poweroff"] }, + { label: "REBOOT", command: ["systemctl", "reboot"] } + ] + + delegate: MouseArea { + id: powerButton + required property var modelData + + Layout.fillWidth: true + implicitHeight: 34 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: Quickshell.execDetached(modelData.command) + + readonly property int chamfer: 6 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 1 + strokeColor: powerButton.containsMouse ? "#FFD063" : "#7A7B7D" + fillColor: "#292C30" + + startX: powerButton.chamfer + startY: 0 + PathLine { x: powerButton.width; y: 0 } + PathLine { x: powerButton.width; y: powerButton.height - powerButton.chamfer } + PathLine { x: powerButton.width - powerButton.chamfer; y: powerButton.height } + PathLine { x: 0; y: powerButton.height } + PathLine { x: 0; y: powerButton.chamfer } + PathLine { x: powerButton.chamfer; y: 0 } + + Behavior on strokeColor { + ColorAnimation { + duration: 150 + } + } + } + } + + Text { + anchors.centerIn: parent + text: powerButton.modelData.label + color: powerButton.containsMouse ? "#FFD063" : "#EEEEEE" + font.family: "Digital-7 Mono" + font.pointSize: 11 + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherDock.qml b/dotfiles/quickshell/widgets/launcher/LauncherDock.qml new file mode 100644 index 0000000..394d62b --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherDock.qml @@ -0,0 +1,288 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +// Variant 5 — "Dock": the Console concept mirrored to the bottom edge. A +// full-width deck rises up out of the bottom bar, which energizes via +// LauncherState.dockOpen while it is open. +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + onActiveChanged: LauncherState.dockOpen = root.active + + GlobalShortcut { + name: "launcher5" + description: "Toggle app launcher (Dock)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active || panel.height > 1 + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: root.active ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + + color: "transparent" + + anchors { + bottom: true + left: true + right: true + } + + // Sit just above the bottom bar strip (12px margin + 6px strip + gap). + margins { + bottom: 22 + left: 12 + right: 12 + } + + implicitHeight: 210 + + readonly property int openHeight: 200 + + property int sel: 0 + onSelChanged: carousel.positionViewAtIndex(sel, ListView.Contain) + + onVisibleChanged: { + if (visible && root.active) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + sel = Math.max(0, Math.min(n - 1, sel + delta)); + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + // The deck, rising up from the bottom bar. Content is pinned to the + // bottom at full height and revealed by the clipping panel as it grows. + Rectangle { + id: panel + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + clip: true + color: "#0F1012" + + height: root.active ? win.openHeight : 0 + + Behavior on height { + NumberAnimation { + duration: 220 + easing.type: root.active ? Easing.OutCubic : Easing.InCubic + } + } + + Item { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: win.openHeight + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Horizontal carousel of app cards. + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: carousel + anchors.fill: parent + anchors.margins: 12 + + orientation: ListView.Horizontal + clip: true + model: model.apps + currentIndex: win.sel + spacing: 10 + boundsBehavior: Flickable.StopAtBounds + + delegate: MouseArea { + id: card + required property int index + required property var modelData + + width: 108 + height: carousel.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + Rectangle { + anchors.fill: parent + color: card.selected ? "#292C30" : "transparent" + border.width: 1 + border.color: card.selected ? "#FFD063" : "#292C30" + + Behavior on border.color { + ColorAnimation { + duration: 120 + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + IconImage { + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + implicitSize: 46 + source: Quickshell.iconPath(card.modelData.icon, "application-x-executable") + } + + Text { + Layout.fillWidth: true + text: card.modelData.name + color: card.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 9 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.Wrap + } + } + } + } + } + + Text { + anchors.centerIn: parent + visible: model.apps.length === 0 + text: "no matches" + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 16 + } + } + + Rectangle { + color: "#292C30" + implicitHeight: 1 + Layout.fillWidth: true + } + + // Search / command line, sitting next to the bottom bar. + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 18 + Layout.rightMargin: 18 + Layout.topMargin: 10 + Layout.bottomMargin: 12 + spacing: 14 + + Text { + text: ">_" + color: "#FFD063" + font.family: "Digital-7 Mono" + font.pointSize: 22 + font.bold: true + } + + Item { + Layout.fillWidth: true + implicitHeight: 30 + + TextInput { + id: input + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.pointSize: 16 + clip: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Right: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Left: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + onTextChanged: win.sel = 0 + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: input.text.length === 0 + text: "launch application…" + color: "#7A7B7D" + font: input.font + } + } + } + + Text { + text: model.apps.length + " apps" + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 15 + } + } + + // Accent lid — mirrors the bottom bar strip. + Rectangle { + color: "#FFD063" + implicitHeight: 4 + Layout.fillWidth: true + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherGrid.qml b/dotfiles/quickshell/widgets/launcher/LauncherGrid.qml new file mode 100644 index 0000000..ec54629 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherGrid.qml @@ -0,0 +1,283 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +// Variant 2 — "Grid": centered app-drawer with a dim backdrop and icon tiles. +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + GlobalShortcut { + name: "launcher2" + description: "Toggle app launcher (Grid)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + // Ignore other panels' exclusive zones so the full-screen dim + // backdrop below isn't shrunk away from the bar strips. + exclusionMode: ExclusionMode.Ignore + + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + property int sel: 0 + + onVisibleChanged: { + if (visible) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + const next = win.sel + delta; + if (next < 0 || next >= n) + return; + win.sel = next; + grid.positionViewAtIndex(next, GridView.Contain); + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + // Dim backdrop — click anywhere outside to dismiss. + Rectangle { + anchors.fill: parent + color: "#0A0A0C" + opacity: 0.55 + + MouseArea { + anchors.fill: parent + onClicked: root.active = false + } + } + + Rectangle { + id: panel + anchors.centerIn: parent + width: 760 + height: 560 + + color: "#0F1012" + border.width: 1 + border.color: "#FFD063" + opacity: 0.98 + + // Accent corner brackets for the cyberpunk frame. + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + width: 5 + height: 28 + color: "#FFD063" + } + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + width: 28 + height: 5 + color: "#FFD063" + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 14 + + // Search header + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Text { + text: "APPS" + color: "#FFD063" + font.family: "Digital-7 Mono" + font.pointSize: 22 + font.letterSpacing: 3 + } + + Item { + Layout.fillWidth: true + implicitHeight: 34 + + Rectangle { + anchors.fill: parent + color: "#292C30" + border.width: 1 + border.color: input.activeFocus ? "#FFD063" : "#7A7B7D" + + Behavior on border.color { + ColorAnimation { + duration: 150 + } + } + + TextInput { + id: input + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.pointSize: 13 + clip: true + + Keys.onPressed: event => { + const cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)); + switch (event.key) { + case Qt.Key_Right: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Left: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Down: + win.move(cols); + event.accepted = true; + break; + case Qt.Key_Up: + win.move(-cols); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + onTextChanged: win.sel = 0 + + Text { + anchors.verticalCenter: parent.verticalCenter + visible: input.text.length === 0 + text: "Type to search…" + color: "#7A7B7D" + font: input.font + } + } + } + } + } + + Rectangle { + color: "#FFD063" + implicitHeight: 3 + Layout.fillWidth: true + } + + GridView { + id: grid + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + model: model.apps + currentIndex: win.sel + cellWidth: 140 + cellHeight: 128 + boundsBehavior: Flickable.StopAtBounds + + delegate: MouseArea { + id: tile + required property int index + required property var modelData + + width: grid.cellWidth + height: grid.cellHeight + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + Rectangle { + anchors.fill: parent + anchors.margins: 6 + color: tile.selected ? "#292C30" : "transparent" + border.width: 1 + border.color: tile.selected ? "#FFD063" : "transparent" + + Behavior on border.color { + ColorAnimation { + duration: 120 + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + IconImage { + Layout.alignment: Qt.AlignHCenter + implicitSize: 52 + source: Quickshell.iconPath(tile.modelData.icon, "application-x-executable") + } + + Text { + Layout.fillWidth: true + text: tile.modelData.name + color: tile.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 10 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.Wrap + } + } + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherSlant.qml b/dotfiles/quickshell/widgets/launcher/LauncherSlant.qml new file mode 100644 index 0000000..8445fd6 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherSlant.qml @@ -0,0 +1,677 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes + +// Variant 6 — "Slant": breaks up the rectangular layout with angled geometry — +// a chamfered panel, a slanted header divider and sheared row highlights. +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + GlobalShortcut { + name: "launcher6" + description: "Toggle app launcher (Slant)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + // Ignore other panels' exclusive zones so the full-screen dim + // backdrop below isn't shrunk away from the bar strips. + exclusionMode: ExclusionMode.Ignore + + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + property int sel: 0 + onSelChanged: list.positionViewAtIndex(sel, ListView.Contain) + + onVisibleChanged: { + if (visible) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + sel = (sel + delta + n) % n; + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + // Dim backdrop — click to dismiss. + Rectangle { + anchors.fill: parent + color: "#0A0A0C" + opacity: 0.55 + + MouseArea { + anchors.fill: parent + onClicked: root.active = false + } + } + + Item { + id: frame + anchors.centerIn: parent + width: 620 + height: 470 + + readonly property int chamfer: 18 + readonly property int bevel: 4 // inward thickness of the bevel borders + readonly property int chunkThick: 8 // outward thickness of the heavy border chunks + readonly property int chunkSlant: 12 // slant of their end pieces + readonly property int capSize: 10 // floating triangle cap in the bottom-right notch + readonly property int smallChamfer: 6 // small bevel on the top-right & bottom-left corners + + // Chamfered panel: rectangle with the top-left and bottom-right + // corners cut off, filled dark with an accent edge. + Shape { + id: panelShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "#0F1012" + strokeColor: "#FFD063" + strokeWidth: 2 + + startX: frame.chamfer + startY: 0 + // top edge to the small top-right bevel + PathLine { + x: panelShape.width - frame.smallChamfer + y: 0 + } + PathLine { + x: panelShape.width + y: frame.smallChamfer + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer + } + PathLine { + x: panelShape.width - frame.chamfer + y: panelShape.height + } + // bottom edge to the small bottom-left bevel + PathLine { + x: frame.smallChamfer + y: panelShape.height + } + PathLine { + x: 0 + y: panelShape.height - frame.smallChamfer + } + PathLine { + x: 0 + y: frame.chamfer + } + PathLine { + x: frame.chamfer + y: 0 + } + } + + // Thick top-left bevel border — a trapezoid whose inner ends + // run out to the top and left edges to meet the straight borders. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: 0 + startY: frame.chamfer + PathLine { + x: frame.chamfer + y: 0 + } + PathLine { + x: frame.chamfer + 2 * frame.bevel + y: 0 + } + PathLine { + x: 0 + y: frame.chamfer + 2 * frame.bevel + } + PathLine { + x: 0 + y: frame.chamfer + } + } + + // Thick bottom-right bevel border — trapezoid running out to the + // bottom and right edges. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: panelShape.width + startY: panelShape.height - frame.chamfer + PathLine { + x: panelShape.width - frame.chamfer + y: panelShape.height + } + PathLine { + x: panelShape.width - frame.chamfer - 2 * frame.bevel + y: panelShape.height + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer - 2 * frame.bevel + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer + } + } + + // Floating triangle cap sitting in the bottom-right bevel notch, + // detached from the panel by the width of the cut. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: panelShape.width + startY: panelShape.height + PathLine { + x: panelShape.width + y: panelShape.height - frame.capSize + } + PathLine { + x: panelShape.width - frame.capSize + y: panelShape.height + } + PathLine { + x: panelShape.width + y: panelShape.height + } + } + + // Heavy outward chunk wrapping the bottom-left corner: a third of + // the way across the bottom edge and 40px up the left edge, with + // slanted end pieces on both arms. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + // inner, right end of the bottom arm + startX: panelShape.width / 3 + startY: panelShape.height + // outer, slanted right end of the bottom arm + PathLine { + x: panelShape.width / 3 - frame.chunkSlant + y: panelShape.height + frame.chunkThick + } + // outer bevel across the corner + PathLine { + x: frame.smallChamfer + y: panelShape.height + frame.chunkThick + } + PathLine { + x: -frame.chunkThick + y: panelShape.height - frame.smallChamfer + } + // outer, slanted top end of the left arm + PathLine { + x: -frame.chunkThick + y: panelShape.height - panelShape.height / 7 + frame.chunkSlant + } + // inner, top end of the left arm + PathLine { + x: 0 + y: panelShape.height - panelShape.height / 7 + } + // inner bevel across the corner, then back along the bottom edge + PathLine { + x: 0 + y: panelShape.height - frame.smallChamfer + } + PathLine { + x: frame.smallChamfer + y: panelShape.height + } + PathLine { + x: panelShape.width / 3 + y: panelShape.height + } + } + + // Heavy outward chunk wrapping the top-right corner: down a third + // of the right edge and a fifth back along the top edge, with + // slanted end pieces on both arms. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + // inner, bottom of the right arm + startX: panelShape.width + startY: panelShape.height / 3 + // outer, slanted bottom end of the right arm + PathLine { + x: panelShape.width + frame.chunkThick + y: panelShape.height / 3 - frame.chunkSlant + } + // outer bevel across the corner + PathLine { + x: panelShape.width + frame.chunkThick + y: frame.smallChamfer + } + PathLine { + x: panelShape.width - frame.smallChamfer + y: -frame.chunkThick + } + // outer, slanted left end of the top arm + PathLine { + x: panelShape.width - panelShape.width / 5 + frame.chunkSlant + y: -frame.chunkThick + } + // inner, left end of the top arm + PathLine { + x: panelShape.width - panelShape.width / 5 + y: 0 + } + // inner bevel across the corner, then back down the right edge + PathLine { + x: panelShape.width - frame.smallChamfer + y: 0 + } + PathLine { + x: panelShape.width + y: frame.smallChamfer + } + PathLine { + x: panelShape.width + y: panelShape.height / 3 + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: 28 + anchors.bottomMargin: 28 + anchors.leftMargin: 30 + anchors.rightMargin: 26 + spacing: 14 + + // Header: slash-trio deco + prompt + count + RowLayout { + Layout.fillWidth: true + spacing: 12 + + // Slanted slash trio, echoing the SearchBar motif. + Shape { + id: slashes + implicitWidth: 26 + implicitHeight: 22 + Layout.alignment: Qt.AlignVCenter + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 6 + startY: 0 + PathLine { + x: 10 + y: 0 + } + PathLine { + x: 4 + y: slashes.height + } + PathLine { + x: 0 + y: slashes.height + } + PathLine { + x: 6 + y: 0 + } + } + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 15 + startY: 0 + PathLine { + x: 19 + y: 0 + } + PathLine { + x: 13 + y: slashes.height + } + PathLine { + x: 9 + y: slashes.height + } + PathLine { + x: 15 + y: 0 + } + } + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 24 + startY: 0 + PathLine { + x: 28 + y: 0 + } + PathLine { + x: 22 + y: slashes.height + } + PathLine { + x: 18 + y: slashes.height + } + PathLine { + x: 24 + y: 0 + } + } + } + + Item { + Layout.fillWidth: true + implicitHeight: 30 + + TextInput { + id: input + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.family: "Digital-7 Mono" + font.pointSize: 20 + font.letterSpacing: 1 + clip: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Up: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + onTextChanged: win.sel = 0 + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: input.text.length === 0 + text: "run" + color: "#7A7B7D" + font: input.font + } + } + } + + Text { + text: model.apps.length + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 18 + } + } + + // Slanted divider. + Item { + Layout.fillWidth: true + implicitHeight: 4 + + Shape { + id: divider + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + readonly property int slant: 2 + readonly property int chunk: 6 + readonly property int chunkSlant: 6 + readonly property int leftChunkLength: 2 + readonly property int rightChunkLength: 7 + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: divider.slant + startY: 0 + PathLine { + x: divider.width + y: 0 + } + PathLine { + x: divider.width - divider.slant + y: divider.height + } + PathLine { + x: 0 + y: divider.height + } + PathLine { + x: divider.slant + y: 0 + } + } + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 0 + startY: divider.height + PathLine { + x: divider.width / divider.leftChunkLength + y: divider.height + } + PathLine { + x: divider.width / divider.leftChunkLength - divider.chunkSlant + y: divider.height + divider.chunk + } + PathLine { + x: 0 + y: divider.height + divider.chunk + } + PathLine { + x: 0 + y: divider.height + } + } + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: divider.width + startY: 0 + PathLine { + x: divider.width - divider.width / divider.rightChunkLength + y: 0 + } + PathLine { + x: divider.width - divider.width / divider.rightChunkLength + divider.chunkSlant + y: -divider.chunk + } + PathLine { + x: divider.width + y: -divider.chunk + } + PathLine { + x: divider.width + y: 0 + } + } + } + } + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + model: model.apps + currentIndex: win.sel + spacing: 3 + boundsBehavior: Flickable.StopAtBounds + + delegate: MouseArea { + id: appRow + required property int index + required property var modelData + + readonly property int shear: 14 + + width: ListView.view.width + implicitHeight: 42 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + // Sheared (parallelogram) selection highlight — the + // rows are anything but rectangular when active. + Shape { + id: rowShape + anchors.fill: parent + visible: appRow.selected + preferredRendererType: Shape.CurveRenderer + + // Body + ShapePath { + strokeWidth: 0 + fillColor: "#22262C" + startX: appRow.shear + startY: 0 + PathLine { + x: rowShape.width + y: 0 + } + PathLine { + x: rowShape.width - appRow.shear + y: rowShape.height + } + PathLine { + x: 0 + y: rowShape.height + } + PathLine { + x: appRow.shear + y: 0 + } + } + // Left accent edge + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: appRow.shear + startY: 0 + PathLine { + x: appRow.shear + 5 + y: 0 + } + PathLine { + x: 5 + y: rowShape.height + } + PathLine { + x: 0 + y: rowShape.height + } + PathLine { + x: appRow.shear + y: 0 + } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: appRow.shear + 16 + anchors.rightMargin: 12 + spacing: 12 + + IconImage { + implicitSize: 28 + source: Quickshell.iconPath(appRow.modelData.icon, "application-x-executable") + } + + Text { + text: appRow.modelData.name + color: appRow.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: appRow.modelData.genericName || "" + color: "#7A7B7D" + font.pointSize: 10 + elide: Text.ElideRight + Layout.maximumWidth: 220 + } + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherSpotlight.qml b/dotfiles/quickshell/widgets/launcher/LauncherSpotlight.qml new file mode 100644 index 0000000..97dda8b --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherSpotlight.qml @@ -0,0 +1,262 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +// Variant 3 — "Spotlight": top-center command bar with a compact result list. +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + GlobalShortcut { + name: "launcher3" + description: "Toggle app launcher (Spotlight)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + // Ignore other panels' exclusive zones so the full-screen dim + // backdrop below isn't shrunk away from the bar strips. + exclusionMode: ExclusionMode.Ignore + + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + readonly property int maxRows: 7 + + property int sel: 0 + onSelChanged: list.positionViewAtIndex(sel, ListView.Contain) + + onVisibleChanged: { + if (visible) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + sel = (sel + delta + n) % n; + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + // Dim backdrop — click to dismiss. + MouseArea { + anchors.fill: parent + onClicked: root.active = false + } + + ColumnLayout { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: Math.round(parent.height * 0.16) + width: 640 + spacing: 0 + + // Command line + Rectangle { + Layout.fillWidth: true + implicitHeight: 60 + color: "#0F1012" + border.width: 1 + border.color: "#FFD063" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 18 + anchors.rightMargin: 18 + spacing: 14 + + Text { + text: ">" + color: "#FFD063" + font.family: "Digital-7 Mono" + font.pointSize: 26 + font.bold: true + } + + Item { + Layout.fillWidth: true + implicitHeight: 34 + + TextInput { + id: input + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.pointSize: 17 + clip: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Up: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + onTextChanged: win.sel = 0 + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: input.text.length === 0 + text: "run application" + color: "#7A7B7D" + font: input.font + } + } + } + + Text { + text: model.apps.length + " ▸" + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 16 + } + } + } + + // Accent seam between the command line and results. + Rectangle { + Layout.fillWidth: true + implicitHeight: 3 + color: "#FFD063" + visible: model.apps.length > 0 + } + + // Results + Rectangle { + Layout.fillWidth: true + // Cap the visible height to maxRows; scroll beyond that. + implicitHeight: Math.min(model.apps.length, win.maxRows) * 44 + 2 + color: "#0F1012" + opacity: 0.97 + visible: model.apps.length > 0 + border.width: 1 + border.color: "#292C30" + + ListView { + id: list + anchors.fill: parent + anchors.margins: 1 + + clip: true + model: model.apps + currentIndex: win.sel + interactive: contentHeight > height + boundsBehavior: Flickable.StopAtBounds + + delegate: MouseArea { + id: entry + required property int index + required property var modelData + + width: ListView.view.width + implicitHeight: 44 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + Rectangle { + anchors.fill: parent + color: entry.selected ? "#1A1C1F" : "transparent" + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 3 + color: "#FFD063" + visible: entry.selected + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 12 + + IconImage { + implicitSize: 24 + source: Quickshell.iconPath(entry.modelData.icon, "application-x-executable") + } + + Text { + text: entry.modelData.name + color: entry.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: entry.modelData.genericName || "" + color: "#7A7B7D" + font.pointSize: 10 + elide: Text.ElideRight + Layout.maximumWidth: 200 + } + } + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherStack.qml b/dotfiles/quickshell/widgets/launcher/LauncherStack.qml new file mode 100644 index 0000000..fb62098 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherStack.qml @@ -0,0 +1,268 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +// Variant 1 — "Stack": left-anchored vertical list launcher. +// Framed with the top/bottom accent strips used by the main Bar. +Scope { + id: root + + property bool active: false + + function toggle() { + root.active = !root.active; + } + + GlobalShortcut { + name: "launcher1" + description: "Toggle app launcher (Stack)" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + color: "transparent" + + anchors { + top: true + right: true + bottom: true + } + + margins { + top: 12 + bottom: 12 + right: 12 + } + + implicitWidth: 360 + + property int sel: 0 + onSelChanged: list.positionViewAtIndex(sel, ListView.Contain) + + // Reset state each time the panel opens. + onVisibleChanged: { + if (visible) { + input.text = ""; + sel = 0; + input.forceActiveFocus(); + } + } + + function move(delta) { + const n = model.apps.length; + if (n === 0) + return; + sel = (sel + delta + n) % n; + } + + function run() { + if (model.launch(sel)) + root.active = false; + } + + AppModel { + id: model + search: input.text + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + color: "#FFD063" + implicitHeight: 5 + Layout.fillWidth: true + } + + WrapperRectangle { + Layout.fillWidth: true + Layout.fillHeight: true + + margin: 12 + border.width: 1 + border.color: "#FFD063" + color: "#0F1012" + opacity: 0.95 + + ColumnLayout { + spacing: 10 + + // Search field + RowLayout { + spacing: 8 + Layout.fillWidth: true + + Text { + text: "▚" + color: "#FFD063" + font.pointSize: 14 + font.bold: true + } + + Item { + Layout.fillWidth: true + implicitHeight: 26 + + TextInput { + id: input + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + + color: "#EEEEEE" + font.family: "Digital-7 Mono" + font.pointSize: 16 + clip: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + case Qt.Key_Tab: + win.move(1); + event.accepted = true; + break; + case Qt.Key_Up: + case Qt.Key_Backtab: + win.move(-1); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + win.run(); + event.accepted = true; + break; + case Qt.Key_Escape: + root.active = false; + event.accepted = true; + break; + } + } + + // Reset selection to top whenever the query changes. + onTextChanged: win.sel = 0 + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: input.text.length === 0 + text: "search" + color: "#7A7B7D" + font: input.font + } + } + } + + Text { + text: model.apps.length + color: "#7A7B7D" + font.family: "Digital-7 Mono" + font.pointSize: 14 + } + } + + Rectangle { + color: "#FFD063" + implicitHeight: 2 + Layout.fillWidth: true + } + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + model: model.apps + currentIndex: win.sel + interactive: true + boundsBehavior: Flickable.StopAtBounds + spacing: 2 + + delegate: MouseArea { + id: appRow + required property int index + required property var modelData + + width: ListView.view.width + implicitHeight: 40 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: win.sel = index + onClicked: win.run() + + readonly property bool selected: index === win.sel + + Rectangle { + anchors.fill: parent + color: appRow.selected ? "#292C30" : "transparent" + + // Accent bar on the selected row. + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 3 + color: "#FFD063" + visible: appRow.selected + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 10 + + IconImage { + implicitSize: 26 + source: Quickshell.iconPath(appRow.modelData.icon, "application-x-executable") + } + + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + + Text { + text: appRow.modelData.name + color: appRow.selected ? "#FFD063" : "#EEEEEE" + font.pointSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + visible: text.length > 0 + text: appRow.modelData.genericName || appRow.modelData.comment || "" + color: "#7A7B7D" + font.pointSize: 9 + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + } + } + } + } + } + + Rectangle { + color: "#FFD063" + implicitHeight: 5 + Layout.fillWidth: true + } + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/LauncherState.qml b/dotfiles/quickshell/widgets/launcher/LauncherState.qml new file mode 100644 index 0000000..a937c4f --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/LauncherState.qml @@ -0,0 +1,14 @@ +pragma Singleton + +import Quickshell +import QtQuick + +// Shared launcher state so other widgets (e.g. the top bar) can react to the +// launcher without a direct dependency on it. +Singleton { + // True while the "Console" launcher (variant 4) is open — top bar reacts. + property bool consoleOpen: false + + // True while the "Dock" launcher (variant 5) is open — bottom bar reacts. + property bool dockOpen: false +} diff --git a/dotfiles/quickshell/widgets/layout/HorizontalStack.qml b/dotfiles/quickshell/widgets/layout/HorizontalStack.qml new file mode 100644 index 0000000..bf1266c --- /dev/null +++ b/dotfiles/quickshell/widgets/layout/HorizontalStack.qml @@ -0,0 +1,16 @@ +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: outerLayout + default property alias content: innerLayout.data + + RowLayout { + id: innerLayout + spacing: outerLayout.spacing + } + + Item { + Layout.fillWidth: true + } +} diff --git a/dotfiles/quickshell/widgets/layout/VerticalStack.qml b/dotfiles/quickshell/widgets/layout/VerticalStack.qml new file mode 100644 index 0000000..fba9586 --- /dev/null +++ b/dotfiles/quickshell/widgets/layout/VerticalStack.qml @@ -0,0 +1,16 @@ +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: outerLayout + default property alias content: innerLayout.data + + ColumnLayout { + id: innerLayout + spacing: outerLayout.spacing + } + + Item { + Layout.fillHeight: true + } +} diff --git a/dotfiles/quickshell/widgets/notifications/NotificationPopup.qml b/dotfiles/quickshell/widgets/notifications/NotificationPopup.qml new file mode 100644 index 0000000..01e3d28 --- /dev/null +++ b/dotfiles/quickshell/widgets/notifications/NotificationPopup.qml @@ -0,0 +1,119 @@ +import Quickshell.Services.Notifications +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Notification modelData + + spacing: 0 + Layout.fillWidth: true + + Rectangle { + color: "#FFD063" + implicitHeight: 5 + + Layout.fillWidth: true + } + + WrapperRectangle { + Layout.fillWidth: true + + margin: 12 + border.width: 1 + border.color: "#FFD063" + color: "#0F1012" + opacity: .9 + + ColumnLayout { + spacing: 4 + + Layout.fillWidth: true + + RowLayout { + spacing: 8 + + Layout.fillWidth: true + + IconImage { + visible: root.modelData.appIcon !== "" + source: root.modelData.appIcon + implicitSize: 20 + } + + Text { + text: root.modelData.summary + color: "#EEEEEE" + font.pointSize: 14 + font.bold: true + elide: Text.ElideRight + + Layout.fillWidth: true + } + + Text { + id: dismissButton + + text: "×" + color: "#FFD063" + font.pointSize: 18 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.modelData.dismiss() + } + } + } + + Text { + visible: root.modelData.body !== "" + text: root.modelData.body + color: "#EEEEEE" + font.pointSize: 12 + wrapMode: Text.Wrap + + Layout.fillWidth: true + } + + RowLayout { + visible: root.modelData.actions.length > 0 + spacing: 6 + + Layout.fillWidth: true + + Repeater { + model: root.modelData.actions + + Rectangle { + id: actionButton + + required property NotificationAction modelData + + color: "#292C30" + border.width: 1 + border.color: "#FFD063" + implicitHeight: 28 + implicitWidth: actionText.implicitWidth + 16 + + Text { + id: actionText + anchors.centerIn: parent + text: actionButton.modelData.text + color: "#EEEEEE" + font.pointSize: 12 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: actionButton.modelData.invoke() + } + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/notifications/Notifications.qml b/dotfiles/quickshell/widgets/notifications/Notifications.qml new file mode 100644 index 0000000..31f6e54 --- /dev/null +++ b/dotfiles/quickshell/widgets/notifications/Notifications.qml @@ -0,0 +1,79 @@ +import Quickshell +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts + +Scope { + id: root + + NotificationServer { + id: server + + keepOnReload: true + bodySupported: true + bodyMarkupSupported: true + imageSupported: true + actionsSupported: true + actionIconsSupported: true + + onNotification: notification => { + notification.tracked = true; + + const timeout = notification.expireTimeout === 0 ? 0 : notification.expireTimeout > 0 ? notification.expireTimeout : (notification.urgency === NotificationUrgency.Critical ? 0 : 5000); + + if (timeout <= 0) + return; + + let closed = false; + notification.closed.connect(() => { + closed = true; + }); + + const timer = expiryTimerComponent.createObject(root, { + interval: timeout + }); + timer.triggered.connect(() => { + if (!closed) + notification.expire(); + timer.destroy(); + }); + } + } + + Component { + id: expiryTimerComponent + + Timer { + running: true + } + } + + PanelWindow { + screen: Quickshell.screens.find(s => s.name === "DP-2") + + color: "transparent" + + anchors { + top: true + right: true + } + + margins.top: 12 + margins.right: 12 + + implicitWidth: 340 + implicitHeight: column.implicitHeight + + ColumnLayout { + id: column + anchors.fill: parent + spacing: 8 + + Repeater { + model: server.trackedNotifications + + NotificationPopup {} + } + } + } +} diff --git a/dotfiles/quickshell/widgets/osd/VolumeOsd.qml b/dotfiles/quickshell/widgets/osd/VolumeOsd.qml new file mode 100644 index 0000000..6200114 --- /dev/null +++ b/dotfiles/quickshell/widgets/osd/VolumeOsd.qml @@ -0,0 +1,111 @@ +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Wayland +import QtQuick + +Scope { + id: root + + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property real volume: sink?.audio?.volume ?? 0 + readonly property bool muted: sink?.audio?.muted ?? false + readonly property real maxVolume: 1.5 + + property bool active: false + + PwObjectTracker { + objects: [root.sink] + } + + Timer { + id: hideTimer + interval: 1500 + onTriggered: root.active = false + } + + Connections { + target: root.sink?.audio + + function onVolumeChanged() { + root.active = true; + hideTimer.restart(); + } + + function onMutedChanged() { + root.active = true; + hideTimer.restart(); + } + } + + PanelWindow { + screen: Quickshell.screens.find(s => s.name === "DP-2") + visible: root.active + + WlrLayershell.layer: WlrLayer.Overlay + exclusionMode: ExclusionMode.Ignore + + color: "transparent" + + anchors { + bottom: true + left: true + right: true + } + + margins { + bottom: 30 + left: 12 + right: 12 + } + + implicitHeight: percentText.implicitHeight + 4 + bar.height + + Text { + id: percentText + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: bar.top + anchors.bottomMargin: 4 + + text: Math.round(Math.min(root.volume, root.maxVolume) * 100) + "%" + color: !root.muted && root.volume > 1 ? "#FF6B4A" : "#FFD063" + font.pointSize: 12 + font.bold: true + } + + Item { + id: bar + + anchors.bottom: parent.bottom + width: parent.width + height: 6 + + Rectangle { + anchors.centerIn: parent + implicitHeight: 6 + width: parent.width * Math.min(root.volume, root.maxVolume) / root.maxVolume + color: "#FF6B4A" + visible: !root.muted && root.volume > 1 + + Behavior on width { + NumberAnimation { + duration: 150 + } + } + } + + Rectangle { + anchors.centerIn: parent + implicitHeight: 6 + width: parent.width * Math.min(root.volume, 1) / root.maxVolume + color: root.muted ? "#7A7B7D" : "#FFD063" + + Behavior on width { + NumberAnimation { + duration: 150 + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/sidebar/SideBar.qml b/dotfiles/quickshell/widgets/sidebar/SideBar.qml new file mode 100644 index 0000000..c094486 --- /dev/null +++ b/dotfiles/quickshell/widgets/sidebar/SideBar.qml @@ -0,0 +1,476 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Services.Pipewire +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes + +// A vertical sidebar carrying the "Slant" (launcher V6) visual language — +// chamfered panel, thick trapezoid bevel accents, an outward bracket, a +// floating triangle cap and a slanted divider. Right-anchored, mirrored. +Scope { + id: root + + property bool active: true + + function toggle() { + root.active = !root.active; + } + + GlobalShortcut { + name: "sidebar" + description: "Toggle the Slant sidebar" + onPressed: root.toggle() + } + + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property real volume: sink?.audio?.volume ?? 0 + readonly property bool muted: sink?.audio?.muted ?? false + readonly property real maxVolume: 1.5 + + PwObjectTracker { + objects: [root.sink] + } + + PanelWindow { + id: win + + visible: root.active + + color: "transparent" + + anchors { + top: true + right: true + bottom: true + } + + margins { + top: 8 + } + + // Frame width plus the gutter the outward bracket grows into. + readonly property int pad: 10 + implicitWidth: 72 + 2 * pad + + Item { + id: frame + + // Inset so the outward bracket has room in the gutter. + anchors.fill: parent + anchors.margins: win.pad + + readonly property int chamfer: 18 // big cut corners (top-right, bottom-left) + readonly property int smallChamfer: 7 // small bevel (top-left, bottom-right) + readonly property int bevel: 4 // inward thickness of the trapezoid accents + readonly property int chunkThick: 7 // outward thickness of the bracket + readonly property int chunkSlant: 10 // slant of the bracket end pieces + readonly property int capSize: 10 // floating triangle cap + readonly property int armSide: 58 // bracket arm down the left edge + readonly property int armTop: 34 // bracket arm along the top edge + + Shape { + id: panelShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + // Panel: big chamfers on top-right & bottom-left, small bevels + // on top-left & bottom-right (mirror of the left-anchored panel). + ShapePath { + fillColor: "#0F1012" + strokeColor: "#FFD063" + strokeWidth: 2 + + startX: panelShape.width - frame.chamfer + startY: 0 + PathLine { + x: frame.smallChamfer + y: 0 + } + PathLine { + x: 0 + y: frame.smallChamfer + } + PathLine { + x: 0 + y: panelShape.height - frame.chamfer + } + PathLine { + x: frame.chamfer + y: panelShape.height + } + PathLine { + x: panelShape.width - frame.smallChamfer + y: panelShape.height + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.smallChamfer + } + PathLine { + x: panelShape.width + y: frame.chamfer + } + PathLine { + x: panelShape.width - frame.chamfer + y: 0 + } + } + + // Thick top-right bevel accent (trapezoid to the edges). + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: panelShape.width + startY: frame.chamfer + PathLine { + x: panelShape.width - frame.chamfer + y: 0 + } + PathLine { + x: panelShape.width - frame.chamfer - 2 * frame.bevel + y: 0 + } + PathLine { + x: panelShape.width + y: frame.chamfer + 2 * frame.bevel + } + PathLine { + x: panelShape.width + y: frame.chamfer + } + } + + // Thick bottom-left bevel accent. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: 0 + startY: panelShape.height - frame.chamfer + PathLine { + x: frame.chamfer + y: panelShape.height + } + PathLine { + x: frame.chamfer + 2 * frame.bevel + y: panelShape.height + } + PathLine { + x: 0 + y: panelShape.height - frame.chamfer - 2 * frame.bevel + } + PathLine { + x: 0 + y: panelShape.height - frame.chamfer + } + } + + // Floating triangle cap in the bottom-left notch. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: 0 + startY: panelShape.height + PathLine { + x: 0 + y: panelShape.height - frame.capSize + } + PathLine { + x: frame.capSize + y: panelShape.height + } + PathLine { + x: 0 + y: panelShape.height + } + } + + // Outward bracket wrapping the top-left corner: down the left + // edge and along the top edge, with slanted ends and a beveled + // corner following the small chamfer. + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + + startX: 0 + startY: frame.armSide + PathLine { + x: -frame.chunkThick + y: frame.armSide - frame.chunkSlant + } + PathLine { + x: -frame.chunkThick + y: frame.smallChamfer + } + PathLine { + x: frame.smallChamfer + y: -frame.chunkThick + } + PathLine { + x: frame.armTop - frame.chunkSlant + y: -frame.chunkThick + } + PathLine { + x: frame.armTop + y: 0 + } + PathLine { + x: frame.smallChamfer + y: 0 + } + PathLine { + x: 0 + y: frame.smallChamfer + } + PathLine { + x: 0 + y: frame.armSide + } + } + } + + // ── Content ──────────────────────────────────────────────────── + ColumnLayout { + anchors.fill: parent + anchors.topMargin: 16 + anchors.bottomMargin: 16 + anchors.leftMargin: 10 + anchors.rightMargin: 8 + spacing: 10 + + // Clock — Digital-7, hh over mm + Text { + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + text: Qt.formatDateTime(clock.date, "hh\nmm") + font.family: "Digital-7 Mono" + font.pointSize: 22 + font.letterSpacing: 1 + color: "#EEEEEE" + + SystemClock { + id: clock + precision: SystemClock.Minutes + } + } + + // Date + Text { + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + text: Qt.formatDateTime(clock.date, "dd\nMMM").toUpperCase() + font.family: "Digital-7 Mono" + font.pointSize: 13 + color: "#FFD063" + } + + // Slanted divider + Item { + Layout.fillWidth: true + implicitHeight: 4 + + Shape { + id: divider + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 8 + startY: 0 + PathLine { x: divider.width; y: 0 } + PathLine { x: divider.width - 8; y: divider.height } + PathLine { x: 0; y: divider.height } + PathLine { x: 8; y: 0 } + } + } + } + + Item { + Layout.fillHeight: true + } + + // Volume — vertical meter with a sheared fill and a % readout + Text { + Layout.alignment: Qt.AlignHCenter + text: "VOL" + font.family: "Digital-7 Mono" + font.pointSize: 10 + color: "#7A7B7D" + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 3 + + // Overflow — three floating slanted segments for volume + // pushed above 100%, lit with the same overload color as + // the volume OSD. + Repeater { + model: 3 + + delegate: Item { + id: seg + required property int index + + readonly property real segStart: 1 + (2 - index) / 3 * (root.maxVolume - 1) + readonly property real segEnd: 1 + (3 - index) / 3 * (root.maxVolume - 1) + readonly property real frac: root.muted ? 0 : Math.max(0, Math.min(1, (root.volume - segStart) / (segEnd - segStart))) + readonly property int chamfer: 4 + + Layout.alignment: Qt.AlignHCenter + implicitWidth: 16 + implicitHeight: 12 + + // Empty track, chamfered to match the main meter. + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 1 + strokeColor: "#7A7B7D" + fillColor: "#292C30" + + startX: seg.chamfer + startY: 0 + PathLine { x: seg.width; y: 0 } + PathLine { x: seg.width; y: seg.height - seg.chamfer } + PathLine { x: seg.width - seg.chamfer; y: seg.height } + PathLine { x: 0; y: seg.height } + PathLine { x: 0; y: seg.chamfer } + PathLine { x: seg.chamfer; y: 0 } + } + } + + // Overload fill — reveals a fixed copy of the same + // chamfered hexagon from the bottom, so filled and + // empty states always share one silhouette. + Item { + id: segFillClip + anchors.left: parent.left + anchors.leftMargin: 2 + anchors.right: parent.right + anchors.rightMargin: 2 + anchors.bottom: parent.bottom + anchors.bottomMargin: 2 + clip: true + height: seg.frac * (seg.height - 4) + + Shape { + id: segFill + width: seg.width - 4 + height: seg.height - 4 + y: segFillClip.height - height + preferredRendererType: Shape.CurveRenderer + + readonly property int chamfer: seg.chamfer - 2 + + ShapePath { + strokeWidth: 0 + fillColor: "#FF6B4A" + startX: segFill.chamfer + startY: 0 + PathLine { x: segFill.width; y: 0 } + PathLine { x: segFill.width; y: segFill.height - segFill.chamfer } + PathLine { x: segFill.width - segFill.chamfer; y: segFill.height } + PathLine { x: 0; y: segFill.height } + PathLine { x: 0; y: segFill.chamfer } + PathLine { x: segFill.chamfer; y: 0 } + } + } + } + } + } + + Item { + id: volMeter + Layout.alignment: Qt.AlignHCenter + implicitWidth: 16 + implicitHeight: 96 + + readonly property int chamfer: 6 + + // Track — chamfered top-left/bottom-right to match the + // panel's slant, so the sheared fill sits in a shape that + // agrees with it rather than a plain rectangle. + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 1 + strokeColor: "#7A7B7D" + fillColor: "#292C30" + + startX: volMeter.chamfer + startY: 0 + PathLine { x: volMeter.width; y: 0 } + PathLine { x: volMeter.width; y: volMeter.height - volMeter.chamfer } + PathLine { x: volMeter.width - volMeter.chamfer; y: volMeter.height } + PathLine { x: 0; y: volMeter.height } + PathLine { x: 0; y: volMeter.chamfer } + PathLine { x: volMeter.chamfer; y: 0 } + } + } + + // Accent fill — reveals a fixed copy of the same + // chamfered hexagon as the track from the bottom, so + // the fill and background always share one silhouette. + Item { + id: fillClip + anchors.left: parent.left + anchors.leftMargin: 2 + anchors.right: parent.right + anchors.rightMargin: 2 + anchors.bottom: parent.bottom + anchors.bottomMargin: 2 + clip: true + + readonly property real frac: root.muted ? 0 : Math.min(root.volume, 1) + height: frac * (volMeter.height - 4) + + Shape { + id: vol + width: volMeter.width - 4 + height: volMeter.height - 4 + y: fillClip.height - height + preferredRendererType: Shape.CurveRenderer + + readonly property int chamfer: volMeter.chamfer - 2 + + ShapePath { + strokeWidth: 0 + fillColor: root.muted ? "#7A7B7D" : "#FFD063" + startX: vol.chamfer + startY: 0 + PathLine { x: vol.width; y: 0 } + PathLine { x: vol.width; y: vol.height - vol.chamfer } + PathLine { x: vol.width - vol.chamfer; y: vol.height } + PathLine { x: 0; y: vol.height } + PathLine { x: 0; y: vol.chamfer } + PathLine { x: vol.chamfer; y: 0 } + } + } + } + } + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: root.muted ? "--" : Math.round(root.volume * 100) + font.family: "Digital-7 Mono" + font.pointSize: 14 + color: root.muted ? "#7A7B7D" : "#EEEEEE" + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/systray/SysTray.qml b/dotfiles/quickshell/widgets/systray/SysTray.qml new file mode 100644 index 0000000..2b2ef0a --- /dev/null +++ b/dotfiles/quickshell/widgets/systray/SysTray.qml @@ -0,0 +1,220 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes + +// The right bar: a compact utility strip for the SystemTray icons, styled +// in the same family as the Launcher (V6 "Slant") and SideBar — accent +// color, chamfered corners, Digital-7 Mono, the slash-trio motif — but with +// its own plain, evenly-chamfered panel rather than their ornate +// bracket-and-cap silhouette, since this is a secondary/utility bar rather +// than a hero surface. +Scope { + id: root + + Variants { + model: Quickshell.screens + + PanelWindow { + id: win + + required property var modelData + screen: modelData + + color: "transparent" + + anchors { + top: true + right: true + bottom: true + } + + margins { + top: 8 + } + + readonly property int pad: 10 + implicitWidth: 64 + 2 * pad + + Item { + id: frame + + anchors.fill: parent + anchors.margins: win.pad + + readonly property int chamfer: 14 // uniform corner cut on all four corners + + Shape { + id: panelShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "#0F1012" + strokeColor: "#FFD063" + strokeWidth: 2 + + startX: frame.chamfer + startY: 0 + PathLine { + x: panelShape.width - frame.chamfer + y: 0 + } + PathLine { + x: panelShape.width + y: frame.chamfer + } + PathLine { + x: panelShape.width + y: panelShape.height - frame.chamfer + } + PathLine { + x: panelShape.width - frame.chamfer + y: panelShape.height + } + PathLine { + x: frame.chamfer + y: panelShape.height + } + PathLine { + x: 0 + y: panelShape.height - frame.chamfer + } + PathLine { + x: 0 + y: frame.chamfer + } + PathLine { + x: frame.chamfer + y: 0 + } + } + } + + // ── Content ──────────────────────────────────────────────── + ColumnLayout { + anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 20 + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 14 + + // Slash-trio deco, echoing the launcher/sidebar header motif. + Shape { + id: slashes + Layout.alignment: Qt.AlignHCenter + implicitWidth: 26 + implicitHeight: 20 + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 6 + startY: 0 + PathLine { x: 10; y: 0 } + PathLine { x: 4; y: slashes.height } + PathLine { x: 0; y: slashes.height } + PathLine { x: 6; y: 0 } + } + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 15 + startY: 0 + PathLine { x: 19; y: 0 } + PathLine { x: 13; y: slashes.height } + PathLine { x: 9; y: slashes.height } + PathLine { x: 15; y: 0 } + } + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 24 + startY: 0 + PathLine { x: 28; y: 0 } + PathLine { x: 22; y: slashes.height } + PathLine { x: 18; y: slashes.height } + PathLine { x: 24; y: 0 } + } + } + + // Slanted divider + Item { + Layout.fillWidth: true + implicitHeight: 4 + + Shape { + id: divider + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + strokeWidth: 0 + fillColor: "#FFD063" + startX: 8 + startY: 0 + PathLine { x: divider.width; y: 0 } + PathLine { x: divider.width - 8; y: divider.height } + PathLine { x: 0; y: divider.height } + PathLine { x: 8; y: 0 } + } + } + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: "TRAY" + font.family: "Digital-7 Mono" + font.pointSize: 12 + color: "#7A7B7D" + } + + ListView { + id: trayList + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + spacing: 10 + boundsBehavior: Flickable.StopAtBounds + model: SystemTray.items + + delegate: Item { + required property SystemTrayItem modelData + + width: ListView.view.width + implicitHeight: 36 + + SysTrayItem { + anchors.horizontalCenter: parent.horizontalCenter + modelData: parent.modelData + } + } + } + + Text { + Layout.alignment: Qt.AlignHCenter + visible: SystemTray.items.values.length === 0 + text: "--" + font.family: "Digital-7 Mono" + font.pointSize: 14 + color: "#7A7B7D" + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: SystemTray.items.values.length + font.family: "Digital-7 Mono" + font.pointSize: 16 + color: "#EEEEEE" + } + } + } + } + } +} diff --git a/dotfiles/quickshell/widgets/systray/SysTrayItem.qml b/dotfiles/quickshell/widgets/systray/SysTrayItem.qml new file mode 100644 index 0000000..9e1ac85 --- /dev/null +++ b/dotfiles/quickshell/widgets/systray/SysTrayItem.qml @@ -0,0 +1,65 @@ +import Quickshell +import Quickshell.Services.SystemTray +import QtQuick +import QtQuick.Shapes + +// A single tray icon, chamfered to match the Slant (launcher V6 / SideBar) +// visual language instead of a plain rectangular hit target. +MouseArea { + id: root + + required property SystemTrayItem modelData + + implicitWidth: 36 + implicitHeight: 36 + + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: event => { + if (event.button === Qt.LeftButton) { + modelData.activate(); + } else if (modelData.hasMenu) { + const window = root.QsWindow?.window; + modelData.display(window, window.width, root.mapToItem(null, 0, 0).y); + } + event.accepted = true; + } + + readonly property int chamfer: 6 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 1 + strokeColor: root.containsMouse ? "#FFD063" : "#7A7B7D" + fillColor: "#292C30" + + startX: root.chamfer + startY: 0 + PathLine { x: root.width; y: 0 } + PathLine { x: root.width; y: root.height - root.chamfer } + PathLine { x: root.width - root.chamfer; y: root.height } + PathLine { x: 0; y: root.height } + PathLine { x: 0; y: root.chamfer } + PathLine { x: root.chamfer; y: 0 } + + Behavior on strokeColor { + ColorAnimation { + duration: 150 + } + } + } + } + + Image { + anchors.centerIn: parent + source: modelData.icon + fillMode: Image.PreserveAspectFit + width: 20 + height: 20 + } +} diff --git a/hosts/terra/home.nix b/hosts/terra/home.nix index 3ea3a53..acb03ea 100644 --- a/hosts/terra/home.nix +++ b/hosts/terra/home.nix @@ -57,10 +57,13 @@ # indexes share/mime/packages inside the hm profile itself, so this has to # be a package in home.packages, not a plain xdg.dataFile. xdg.mime.enable = true; + xdg.configFile."quickshell".source = ../../dotfiles/quickshell; home.packages = [ (pkgs.writeTextDir "share/mime/packages/application-x-ms-sln.xml" (builtins.readFile ../../dotfiles/mime/application-x-ms-sln.xml)) pkgs.claude-code + pkgs.quickshell + pkgs.opencode ]; programs.alacritty = {