unify color palette, harden shikane service, misc sway/opencode updates
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}
This commit is contained in:
1 parent
e338d15922
commit
63345e51e3
12 files changed
+623
-206
No files matched your search
@@ -0,0 +1,357 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user