sway:
- config: re-apply shikane profile after every reload
(exec_always shikanectl reload); restart waybar on reload
(exec_always pkill+waybar); enable default_border pixel 2;
add tlpctl power-saver at startup
- keybinds: disable floating_modifier (was "$mod normal") -
experiment for windows popping out via accidental mod+click
- windows: case-insensitive pydm title match for floating rule
- swayfx + waybar style: unified 5-color palette (base00-0F) with
doc headers; waybar theme restyled to match the sway palette
- waybar config: scratchpad module in left bar, "S{count}" format
- new quickshell shell.qml bar
systemd user units:
- shikane.service: bind to graphical-session/sway-session targets,
Type=simple, Restart=on-failure (2s) with start limits so crashes
recover without giving up permanently
- new sway-session.target and its wants/ symlinks
opencode:
- load AGENTS.md as instructions (new file)
- move OpenWebUI API key from hardcoded to {env:OPENWEBUI_API_KEY}
357 lines
12 KiB
QML
357 lines
12 KiB
QML
import Quickshell._Window
|
|
import Quickshell.I3
|
|
import Quickshell.Services.UPower
|
|
import Quickshell.Services.Pipewire
|
|
import Quickshell.Services.SystemTray
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls as QQC2
|
|
|
|
FloatingWindow {
|
|
id: root
|
|
title: "Quickshell Bar"
|
|
visible: true
|
|
implicitWidth: 1920
|
|
implicitHeight: 40
|
|
|
|
property string mode: ""
|
|
property string windowTitle: ""
|
|
|
|
Component.onCompleted: {
|
|
I3.rawEvent.connect(handleRawEvent)
|
|
pollWindowTitle()
|
|
pinBar()
|
|
}
|
|
|
|
function pinBar() {
|
|
I3.dispatch('[title="Quickshell Bar"] floating enable')
|
|
I3.dispatch('[title="Quickshell Bar"] move absolute position 0 0')
|
|
I3.dispatch('[title="Quickshell Bar"] resize set 1920 40')
|
|
I3.dispatch('[title="Quickshell Bar"] sticky enable')
|
|
}
|
|
|
|
function handleRawEvent(event) {
|
|
if (event.type === "mode") {
|
|
root.mode = event.data === "default" ? "" : event.data
|
|
}
|
|
if (event.type === "window" || event.type === "workspace") {
|
|
pollWindowTitle()
|
|
}
|
|
}
|
|
|
|
function pollWindowTitle() {
|
|
try {
|
|
var ws = I3.focusedWorkspace
|
|
if (ws && ws.lastIpcObject) {
|
|
root.windowTitle = findFocusedTitle(ws.lastIpcObject.nodes || [])
|
|
}
|
|
} catch (e) { root.windowTitle = "" }
|
|
}
|
|
|
|
function findFocusedTitle(nodes) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var n = nodes[i]
|
|
if (n.focused && n.name && !["$external", "i3bar"].includes(n.name)) {
|
|
return n.name
|
|
}
|
|
if (n.nodes && findFocusedTitle(n.nodes)) return findFocusedTitle(n.nodes)
|
|
if (n.floating_nodes) {
|
|
for (var j = 0; j < n.floating_nodes.length; j++) {
|
|
var fn = n.floating_nodes[j]
|
|
if (fn.focused && fn.name && fn.name !== "$external") return fn.name
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function wsColor(num) {
|
|
var colors = ["#eb4d4b", "#64727D", "#f7ca18", "#fb6f92",
|
|
"#2ecc71", "#2980b9", "#f7768e", "#667581"]
|
|
return colors[num % 8] || colors[0]
|
|
}
|
|
|
|
function updateBattery() {
|
|
if (!UPower.displayDevice || !UPower.displayDevice.isReady ||
|
|
!UPower.displayDevice.powerSupply) {
|
|
batteryLabel.text = ""
|
|
return
|
|
}
|
|
var dev = UPower.displayDevice
|
|
var pct = Math.round(dev.percentage)
|
|
var state = dev.state
|
|
batteryItem.isCritical = pct <= 15 && state === 2
|
|
|
|
if (state === 4) { batteryLabel.text = pct + "%"; return }
|
|
|
|
var timeStr = ""
|
|
var seconds = 0
|
|
if (state === 1 || state === 5) seconds = dev.timeToFull * 60
|
|
else if (state === 2 || state === 6) seconds = dev.timeToEmpty * 60
|
|
|
|
if (seconds > 0 && seconds < 86400) {
|
|
var h = Math.floor(seconds / 3600)
|
|
var m = Math.floor((seconds % 3600) / 60)
|
|
timeStr = h > 0 ? h + ":" + m.toString().padStart(2, '0') + " " : m + "m "
|
|
}
|
|
|
|
var suffix = ""
|
|
if (state === 1 || state === 5) suffix = " C"
|
|
else if (state === 3) suffix = " P"
|
|
|
|
batteryLabel.text = timeStr + pct + "%" + suffix
|
|
}
|
|
|
|
// Background
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: "black"
|
|
z: 0
|
|
}
|
|
|
|
// Bar content
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 4
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
spacing: 2
|
|
z: 1
|
|
|
|
// LEFT SECTION
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.maximumWidth: root.implicitWidth * 0.33
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 2
|
|
|
|
// Workspaces
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.maximumWidth: 600
|
|
spacing: 2
|
|
|
|
Repeater {
|
|
model: I3.workspaces
|
|
|
|
delegate: QQC2.ToolButton {
|
|
id: wsBtn
|
|
property bool isFocused: modelData.focused
|
|
property int wsNum: modelData.number
|
|
|
|
text: modelData.name
|
|
visible: modelData.active || modelData.focused || modelData.urgent
|
|
highlighted: isFocused
|
|
checkable: true
|
|
checked: isFocused
|
|
down: isFocused
|
|
|
|
onClicked: {
|
|
if (modelData.activate) modelData.activate()
|
|
else I3.dispatch(`workspace ${modelData.name}`)
|
|
}
|
|
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
width: implicitWidth + 8
|
|
padding: 0
|
|
|
|
palette {
|
|
button: isFocused ? wsColor(wsNum) : "transparent"
|
|
buttonText: "#ffffff"
|
|
}
|
|
|
|
// Hover underline
|
|
background: Rectangle {
|
|
anchors.fill: parent
|
|
color: "transparent"
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 3
|
|
color: wsBtn.hovered || wsBtn.isFocused ? "#ffffff" : "transparent"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mode indicator
|
|
QQC2.Label {
|
|
text: root.mode
|
|
visible: root.mode !== ""
|
|
color: "#64727D"
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
font.italic: true
|
|
padding: 10
|
|
}
|
|
|
|
// Window title
|
|
QQC2.Label {
|
|
text: root.windowTitle
|
|
visible: root.windowTitle !== ""
|
|
Layout.fillWidth: true
|
|
Layout.maximumWidth: 300
|
|
elide: Text.ElideMiddle
|
|
color: "#ffffff"
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
padding: 10
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spacer
|
|
Item { Layout.fillWidth: true }
|
|
|
|
// RIGHT SECTION
|
|
RowLayout {
|
|
spacing: 2
|
|
|
|
// Volume
|
|
Item {
|
|
height: parent.height
|
|
|
|
QQC2.Label {
|
|
id: volumeLabel
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: "#ffffff"
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
text: {
|
|
if (!Pipewire.ready || !Pipewire.defaultAudioSink) return ""
|
|
var sink = Pipewire.defaultAudioSink
|
|
var vol = sink.audio ? sink.audio.averageVolume * 100 : 0
|
|
var muted = sink.audio ? sink.audio.isMuted : false
|
|
return muted ? "Muted" : Math.round(vol) + "%"
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: Qt.shellCommand("pavucontrol")
|
|
onWheel: function(event) {
|
|
if (Pipewire.ready && Pipewire.defaultAudioSink &&
|
|
Pipewire.defaultAudioSink.audio) {
|
|
var v = Pipewire.defaultAudioSink.audio.averageVolume
|
|
var step = event.angleDelta.y > 0 ? 0.05 : -0.05
|
|
Pipewire.defaultAudioSink.audio.averageVolume =
|
|
Math.max(0, Math.min(1, v + step))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Battery
|
|
Item {
|
|
id: batteryItem
|
|
height: parent.height
|
|
property bool isCritical: false
|
|
|
|
QQC2.Label {
|
|
id: batteryLabel
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: batteryItem.isCritical ? "#f53c3c" : "#ffffff"
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: root.updateBattery()
|
|
}
|
|
|
|
Component.onCompleted: root.updateBattery()
|
|
}
|
|
|
|
// Clock
|
|
Item {
|
|
height: parent.height
|
|
|
|
QQC2.Label {
|
|
id: clockLabel
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: "#ffffff"
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Timer {
|
|
interval: 1000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
var now = new Date()
|
|
var h = now.getHours()
|
|
var ampm = h >= 12 ? "PM" : "AM"
|
|
h = h % 12 || 12
|
|
var time = h + ":" + now.getMinutes().toString().padStart(2, '0') +
|
|
":" + now.getSeconds().toString().padStart(2, '0') + " " + ampm
|
|
var date = now.getFullYear() + "-" +
|
|
(now.getMonth()+1).toString().padStart(2, '0') + "-" +
|
|
now.getDate().toString().padStart(2, '0')
|
|
clockLabel.text = time + " " + date
|
|
}
|
|
}
|
|
}
|
|
|
|
// System Tray
|
|
Item {
|
|
id: trayContainer
|
|
height: parent.height
|
|
|
|
Repeater {
|
|
model: SystemTray.items
|
|
|
|
delegate: Item {
|
|
width: 24
|
|
height: trayContainer.height
|
|
|
|
IconImage {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 2
|
|
width: 22
|
|
height: 22
|
|
source: modelData.iconName || ""
|
|
visible: source !== ""
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: function(mouse) {
|
|
if (modelData.hasMenu) {
|
|
modelData.display(QsWindowAttached.window, 0, 0)
|
|
} else {
|
|
modelData.activate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |