dope ass quickshell bar
0

Configure Feed

Select the types of activity you want to include in your feed.

animate

Callie Peden (Jun 29, 2026, 9:31 PM EDT) ae78f048 eb20f6b9

+355 -193
+355 -193
Bar.qml
··· 41 41 return pillFm.advanceWidth(appId ?? "") + 16 42 42 } 43 43 44 - property int focusedWindowIndex: { 45 - var wins = root.monitorWindows 46 - for (var i = 0; i < wins.length; i++) { 47 - if (wins[i].inViewport) return i 48 - } 49 - return -1 44 + readonly property int stripHeight: 26 45 + 46 + // What workspace's pills are currently shown in the "current" slot of the strip stack. 47 + // Lags activeWorkspace.id during a slide, so the strip can render the OLD state 48 + // while sliding to reveal the NEW workspace (rendered in the above/below slot). 49 + property int displayedWorkspaceId: -1 50 + 51 + property var displayedWorkspace: niriWorkspacesList.find( 52 + function(ws) { return ws.id === displayedWorkspaceId }) || null 53 + 54 + property int aboveAdjWorkspaceId: { 55 + if (!displayedWorkspace) return -1 56 + var ws = niriWorkspacesList.find(function(w) { 57 + return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx - 1 58 + }) 59 + return ws ? ws.id : -1 60 + } 61 + 62 + property int belowAdjWorkspaceId: { 63 + if (!displayedWorkspace) return -1 64 + var ws = niriWorkspacesList.find(function(w) { 65 + return w.output === displayedWorkspace.output && w.idx === displayedWorkspace.idx + 1 66 + }) 67 + return ws ? ws.id : -1 68 + } 69 + 70 + // Vertical translation of the strip stack during a transition. 71 + // Steady state = 0. +stripHeight = above slot in view. -stripHeight = below slot in view. 72 + property real transitionOffset: 0 73 + 74 + Component.onCompleted: { 75 + if (activeWorkspace) displayedWorkspaceId = activeWorkspace.id 50 76 } 51 77 52 - property real focusedPillX: { 53 - if (root.focusedWindowIndex < 0) return 0 54 - var x = 0 55 - for (var i = 0; i < root.focusedWindowIndex; i++) { 56 - x += pillWidth(root.monitorWindows[i].app_id) 78 + // Drive the slide animation from displayedWorkspaceId toward activeWorkspace.id. 79 + // If a slide is already in flight, leave it alone; onStopped will re-invoke us to 80 + // chain a new slide from the just-reached workspace to whatever the latest active 81 + // workspace is. That way rapid switches turn into a sequence of slides instead of 82 + // a single slide followed by a snap-cut. 83 + function syncWorkspaceAnim() { 84 + if (!activeWorkspace) return 85 + var newId = activeWorkspace.id 86 + if (displayedWorkspaceId === -1) { 87 + displayedWorkspaceId = newId 88 + return 89 + } 90 + if (displayedWorkspaceId === newId) return 91 + if (slideAnim.running) return 92 + var oldWs = niriWorkspacesList.find(function(ws) { return ws.id === displayedWorkspaceId }) 93 + if (!oldWs || oldWs.output !== activeWorkspace.output) { 94 + displayedWorkspaceId = newId 95 + return 57 96 } 58 - return x 97 + // niri slides screen DOWN when focusing workspace UP -> stack offset goes +. 98 + slideAnim.goingUp = activeWorkspace.idx < oldWs.idx 99 + slideAnim.targetWsId = newId 100 + slideAnim.start() 59 101 } 60 102 61 - property real focusedPillWidth: { 62 - if (root.focusedWindowIndex < 0) return 0 63 - return pillWidth(root.monitorWindows[root.focusedWindowIndex].app_id) 64 - } 103 + onActiveWorkspaceChanged: syncWorkspaceAnim() 65 104 66 - property var monitorWindows: { 67 - if (!root.activeWorkspace) return [] 68 - var wsId = root.activeWorkspace.id 69 - var wins = niriWindowsList.filter(function(w) { return w.workspace_id === wsId }) 70 - wins.sort(function(a, b) { 71 - var ap = a.layout && a.layout.pos_in_scrolling_layout 72 - var bp = b.layout && b.layout.pos_in_scrolling_layout 73 - if (ap && bp) return ap[0] !== bp[0] ? ap[0] - bp[0] : ap[1] - bp[1] 74 - var ax = a.layout && a.layout.tile_pos_in_workspace_view ? a.layout.tile_pos_in_workspace_view[0] : 0 75 - var bx = b.layout && b.layout.tile_pos_in_workspace_view ? b.layout.tile_pos_in_workspace_view[0] : 0 76 - return ax - bx 77 - }) 78 - return wins.map(function(w) { 79 - return Object.assign({}, w, {inViewport: w.is_focused, onCurrentWorkspace: true}) 80 - }) 105 + NumberAnimation { 106 + id: slideAnim 107 + target: root 108 + property: "transitionOffset" 109 + duration: 200 110 + easing.type: Easing.OutCubic 111 + property bool goingUp: false 112 + // The workspace this slide is animating *to*. Tracked explicitly so onStopped 113 + // doesn't snap straight to the latest activeWorkspace (which can race ahead 114 + // mid-flight during rapid switching). 115 + property int targetWsId: -1 116 + to: goingUp ? root.stripHeight : -root.stripHeight 117 + onStopped: { 118 + // Land on this slide's intended target, then re-evaluate: if the active 119 + // workspace has moved further while we were animating, syncWorkspaceAnim 120 + // kicks off the next slide from here. 121 + root.displayedWorkspaceId = targetWsId 122 + root.transitionOffset = 0 123 + root.syncWorkspaceAnim() 124 + } 81 125 } 82 126 83 127 function windowsForWorkspace(wsId) { ··· 91 135 return wins 92 136 } 93 137 94 - // windows in workspaces above the current one, ordered farthest -> closest 95 - property var aboveWindows: { 96 - if (!root.activeWorkspace) return [] 97 - var output = root.screen.name 98 - var activeIdx = root.activeWorkspace.idx 99 - var aboveWs = niriWorkspacesList 100 - .filter(function(ws) { return ws.output === output && ws.idx < activeIdx }) 138 + // workspaces above a given workspace, farthest -> closest 139 + function workspacesAbove(wsId) { 140 + var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) 141 + if (!ws) return [] 142 + return niriWorkspacesList 143 + .filter(function(w) { return w.output === ws.output && w.idx < ws.idx }) 101 144 .sort(function(a, b) { return a.idx - b.idx }) 102 - var result = [] 103 - aboveWs.forEach(function(ws) { 104 - result = result.concat(windowsForWorkspace(ws.id)) 105 - }) 106 - return result 107 145 } 108 146 109 - // windows in workspaces below the current one, ordered closest -> farthest 110 - property var belowWindows: { 111 - if (!root.activeWorkspace) return [] 112 - var output = root.screen.name 113 - var activeIdx = root.activeWorkspace.idx 114 - var belowWs = niriWorkspacesList 115 - .filter(function(ws) { return ws.output === output && ws.idx > activeIdx }) 147 + // workspaces below a given workspace, closest -> farthest 148 + function workspacesBelow(wsId) { 149 + var ws = niriWorkspacesList.find(function(w) { return w.id === wsId }) 150 + if (!ws) return [] 151 + return niriWorkspacesList 152 + .filter(function(w) { return w.output === ws.output && w.idx > ws.idx }) 116 153 .sort(function(a, b) { return a.idx - b.idx }) 117 - var result = [] 118 - belowWs.forEach(function(ws) { 119 - result = result.concat(windowsForWorkspace(ws.id)) 120 - }) 121 - return result 122 154 } 123 155 124 156 Process { ··· 137 169 command: ["niri", "msg", "action", "focus-workspace-down"] 138 170 } 139 171 172 + // A full duplicate of the bar's left section, rendered for a given workspace. 173 + // Includes: bg + focus highlight + current-workspace pills + ↑ + above pills + ↓ + below pills. 174 + // Three instances live in the clipped viewport (above / current / below) and slide vertically 175 + // on workspace change. 176 + Component { 177 + id: leftSectionComp 178 + 179 + Item { 180 + id: section 181 + property int displayWsId: -1 182 + property var displayWs: niriWorkspacesList.find(function(ws) { return ws.id === displayWsId }) || null 183 + property bool isCurrent: displayWsId === (root.activeWorkspace ? root.activeWorkspace.id : -1) 184 + 185 + // Briefly suppresses the highlight's slide Behavior whenever this section's 186 + // workspace changes (e.g., the Loader rebinds after a slide ends). Without this, 187 + // the highlight would animate from the OLD workspace's focused position to the 188 + // NEW one's across now-different pills, producing the "second" animation. 189 + property bool _allowHighlightAnim: true 190 + onDisplayWsIdChanged: { 191 + _allowHighlightAnim = false 192 + Qt.callLater(function() { section._allowHighlightAnim = true }) 193 + } 194 + 195 + // pills for the workspace this section represents 196 + property var currentPills: { 197 + if (displayWsId < 0) return [] 198 + return windowsForWorkspace(displayWsId).map(function(w) { 199 + return Object.assign({}, w, {inViewport: w.is_focused, onCurrentWorkspace: section.isCurrent}) 200 + }) 201 + } 202 + 203 + // flat list of windows from all workspaces above this section's workspace 204 + property var aboveWinsList: { 205 + if (!displayWs) return [] 206 + var result = [] 207 + workspacesAbove(displayWsId).forEach(function(ws) { 208 + result = result.concat(windowsForWorkspace(ws.id)) 209 + }) 210 + return result 211 + } 212 + 213 + // flat list of windows from all workspaces below this section's workspace 214 + property var belowWinsList: { 215 + if (!displayWs) return [] 216 + var result = [] 217 + workspacesBelow(displayWsId).forEach(function(ws) { 218 + result = result.concat(windowsForWorkspace(ws.id)) 219 + }) 220 + return result 221 + } 222 + 223 + // focus highlight is meaningful only for the workspace that actually has the 224 + // globally-focused window (niri marks exactly one window is_focused) 225 + property int focusedIdx: { 226 + for (var i = 0; i < currentPills.length; i++) { 227 + if (currentPills[i].inViewport) return i 228 + } 229 + return -1 230 + } 231 + 232 + property real focusedX: { 233 + if (focusedIdx < 0) return 0 234 + var x = 0 235 + for (var i = 0; i < focusedIdx; i++) { 236 + x += pillWidth(currentPills[i].app_id) 237 + } 238 + return x 239 + } 240 + 241 + property real focusedW: { 242 + if (focusedIdx < 0) return 0 243 + return pillWidth(currentPills[focusedIdx].app_id) 244 + } 245 + 246 + height: root.stripHeight 247 + width: sectionRow.width 248 + 249 + // bg strip behind everything 250 + Rectangle { 251 + anchors.fill: parent 252 + color: "#1a1a1a" 253 + } 254 + 255 + // focus highlight. Slides between pills within a workspace, but snaps during 256 + // workspace transitions (controlled by _allowHighlightAnim and slideAnim.running). 257 + Rectangle { 258 + visible: section.focusedIdx >= 0 259 + x: section.focusedX 260 + width: section.focusedW 261 + height: root.stripHeight 262 + color: "#555" 263 + border.color: "#888" 264 + border.width: 1 265 + Behavior on x { 266 + enabled: section._allowHighlightAnim && !slideAnim.running 267 + NumberAnimation { duration: 180; easing.type: Easing.OutCubic } 268 + } 269 + Behavior on width { 270 + enabled: section._allowHighlightAnim && !slideAnim.running 271 + NumberAnimation { duration: 180; easing.type: Easing.OutCubic } 272 + } 273 + } 274 + 275 + Row { 276 + id: sectionRow 277 + spacing: 0 278 + height: root.stripHeight 279 + 280 + Repeater { 281 + model: section.currentPills 282 + delegate: windowPill 283 + } 284 + 285 + // ↑ arrow with 1px bar-bg dividers 286 + Item { 287 + visible: section.aboveWinsList.length > 0 288 + width: visible ? 28 : 0 289 + height: parent.height 290 + 291 + Rectangle { 292 + anchors.left: parent.left 293 + anchors.verticalCenter: parent.verticalCenter 294 + width: 1 295 + height: 26 296 + color: root.color 297 + } 298 + Rectangle { 299 + anchors.right: parent.right 300 + anchors.verticalCenter: parent.verticalCenter 301 + width: 1 302 + height: 26 303 + color: root.color 304 + } 305 + 306 + Rectangle { 307 + anchors.centerIn: parent 308 + width: 26 309 + height: 26 310 + radius: 0 311 + color: upArea.containsMouse ? "#2a2a2a" : "transparent" 312 + border.color: "transparent" 313 + border.width: 1 314 + 315 + Text { 316 + anchors.centerIn: parent 317 + text: "↑" 318 + color: "#888" 319 + font.family: "Inter" 320 + font.pixelSize: 14 321 + font.weight: 700 322 + } 323 + } 324 + 325 + MouseArea { 326 + id: upArea 327 + anchors.fill: parent 328 + hoverEnabled: true 329 + onClicked: focusWorkspaceUp.running = true 330 + } 331 + } 332 + 333 + Repeater { 334 + model: section.aboveWinsList 335 + delegate: windowPill 336 + } 337 + 338 + // ↓ arrow with 1px bar-bg dividers 339 + Item { 340 + visible: section.belowWinsList.length > 0 341 + width: visible ? 28 : 0 342 + height: parent.height 343 + 344 + Rectangle { 345 + anchors.left: parent.left 346 + anchors.verticalCenter: parent.verticalCenter 347 + width: 1 348 + height: 26 349 + color: root.color 350 + } 351 + Rectangle { 352 + anchors.right: parent.right 353 + anchors.verticalCenter: parent.verticalCenter 354 + width: 1 355 + height: 26 356 + color: root.color 357 + } 358 + 359 + Rectangle { 360 + anchors.centerIn: parent 361 + width: 26 362 + height: 26 363 + radius: 0 364 + color: downArea.containsMouse ? "#2a2a2a" : "transparent" 365 + border.color: "transparent" 366 + border.width: 1 367 + 368 + Text { 369 + anchors.centerIn: parent 370 + text: "↓" 371 + color: "#888" 372 + font.family: "Inter" 373 + font.pixelSize: 14 374 + font.weight: 700 375 + } 376 + } 377 + 378 + MouseArea { 379 + id: downArea 380 + anchors.fill: parent 381 + hoverEnabled: true 382 + onClicked: focusWorkspaceDown.running = true 383 + } 384 + } 385 + 386 + Repeater { 387 + model: section.belowWinsList 388 + delegate: windowPill 389 + } 390 + } 391 + } 392 + } 393 + 140 394 Component { 141 395 id: windowPill 142 396 ··· 206 460 bar: root 207 461 } 208 462 209 - // Single background strip behind the whole left Row. Pills sit transparently on top. 210 - Rectangle { 211 - id: leftStripBg 212 - x: leftRow.x 213 - width: leftRow.width 214 - height: 26 215 - anchors.verticalCenter: parent.verticalCenter 216 - color: "#1a1a1a" 217 - } 218 - 219 - // Sliding highlight that tracks the focused pill. Renders ON TOP of the strip bg 220 - // (declared after it). The focused pill's own bg/border are transparent so this shows. 221 - Rectangle { 222 - id: focusHighlight 223 - visible: root.focusedWindowIndex >= 0 463 + // Clipped viewport showing one strip's worth of vertical space. Inside, a stack of 464 + // three full leftSectionComp instances (above / current / below) is translated up/down 465 + // on workspace change to reveal the adjacent section. 466 + Item { 467 + id: leftViewport 468 + clip: true 224 469 anchors.verticalCenter: parent.verticalCenter 225 - x: leftRow.x + root.focusedPillX 226 - width: root.focusedPillWidth 227 - height: 26 228 - radius: 0 229 - color: "#555" 230 - border.color: "#888" 231 - border.width: 1 232 - 233 - Behavior on x { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } 234 - Behavior on width { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } 235 - } 236 - 237 - // Left: window list — flat strip with arrows acting as visual separators 238 - Row { 239 - id: leftRow 240 - anchors { 241 - left: parent.left 242 - leftMargin: px(10) 243 - verticalCenter: parent.verticalCenter 244 - } 245 - height: parent.height 246 - spacing: 0 247 - 248 - Repeater { 249 - model: root.monitorWindows 250 - delegate: windowPill 251 - } 470 + anchors.left: parent.left 471 + anchors.leftMargin: px(10) 472 + height: root.stripHeight 473 + width: Math.max( 474 + aboveSectionLoader.item ? aboveSectionLoader.item.width : 0, 475 + currentSectionLoader.item ? currentSectionLoader.item.width : 0, 476 + belowSectionLoader.item ? belowSectionLoader.item.width : 0 477 + ) 252 478 253 479 Item { 254 - visible: root.aboveWindows.length > 0 255 - width: visible ? 28 : 0 480 + id: sectionStack 481 + anchors.left: parent.left 482 + y: root.transitionOffset 256 483 height: parent.height 484 + width: parent.width 257 485 258 - // 1px bar-bg divider on the left side of the arrow, punches a gap into the strip 259 - Rectangle { 260 - anchors.left: parent.left 261 - anchors.verticalCenter: parent.verticalCenter 262 - width: 1 263 - height: 26 264 - color: root.color 265 - } 266 - // 1px bar-bg divider on the right side 267 - Rectangle { 268 - anchors.right: parent.right 269 - anchors.verticalCenter: parent.verticalCenter 270 - width: 1 271 - height: 26 272 - color: root.color 486 + Loader { 487 + id: aboveSectionLoader 488 + sourceComponent: leftSectionComp 489 + y: -root.stripHeight 490 + Binding { 491 + target: aboveSectionLoader.item 492 + property: "displayWsId" 493 + value: root.aboveAdjWorkspaceId 494 + when: aboveSectionLoader.item !== null 495 + } 273 496 } 274 497 275 - Rectangle { 276 - anchors.centerIn: parent 277 - width: 26 278 - height: 26 279 - radius: 0 280 - color: upArea.containsMouse ? "#2a2a2a" : "transparent" 281 - border.color: "transparent" 282 - border.width: 1 283 - 284 - Text { 285 - anchors.centerIn: parent 286 - text: "↑" 287 - color: "#888" 288 - font.family: "Inter" 289 - font.pixelSize: 14 290 - font.weight: 700 498 + Loader { 499 + id: currentSectionLoader 500 + sourceComponent: leftSectionComp 501 + y: 0 502 + Binding { 503 + target: currentSectionLoader.item 504 + property: "displayWsId" 505 + value: root.displayedWorkspaceId 506 + when: currentSectionLoader.item !== null 291 507 } 292 508 } 293 509 294 - MouseArea { 295 - id: upArea 296 - anchors.fill: parent 297 - hoverEnabled: true 298 - onClicked: focusWorkspaceUp.running = true 299 - } 300 - } 301 - 302 - Repeater { 303 - model: root.aboveWindows 304 - delegate: windowPill 305 - } 306 - 307 - Item { 308 - visible: root.belowWindows.length > 0 309 - width: visible ? 28 : 0 310 - height: parent.height 311 - 312 - // 1px bar-bg divider on the left side of the arrow 313 - Rectangle { 314 - anchors.left: parent.left 315 - anchors.verticalCenter: parent.verticalCenter 316 - width: 1 317 - height: 26 318 - color: root.color 319 - } 320 - // 1px bar-bg divider on the right side 321 - Rectangle { 322 - anchors.right: parent.right 323 - anchors.verticalCenter: parent.verticalCenter 324 - width: 1 325 - height: 26 326 - color: root.color 327 - } 328 - 329 - Rectangle { 330 - anchors.centerIn: parent 331 - width: 26 332 - height: 26 333 - radius: 0 334 - color: downArea.containsMouse ? "#2a2a2a" : "transparent" 335 - border.color: "transparent" 336 - border.width: 1 337 - 338 - Text { 339 - anchors.centerIn: parent 340 - text: "↓" 341 - color: "#888" 342 - font.family: "Inter" 343 - font.pixelSize: 14 344 - font.weight: 700 510 + Loader { 511 + id: belowSectionLoader 512 + sourceComponent: leftSectionComp 513 + y: root.stripHeight 514 + Binding { 515 + target: belowSectionLoader.item 516 + property: "displayWsId" 517 + value: root.belowAdjWorkspaceId 518 + when: belowSectionLoader.item !== null 345 519 } 346 520 } 347 - 348 - MouseArea { 349 - id: downArea 350 - anchors.fill: parent 351 - hoverEnabled: true 352 - onClicked: focusWorkspaceDown.running = true 353 - } 354 - } 355 - 356 - Repeater { 357 - model: root.belowWindows 358 - delegate: windowPill 359 521 } 360 522 } 361 523