feat(quickshell): add dense telemetry bar #4

Open
luna wants to merge 18 commits from feat/quickshell-dense-bar into master
7 changed files with 770 additions and 5 deletions
Showing only changes of commit fdc00a8574 - Show all commits
+1 -1
View File
@@ -27,7 +27,7 @@ Quickshell hot-reloads QML on file save when already running, so for most edits
**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/launcher/`shared `AppModel` search/execution plus eight launcher variants. `ApplicationLauncher` (variant 8 and the primary `SUPER` launcher) keeps its visual core in the headlessly renderable `ApplicationLauncherContent`; variants 17 remain available on `SUPER CTRL 17` for comparison.
- `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.
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
hyprctl dispatch 'hl.dsp.global ("quickshell:launcher7")'
hyprctl dispatch 'hl.dsp.global ("quickshell:launcher8")'
+2 -2
View File
@@ -17,8 +17,7 @@ Scope {
SideBar {}
BarBottom {}
// App launcher variants — toggled via Hyprland global shortcuts
// (SUPER CTRL 1/2/3). Try each and keep the one you like.
// App launcher variants — SUPER CTRL 18; variant 8 is the primary HUD.
LauncherStack {} // 1 — left vertical list
LauncherGrid {} // 2 — centered icon grid
LauncherSpotlight {} // 3 — top-center command bar
@@ -26,6 +25,7 @@ Scope {
LauncherDock {} // 5 — deck rising from the bottom bar
LauncherSlant {} // 6 — angular / sheared panel
LauncherCorner {} // 7 — Slant (V6) copy + floating power panel (shutdown/reboot)
ApplicationLauncher {} // 8 — dense HUD command index (primary)
Notifications {}
VolumeOsd {}
@@ -0,0 +1,62 @@
import QtQuick
import qs.widgets.launcher
ApplicationLauncherContent {
width: 1080
height: 620
query: "term"
selectedIndex: 1
showIcons: false
apps: [
{
name: "Alacritty",
genericName: "Terminal Emulator",
comment: "GPU accelerated command interface",
icon: "utilities-terminal",
categories: ["System", "TerminalEmulator"]
},
{
name: "Ghostty",
genericName: "Terminal",
comment: "Fast native terminal for local operations",
icon: "com.mitchellh.ghostty",
categories: ["System", "TerminalEmulator"]
},
{
name: "Kitty",
genericName: "Terminal Emulator",
comment: "GPU based terminal with multiplexing",
icon: "kitty",
categories: ["System", "TerminalEmulator"]
},
{
name: "Vivaldi",
genericName: "Web Browser",
comment: "Access network and web applications",
icon: "vivaldi",
categories: ["Network", "WebBrowser"]
},
{
name: "Cosmic Files",
genericName: "File Manager",
comment: "Browse and manage local storage",
icon: "com.system76.CosmicFiles",
categories: ["System", "FileManager"]
},
{
name: "Obsidian",
genericName: "Knowledge Base",
comment: "Local-first markdown workspace",
icon: "obsidian",
categories: ["Office"]
},
{
name: "Steam",
genericName: "Game Platform",
comment: "Launch and manage games",
icon: "steam",
categories: ["Game"]
}
]
}
@@ -0,0 +1,101 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import qs.widgets.theme
// Primary application launcher (variant 8). The full-screen layer-shell adapter
// owns focus, DesktopEntries and execution; ApplicationLauncherContent remains
// an Item so the complete visual state can be rendered headlessly.
Scope {
id: root
property bool active: false
function toggle() { root.active = !root.active; }
GlobalShortcut {
name: "launcher8"
description: "Toggle dense application command index"
onPressed: root.toggle()
}
PanelWindow {
id: win
visible: root.active
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
color: Theme.textAlpha(0)
anchors {
top: true
left: true
right: true
bottom: true
}
property int selectedIndex: 0
function clampSelection(index) {
if (model.apps.length === 0)
return 0;
return Math.max(0, Math.min(model.apps.length - 1, index));
}
function move(delta) {
const count = model.apps.length;
if (count === 0)
return;
selectedIndex = ((selectedIndex + delta) % count + count) % count;
}
function launch(index) {
if (model.launch(index))
root.active = false;
}
onVisibleChanged: {
if (visible) {
content.clearSearch();
selectedIndex = 0;
content.focusSearch();
}
}
AppModel {
id: model
search: content.query
}
Rectangle {
anchors.fill: parent
color: Theme.surface
opacity: 0.72
MouseArea {
anchors.fill: parent
onClicked: root.active = false
}
}
ApplicationLauncherContent {
id: content
anchors.centerIn: parent
width: 1080
height: 620
scale: Math.min(1, (parent.width - 64) / width, (parent.height - 64) / height)
transformOrigin: Item.Center
apps: model.apps
selectedIndex: win.selectedIndex
onSelectionRequested: index => win.selectedIndex = win.clampSelection(index)
onMoveRequested: delta => win.move(delta)
onLaunchRequested: index => win.launch(index)
onDismissRequested: root.active = false
}
}
}
@@ -0,0 +1,602 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.widgets.bar
import qs.widgets.theme
// Headlessly renderable visual core for the primary application launcher.
// Runtime concerns (DesktopEntries, layer shell, launching) stay in
// ApplicationLauncher.qml; this component only renders state and emits intent.
Item {
id: root
property var apps: []
property int selectedIndex: 0
property bool showIcons: true
property alias query: searchInput.text
readonly property var selectedApp: apps.length > 0 && selectedIndex >= 0 && selectedIndex < apps.length
? apps[selectedIndex] : null
signal selectionRequested(int index)
signal moveRequested(int delta)
signal launchRequested(int index)
signal dismissRequested
function focusSearch() { searchInput.forceActiveFocus(); }
function clearSearch() { searchInput.text = ""; }
implicitWidth: 1080
implicitHeight: 620
component MicroText: Text {
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 7
font.letterSpacing: 0.9
elide: Text.ElideRight
}
component KeyHint: Row {
property string keyText: ""
property string actionText: ""
spacing: 6
Rectangle {
width: keyLabel.implicitWidth + 10
height: 18
color: Theme.accentAlpha(0.14)
border.width: 1
border.color: Theme.accent
Text {
id: keyLabel
anchors.centerIn: parent
text: parent.parent.keyText
color: Theme.accent
font.family: Theme.microFont
font.pixelSize: 7
font.bold: true
}
}
MicroText {
anchors.verticalCenter: parent.verticalCenter
text: parent.actionText
}
}
StatusBarPanel {
anchors.fill: parent
panelId: "008"
title: "APPLICATION COMMAND INDEX"
meta: root.apps.length + " TARGETS"
chamfer: 18
// Query module.
StatusBarPanel {
id: queryPanel
x: 18
y: 34
width: 670
height: 76
panelId: "QRY"
title: "SEARCH VECTOR"
meta: "FUZZY / PREFIX"
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 31
height: 34
spacing: 12
Text {
text: ">_"
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 20
font.bold: true
}
TextInput {
id: searchInput
Layout.fillWidth: true
Layout.fillHeight: true
verticalAlignment: TextInput.AlignVCenter
color: Theme.text
selectionColor: Theme.accent
selectedTextColor: Theme.surface
font.family: Theme.displayFont
font.pixelSize: 18
font.letterSpacing: 1
clip: true
onTextChanged: root.selectionRequested(0)
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
case Qt.Key_Tab:
root.moveRequested(1);
event.accepted = true;
break;
case Qt.Key_Up:
case Qt.Key_Backtab:
root.moveRequested(-1);
event.accepted = true;
break;
case Qt.Key_PageDown:
root.moveRequested(5);
event.accepted = true;
break;
case Qt.Key_PageUp:
root.moveRequested(-5);
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
root.launchRequested(root.selectedIndex);
event.accepted = true;
break;
case Qt.Key_Escape:
root.dismissRequested();
event.accepted = true;
break;
}
}
Text {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
visible: searchInput.text.length === 0
text: "TYPE APPLICATION DESIGNATION"
color: Theme.muted
font: searchInput.font
}
}
Rectangle {
Layout.preferredWidth: 84
Layout.preferredHeight: 22
color: Theme.accentAlpha(0.12)
border.width: 1
border.color: Theme.hair
Text {
anchors.centerIn: parent
text: "LIVE INDEX"
color: Theme.accent
font.family: Theme.microFont
font.pixelSize: 7
font.bold: true
font.letterSpacing: 0.8
}
}
}
}
// Search result table.
StatusBarPanel {
id: resultPanel
x: 18
y: 118
width: 670
height: 424
panelId: "IDX"
title: "APPLICATION TABLE"
meta: root.query.length > 0 ? "FILTER ACTIVE" : "AZ / LOCAL"
ListView {
id: appList
x: 9
y: 29
width: parent.width - 18
height: parent.height - 38
clip: true
spacing: 3
model: root.apps
currentIndex: root.selectedIndex
boundsBehavior: Flickable.StopAtBounds
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
delegate: MouseArea {
id: row
required property int index
required property var modelData
width: ListView.view.width
height: 48
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
readonly property bool selected: index === root.selectedIndex
onEntered: root.selectionRequested(index)
onClicked: root.launchRequested(index)
Shape {
id: rowShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: row.selected ? Theme.selection : Theme.textAlpha(0.025)
strokeColor: row.selected ? Theme.accent : Theme.textAlpha(0.10)
strokeWidth: 1
startX: 9; startY: 0
PathLine { x: rowShape.width; y: 0 }
PathLine { x: rowShape.width - 9; y: rowShape.height }
PathLine { x: 0; y: rowShape.height }
PathLine { x: 9; y: 0 }
}
ShapePath {
fillColor: row.selected ? Theme.accent : Theme.textAlpha(0.12)
strokeWidth: 0
startX: 9; startY: 0
PathLine { x: 13; y: 0 }
PathLine { x: 4; y: rowShape.height }
PathLine { x: 0; y: rowShape.height }
PathLine { x: 9; y: 0 }
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 18
spacing: 12
Text {
Layout.preferredWidth: 28
text: String(row.index + 1).padStart(2, "0")
color: row.selected ? Theme.accent : Theme.muted
font.family: Theme.microFont
font.pixelSize: 8
font.bold: row.selected
}
Rectangle {
Layout.preferredWidth: 34
Layout.preferredHeight: 34
color: row.selected ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.035)
border.width: 1
border.color: row.selected ? Theme.accent : Theme.hair
IconImage {
visible: root.showIcons
anchors.centerIn: parent
implicitSize: 24
source: root.showIcons
? Quickshell.iconPath(row.modelData.icon || "application-x-executable", "application-x-executable")
: ""
}
Text {
visible: !root.showIcons
anchors.centerIn: parent
text: String(row.modelData.name || "?").charAt(0).toUpperCase()
color: row.selected ? Theme.accent : Theme.text
font.family: Theme.displayFont
font.pixelSize: 14
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Text {
Layout.fillWidth: true
text: row.modelData.name || "UNKNOWN TARGET"
color: row.selected ? Theme.accent : Theme.text
font.family: Theme.displayFont
font.pixelSize: 11
font.bold: row.selected
font.letterSpacing: 0.5
elide: Text.ElideRight
}
MicroText {
Layout.fillWidth: true
text: row.modelData.genericName || row.modelData.comment || "DESKTOP APPLICATION"
}
}
MicroText {
Layout.preferredWidth: 100
horizontalAlignment: Text.AlignRight
text: row.index === root.selectedIndex ? "LOCKED" : "AVAILABLE"
color: row.selected ? Theme.accent : Theme.muted
}
Rectangle {
Layout.preferredWidth: 38
Layout.preferredHeight: 6
color: Theme.textAlpha(0.05)
border.width: 1
border.color: Theme.hair
Rectangle {
width: row.selected ? parent.width : Math.max(5, parent.width * (1 - row.index / Math.max(1, root.apps.length)))
height: parent.height
color: row.selected ? Theme.accent : Theme.textAlpha(0.20)
}
}
}
}
Text {
anchors.centerIn: parent
visible: root.apps.length === 0
text: "NO EXECUTABLE TARGETS MATCH QUERY"
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 9
font.letterSpacing: 1.2
}
}
}
// Selected application inspector.
StatusBarPanel {
id: inspector
x: 700
y: 34
width: 362
height: 508
panelId: "SEL"
title: "TARGET INSPECTOR"
meta: root.selectedApp ? "LOCKED" : "NO TARGET"
Item {
id: reticle
anchors.horizontalCenter: parent.horizontalCenter
y: 39
width: 126
height: 126
Rectangle {
anchors.centerIn: parent
width: 112
height: 112
radius: 56
color: Theme.textAlpha(0.015)
border.width: 1
border.color: Theme.accentAlpha(0.52)
}
Rectangle {
anchors.centerIn: parent
width: 84
height: 84
radius: 42
color: Theme.textAlpha(0)
border.width: 1
border.color: Theme.hair
}
Rectangle { anchors.centerIn: parent; width: 126; height: 1; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 1; height: 126; color: Theme.hair }
Rectangle {
anchors.centerIn: parent
width: 60
height: 60
color: Theme.surface
border.width: 1
border.color: Theme.accent
IconImage {
visible: root.showIcons
anchors.centerIn: parent
implicitSize: 44
source: root.showIcons && root.selectedApp
? Quickshell.iconPath(root.selectedApp.icon || "application-x-executable", "application-x-executable")
: ""
}
Text {
visible: !root.showIcons
anchors.centerIn: parent
text: root.selectedApp ? String(root.selectedApp.name || "?").charAt(0).toUpperCase() : "?"
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 28
font.bold: true
}
}
Repeater {
model: 4
Rectangle {
required property int index
width: 5; height: 5
x: index % 2 === 0 ? 8 : reticle.width - 13
y: index < 2 ? 8 : reticle.height - 13
color: Theme.accent
}
}
}
Text {
anchors.top: reticle.bottom
anchors.topMargin: 14
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 34
horizontalAlignment: Text.AlignHCenter
text: root.selectedApp ? root.selectedApp.name : "NO TARGET"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 19
font.bold: true
font.letterSpacing: 1
elide: Text.ElideRight
}
MicroText {
id: genericLabel
anchors.top: reticle.bottom
anchors.topMargin: 42
x: 18
width: parent.width - 36
horizontalAlignment: Text.AlignHCenter
text: root.selectedApp ? (root.selectedApp.genericName || "DESKTOP APPLICATION") : "AWAITING SEARCH VECTOR"
color: Theme.accent
}
Rectangle {
x: 18
anchors.top: genericLabel.bottom
anchors.topMargin: 12
width: parent.width - 36
height: 1
color: Theme.hair
}
MicroText {
id: description
x: 22
anchors.top: genericLabel.bottom
anchors.topMargin: 27
width: parent.width - 44
height: 42
text: root.selectedApp ? (root.selectedApp.comment || "No application description supplied by desktop entry.") : "Enter a designation to acquire an executable target."
wrapMode: Text.Wrap
elide: Text.ElideRight
maximumLineCount: 3
horizontalAlignment: Text.AlignHCenter
}
GridLayout {
id: telemetryGrid
x: 18
anchors.top: description.bottom
anchors.topMargin: 15
width: parent.width - 36
columns: 2
rowSpacing: 7
columnSpacing: 7
Repeater {
model: [
{ label: "ENTRY", value: "DESKTOP" },
{ label: "MODE", value: "DETACHED" },
{ label: "AUTH", value: "LOCAL USER" },
{ label: "INDEX", value: String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0") }
]
Rectangle {
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: 46
color: Theme.textAlpha(0.025)
border.width: 1
border.color: Theme.hair
MicroText { x: 8; y: 7; width: parent.width - 16; text: modelData.label }
Text {
x: 8; y: 22; width: parent.width - 16
text: modelData.value
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 9
font.bold: true
font.letterSpacing: 0.6
elide: Text.ElideRight
}
}
}
}
Item {
x: 18
anchors.bottom: executeTile.top
anchors.bottomMargin: 12
width: parent.width - 36
height: 18
Row {
spacing: 5
Repeater {
model: 24
Rectangle {
required property int index
width: 7; height: 3
y: index % 3 === 0 ? 0 : 4
color: index < 16 ? Theme.accent : Theme.hair
transform: Rotation { angle: -35 }
}
}
}
}
MouseArea {
id: executeTile
x: 18
anchors.bottom: parent.bottom
anchors.bottomMargin: 14
width: parent.width - 36
height: 42
enabled: root.selectedApp !== null
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.launchRequested(root.selectedIndex)
Rectangle {
anchors.fill: parent
color: executeTile.containsMouse ? Theme.accent : Theme.accentAlpha(0.16)
border.width: 1
border.color: Theme.accent
Text {
anchors.centerIn: parent
text: root.selectedApp ? "EXECUTE SELECTED TARGET" : "NO TARGET ACQUIRED"
color: executeTile.containsMouse ? Theme.surface : Theme.accent
font.family: Theme.displayFont
font.pixelSize: 10
font.bold: true
font.letterSpacing: 1.2
}
}
}
}
// Dense command footer.
StatusBarPanel {
x: 18
y: 550
width: 1044
height: 52
showHeader: false
chamfer: 9
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 18
MicroText { text: "INPUT CHANNEL // KEYBOARD"; color: Theme.accent }
Rectangle { Layout.preferredWidth: 1; Layout.preferredHeight: 22; color: Theme.hair }
KeyHint { keyText: "↑ ↓"; actionText: "SELECT" }
KeyHint { keyText: "ENTER"; actionText: "EXECUTE" }
KeyHint { keyText: "ESC"; actionText: "ABORT" }
Item { Layout.fillWidth: true }
MicroText { text: "QRY:" + (root.query.length > 0 ? "ACTIVE" : "IDLE") }
MicroText { text: "CRC // A7F2" }
Rectangle {
Layout.preferredWidth: 76
Layout.preferredHeight: 4
color: Theme.accent
}
}
}
}
}
+1 -1
View File
@@ -198,7 +198,7 @@ in
# quickshell app-launcher variants (evaluating — pick one)
]
++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 7))
++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 8))
++ [
# restart quickshell (also starts it if not running)
(bind "SUPER + CTRL + 0" (dsp.exec "qs kill; sleep 0.3; qs"))