Something went wrong. Try again.
dope ass quickshell bar
Something went wrong. Try again.
quickshell NiriWindowList.qml
18 kB · 478 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479import QtQuick 2.15import Quickshell.Io
// Left section of the bar: a vertically-sliding strip of workspace/window pills.// Renders the currently displayed workspace's pills plus ↑/↓ affordances into adjacent// workspaces, and animates a vertical slide when the active workspace changes.Item { id: windowList
// --- inputs (fed by Bar / shell state) --- property var niriWindowsList: [] property var niriWorkspacesList: [] property string outputName: "" // this monitor's niri output name property color barColor: Config.color("background") // bar background, used for 1px pill dividers
clip: true implicitHeight: stripHeight implicitWidth: Math.max( aboveSectionLoader.item ? aboveSectionLoader.item.width : 0, currentSectionLoader.item ? currentSectionLoader.item.width : 0, belowSectionLoader.item ? belowSectionLoader.item.width : 0 )
property var activeWorkspace: niriWorkspacesList.find( function(ws) { return ws.output === windowList.outputName && ws.is_active }) || null
// shared font metrics for measuring pill widths so the highlight knows where to slide FontMetrics { id: pillFm font.family: "Inter" font.pixelSize: 13 font.weight: Font.Bold }
function pillWidth(appId) { return pillFm.advanceWidth(appId ?? "") + 16 }
readonly property int stripHeight: 26
// What workspace's pills are currently shown in the "current" slot of the strip stack. // Lags activeWorkspace.id during a slide, so the strip can render the OLD state // while sliding to reveal the NEW workspace (rendered in the above/below slot). property int displayedWorkspaceId: -1
property var displayedWorkspace: niriWorkspacesList.find( function(ws) { return ws.id === displayedWorkspaceId }) || null
property int aboveAdjWorkspaceId: { if (!displayedWorkspace) return -1 var ws = niriWorkspacesList.find(function(w) { return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx - 1 }) return ws ? ws.id : -1 }
property int belowAdjWorkspaceId: { if (!displayedWorkspace) return -1 var ws = niriWorkspacesList.find(function(w) { return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx + 1 }) return ws ? ws.id : -1 }
// Vertical translation of the strip stack during a transition. // Steady state = 0. +stripHeight = above slot in view. -stripHeight = below slot in view. property real transitionOffset: 0
Component.onCompleted: { if (activeWorkspace) displayedWorkspaceId = activeWorkspace.id }
// Drive the slide animation from displayedWorkspaceId toward activeWorkspace.id. // If a slide is already in flight, leave it alone; onStopped will re-invoke us to // chain a new slide from the just-reached workspace to whatever the latest active // workspace is. That way rapid switches turn into a sequence of slides instead of // a single slide followed by a snap-cut. function syncWorkspaceAnim() { if (!activeWorkspace) return var newId = activeWorkspace.id if (displayedWorkspaceId === -1) { displayedWorkspaceId = newId return } if (displayedWorkspaceId === newId) return if (slideAnim.running) return var oldWs = niriWorkspacesList.find(function(ws) { return ws.id === displayedWorkspaceId }) if (!oldWs || oldWs.output !== activeWorkspace.output) { displayedWorkspaceId = newId return } // niri slides screen DOWN when focusing workspace UP -> stack offset goes +. slideAnim.goingUp = activeWorkspace.idx < oldWs.idx slideAnim.targetWsId = newId slideAnim.start() }
onActiveWorkspaceChanged: syncWorkspaceAnim()
NumberAnimation { id: slideAnim target: windowList property: "transitionOffset" duration: 200 easing.type: Easing.OutCubic property bool goingUp: false // The workspace this slide is animating *to*. Tracked explicitly so onStopped // doesn't snap straight to the latest activeWorkspace (which can race ahead // mid-flight during rapid switching). property int targetWsId: -1 to: goingUp ? windowList.stripHeight : -windowList.stripHeight onStopped: { // Land on this slide's intended target, then re-evaluate: if the active // workspace has moved further while we were animating, syncWorkspaceAnim // kicks off the next slide from here. windowList.displayedWorkspaceId = targetWsId windowList.transitionOffset = 0 windowList.syncWorkspaceAnim() } }
function windowsForWorkspace(wsId) { var wins = niriWindowsList.filter(function(w) { return w.workspace_id === wsId }) wins.sort(function(a, b) { var ap = a.layout && a.layout.pos_in_scrolling_layout var bp = b.layout && b.layout.pos_in_scrolling_layout if (ap && bp) return ap[0] !== bp[0] ? ap[0] - bp[0] : ap[1] - bp[1] return 0 }) return wins }
// workspaces above a given workspace, farthest -> closest function workspacesAbove(wsId) { var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) if (!ws) return [] return niriWorkspacesList .filter(function(w) { return w.output === ws.output && w.idx < ws.idx }) .sort(function(a, b) { return a.idx - b.idx }) }
// workspaces below a given workspace, closest -> farthest function workspacesBelow(wsId) { var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) if (!ws) return [] return niriWorkspacesList .filter(function(w) { return w.output === ws.output && w.idx > ws.idx }) .sort(function(a, b) { return a.idx - b.idx }) }
Process { id: focusWindowProcess property int targetId: 0 command: ["niri", "msg", "action", "focus-window", "--id", targetId.toString()] }
Process { id: focusWorkspaceUp command: ["niri", "msg", "action", "focus-workspace-up"] }
Process { id: focusWorkspaceDown command: ["niri", "msg", "action", "focus-workspace-down"] }
// A full duplicate of the bar's left section, rendered for a given workspace. // Includes: bg + focus highlight + current-workspace pills + ↑ + above pills + ↓ + below pills. // Three instances live in the clipped viewport (above / current / below) and slide vertically // on workspace change. Component { id: leftSectionComp
Item { id: section property int displayWsId: -1 property var displayWs: niriWorkspacesList.find(function(ws) { return ws.id === displayWsId }) || null property bool isCurrent: displayWsId === (windowList.activeWorkspace ? windowList.activeWorkspace.id : -1)
// Briefly suppresses the highlight's slide Behavior whenever this section's // workspace changes (e.g., the Loader rebinds after a slide ends). Without this, // the highlight would animate from the OLD workspace's focused position to the // NEW one's across now-different pills, producing the "second" animation. property bool _allowHighlightAnim: true onDisplayWsIdChanged: { _allowHighlightAnim = false Qt.callLater(function() { section._allowHighlightAnim = true }) }
// pills for the workspace this section represents property var currentPills: { if (displayWsId < 0) return [] return windowsForWorkspace(displayWsId).map(function(w) { return Object.assign({}, w, {inViewport: w.is_focused, onCurrentWorkspace: section.isCurrent}) }) }
// flat list of windows from all workspaces above this section's workspace property var aboveWinsList: { if (!displayWs) return [] var result = [] workspacesAbove(displayWsId).forEach(function(ws) { result = result.concat(windowsForWorkspace(ws.id)) }) return result }
// flat list of windows from all workspaces below this section's workspace property var belowWinsList: { if (!displayWs) return [] var result = [] workspacesBelow(displayWsId).forEach(function(ws) { result = result.concat(windowsForWorkspace(ws.id)) }) return result }
// focus highlight is meaningful only for the workspace that actually has the // globally-focused window (niri marks exactly one window is_focused) property int focusedIdx: { for (var i = 0; i < currentPills.length; i++) { if (currentPills[i].inViewport) return i } return -1 }
property real focusedX: { if (focusedIdx < 0) return 0 var x = 0 for (var i = 0; i < focusedIdx; i++) { x += pillWidth(currentPills[i].app_id) } return x }
property real focusedW: { if (focusedIdx < 0) return 0 return pillWidth(currentPills[focusedIdx].app_id) }
height: windowList.stripHeight width: sectionRow.width
// bg strip behind everything Rectangle { anchors.fill: parent color: Config.color("surface") }
// focus highlight. Slides between pills within a workspace, but snaps during // workspace transitions (controlled by _allowHighlightAnim and slideAnim.running). Rectangle { visible: section.focusedIdx >= 0 x: section.focusedX width: section.focusedW height: windowList.stripHeight color: Config.color("focus") border.color: Config.color("focusBorder") border.width: 1 Behavior on x { enabled: section._allowHighlightAnim && !slideAnim.running NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } Behavior on width { enabled: section._allowHighlightAnim && !slideAnim.running NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } }
Row { id: sectionRow spacing: 0 height: windowList.stripHeight
Repeater { model: section.currentPills delegate: windowPill }
// ↑ arrow with 1px bar-bg dividers Item { visible: section.aboveWinsList.length > 0 width: visible ? 28 : 0 height: parent.height
Rectangle { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 1 height: 26 color: windowList.barColor } Rectangle { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: 1 height: 26 color: windowList.barColor }
Rectangle { anchors.centerIn: parent width: 26 height: 26 radius: 0 color: upArea.containsMouse ? Config.color("hover") : Config.color("transparent") border.color: Config.color("transparent") border.width: 1
Text { anchors.centerIn: parent text: "↑" color: Config.color("muted") font.family: "Inter" font.pixelSize: 14 font.weight: 700 } }
MouseArea { id: upArea anchors.fill: parent hoverEnabled: true onClicked: focusWorkspaceUp.running = true } }
Repeater { model: section.aboveWinsList delegate: windowPill }
// ↓ arrow with 1px bar-bg dividers Item { visible: section.belowWinsList.length > 0 width: visible ? 28 : 0 height: parent.height
Rectangle { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 1 height: 26 color: windowList.barColor } Rectangle { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: 1 height: 26 color: windowList.barColor }
Rectangle { anchors.centerIn: parent width: 26 height: 26 radius: 0 color: downArea.containsMouse ? Config.color("hover") : Config.color("transparent") border.color: Config.color("transparent") border.width: 1
Text { anchors.centerIn: parent text: "↓" color: Config.color("muted") font.family: "Inter" font.pixelSize: 14 font.weight: 700 } }
MouseArea { id: downArea anchors.fill: parent hoverEnabled: true onClicked: focusWorkspaceDown.running = true } }
Repeater { model: section.belowWinsList delegate: windowPill } } } }
Component { id: windowPill
Item { id: windowItem required property var modelData height: parent.height width: winLabel.implicitWidth + 16
Rectangle { anchors.centerIn: parent width: winLabel.implicitWidth + 16 height: 26 radius: 0 color: (!windowItem.modelData.inViewport && hoverArea.containsMouse) ? Config.color("hover") : Config.color("transparent") border.color: Config.color("transparent") border.width: 1
Text { id: winLabel anchors.centerIn: parent text: windowItem.modelData.app_id ?? "" color: windowItem.modelData.inViewport ? Config.color("foreground") : (windowItem.modelData.onCurrentWorkspace ? Config.color("secondaryText") : Config.color("muted")) font.family: "Inter" font.pixelSize: 13 font.weight: 700 } }
MouseArea { id: hoverArea anchors.fill: parent hoverEnabled: true onClicked: { focusWindowProcess.targetId = windowItem.modelData.id focusWindowProcess.running = true } } } }
// Clipped viewport showing one strip's worth of vertical space. Inside, a stack of // three full leftSectionComp instances (above / current / below) is translated up/down // on workspace change to reveal the adjacent section. Item { id: sectionStack anchors.left: parent.left y: windowList.transitionOffset height: parent.height width: parent.width
Loader { id: aboveSectionLoader sourceComponent: leftSectionComp y: -windowList.stripHeight Binding { target: aboveSectionLoader.item property: "displayWsId" value: windowList.aboveAdjWorkspaceId when: aboveSectionLoader.item !== null } }
Loader { id: currentSectionLoader sourceComponent: leftSectionComp y: 0 Binding { target: currentSectionLoader.item property: "displayWsId" value: windowList.displayedWorkspaceId when: currentSectionLoader.item !== null } }
Loader { id: belowSectionLoader sourceComponent: leftSectionComp y: windowList.stripHeight Binding { target: belowSectionLoader.item property: "displayWsId" value: windowList.belowAdjWorkspaceId when: belowSectionLoader.item !== null } } }}