2 Commits

Author SHA1 Message Date
c4b4a3bd26 added quickshell config 2026-08-24 10:03:59 +01:00
866ee0bb9b Merge pull request 'features/updating_fonts' (#1) from features/updating_fonts into main
Reviewed-on: #1
2026-08-21 09:22:44 +01:00
14 changed files with 573 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
pragma Singleton
import QtQuick
QtObject {
readonly property string fontFamily: "Noto Sans"
readonly property string iconFontFamily: "Material Symbols Outlined"
// Material Symbols is a variable font. Left unset, glyphs fall back to the
// font's default optical size (24px), which is tuned for larger rendering
// and makes some icons (e.g. "lock") look heavier/bigger than others when
// drawn small — pin "opsz" near the actual render size to keep icons
// visually consistent. See Appearance.iconSize for the matching pixelSize.
readonly property var iconFontAxes: ({ "FILL": 0, "wght": 400, "GRAD": 0, "opsz": 20 })
readonly property int iconSize: 16
readonly property int fontSize: 13
readonly property int fontSizeSmall: 11
readonly property int radius: 14
readonly property int radiusSmall: 8
readonly property int barHeight: 34
readonly property int spacing: 10
readonly property int animationDuration: 150
}

View File

@@ -0,0 +1,59 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Wallpaper-driven palette, sourced from pywal's ~/.cache/wal/colors.json.
// Run `wal -i /path/to/wallpaper.png` to (re)generate it — this file watches
// the cache and updates live, no shell restart needed. Until that's been run
// once, everything falls back to a hand-picked dark palette below.
QtObject {
id: root
readonly property bool active: _parsed !== null
property var _parsed: null
// --- Fallback palette (used until pywal has run) ---
readonly property color fallbackBg: "#1a1b26"
readonly property color fallbackFg: "#c0caf5"
readonly property var fallbackPalette: [
"#15161e", "#f7768e", "#9ece6a", "#e0af68",
"#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6",
"#414868", "#f7768e", "#9ece6a", "#e0af68",
"#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"
]
function paletteColor(i) {
if (_parsed && _parsed.colors)
return _parsed.colors["color" + i] || fallbackPalette[i]
return fallbackPalette[i]
}
readonly property color background: _parsed && _parsed.special ? _parsed.special.background : fallbackBg
readonly property color foreground: _parsed && _parsed.special ? _parsed.special.foreground : fallbackFg
readonly property color primary: paletteColor(4)
// Named primaryText (not "onPrimary") because QML treats any "onXxx"
// identifier as a signal handler slot, not a regular property.
readonly property color primaryText: background
readonly property color urgent: paletteColor(1)
readonly property color muted: paletteColor(8)
readonly property color surface: Qt.lighter(background, 1.35)
// QtObject has no default property, so the FileView is attached via a
// property assignment rather than as a plain child element.
readonly property FileView _walFile: FileView {
path: Quickshell.env("HOME") + "/.cache/wal/colors.json"
watchChanges: true
onLoaded: {
try {
root._parsed = JSON.parse(text())
} catch (e) {
console.warn("Colors: failed to parse pywal cache:", e)
root._parsed = null
}
}
onLoadFailed: (error) => {
root._parsed = null
}
}
}

View File

@@ -0,0 +1,3 @@
module config
singleton Colors 1.0 Colors.qml
singleton Appearance 1.0 Appearance.qml

View File

@@ -0,0 +1,13 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import "../../config"
Text {
Layout.maximumWidth: 320
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
text: Hyprland.activeToplevel ? Hyprland.activeToplevel.title : ""
}

View File

@@ -0,0 +1,55 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import "../../config"
PanelWindow {
id: bar
anchors {
top: true
left: true
right: true
}
implicitHeight: Appearance.barHeight
exclusiveZone: implicitHeight
color: "transparent"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "quickshell:bar"
Rectangle {
anchors.fill: parent
color: Colors.background
}
RowLayout {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: Appearance.spacing
}
spacing: Appearance.spacing
Workspaces { screen: bar.screen }
ActiveWindow {}
}
Clock {
anchors.centerIn: parent
}
RowLayout {
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Appearance.spacing
}
spacing: Appearance.spacing
Tray {}
NetworkIndicator {}
BluetoothIndicator {}
Volume {}
PowerButton {}
}
}

View File

@@ -0,0 +1,38 @@
import QtQuick
import Quickshell.Bluetooth
import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitHeight: Appearance.barHeight
readonly property var adapter: Bluetooth.defaultAdapter
readonly property bool anyConnected: {
if (!adapter) return false
const devices = adapter.devices.values
for (let i = 0; i < devices.length; i++) {
if (devices[i].connected) return true
}
return false
}
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: (root.adapter && root.adapter.enabled) ? Colors.foreground : Colors.muted
text: {
if (!root.adapter || !root.adapter.enabled) return "bluetooth_disabled"
return root.anyConnected ? "bluetooth_connected" : "bluetooth"
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: if (root.adapter) root.adapter.enabled = !root.adapter.enabled
}
}

View File

@@ -0,0 +1,18 @@
import QtQuick
import "../../config"
Text {
id: root
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: true
color: Colors.foreground
text: Qt.formatDateTime(new Date(), "ddd d MMM hh:mm")
Timer {
interval: 1000
running: true
repeat: true
onTriggered: root.text = Qt.formatDateTime(new Date(), "ddd d MMM hh:mm")
}
}

View File

@@ -0,0 +1,35 @@
import QtQuick
import Quickshell.Networking
import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitHeight: Appearance.barHeight
readonly property var connectedDevice: {
const devices = Networking.devices.values
for (let i = 0; i < devices.length; i++) {
if (devices[i].connected)
return devices[i]
}
return null
}
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: root.connectedDevice ? Colors.foreground : Colors.muted
text: {
if (!root.connectedDevice)
return "wifi_off"
return root.connectedDevice.type === DeviceType.Wired ? "settings_ethernet" : "wifi"
}
}
// TODO(follow-up): wire this up to a network panel (SSID list, connect/forget)
// once the control center lands.
}

View File

@@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitHeight: Appearance.barHeight
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.foreground
text: "power_settings_new"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: menu.visible = !menu.visible
}
Process { id: proc }
PopupWindow {
id: menu
visible: false
color: "transparent"
implicitWidth: 170
implicitHeight: list.model.length * 32 + (list.model.length - 1) * 2 + 16
anchor {
item: root
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.top: 4
}
Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: Colors.surface
border.color: Colors.muted
border.width: 1
Column {
anchors.fill: parent
anchors.margins: 8
spacing: 2
Repeater {
id: list
model: [
// "iconSize" is optional — omit it to use the 15px default below.
// Material Symbols glyphs aren't all the same visual weight at a
// given size (a padlock silhouette reads bulkier than a thin arrow),
// so nudge individual icons here to balance them by eye.
{ label: "Lock", icon: "lock", iconSize: 13, cmd: ["hyprlock"] },
{ label: "Log out", icon: "logout", cmd: ["hyprctl", "dispatch", "exit"] },
{ label: "Reboot", icon: "restart_alt", cmd: ["systemctl", "reboot"] },
{ label: "Shut down", icon: "power_settings_new", cmd: ["systemctl", "poweroff"] }
]
delegate: Rectangle {
id: entry
required property var modelData
width: parent ? parent.width : 0
height: 32
radius: Appearance.radiusSmall
color: entryMouse.containsMouse ? Colors.primary : "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 8
Text {
Layout.preferredWidth: 20
horizontalAlignment: Text.AlignHCenter
text: entry.modelData.icon
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: entry.modelData.iconSize || 15
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
text: entry.modelData.label
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
}
MouseArea {
id: entryMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
proc.command = entry.modelData.cmd
proc.running = true
menu.visible = false
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config"
RowLayout {
spacing: Appearance.spacing * 0.6
Repeater {
model: SystemTray.items
delegate: IconImage {
id: trayIcon
required property var modelData
implicitWidth: 16
implicitHeight: 16
source: modelData.icon
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton)
trayIcon.modelData.activate()
else
trayIcon.modelData.secondaryActivate()
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
import QtQuick
import Quickshell.Services.Pipewire
import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitHeight: Appearance.barHeight
readonly property var sink: Pipewire.defaultAudioSink
readonly property real vol: (sink && sink.audio) ? sink.audio.volume : 0
readonly property bool muted: (sink && sink.audio) ? sink.audio.muted : false
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.foreground
text: {
if (root.muted || root.vol <= 0) return "volume_off"
if (root.vol < 0.5) return "volume_down"
return "volume_up"
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: if (root.sink && root.sink.audio) root.sink.audio.muted = !root.sink.audio.muted
onWheel: (wheel) => {
if (!root.sink || !root.sink.audio) return
const step = 0.05
const delta = wheel.angleDelta.y > 0 ? step : -step
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + delta))
}
}
}

View File

@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import "../../config"
RowLayout {
id: root
property var screen
spacing: 4
Repeater {
model: Hyprland.workspaces
delegate: Rectangle {
id: pill
required property var modelData
visible: modelData.monitor && root.screen && modelData.monitor.name === root.screen.name
implicitWidth: 26
implicitHeight: 22
radius: Appearance.radiusSmall
color: modelData.urgent ? Colors.urgent
: modelData.active ? Colors.primary
: "transparent"
Behavior on color { ColorAnimation { duration: Appearance.animationDuration } }
Text {
anchors.centerIn: parent
text: modelData.name
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: modelData.active ? Colors.primaryText : Colors.foreground
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Hyprland.dispatch("workspace " + modelData.name)
}
}
}
}

View File

@@ -0,0 +1,78 @@
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import "../../config"
PanelWindow {
id: osd
visible: false
color: "transparent"
exclusiveZone: 0
anchors { bottom: true }
margins.bottom: 70
implicitWidth: 240
implicitHeight: 54
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell:osd"
readonly property var sink: Pipewire.defaultAudioSink
PwObjectTracker { objects: osd.sink ? [osd.sink] : [] }
Timer {
id: hideTimer
interval: 1400
onTriggered: osd.visible = false
}
Connections {
target: osd.sink ? osd.sink.audio : null
function onVolumeChanged() { osd.visible = true; hideTimer.restart() }
function onMutedChanged() { osd.visible = true; hideTimer.restart() }
}
Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: Colors.surface
border.color: Colors.muted
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 12
Text {
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
color: Colors.foreground
text: {
const audio = osd.sink ? osd.sink.audio : null
if (!audio || audio.muted || audio.volume <= 0) return "volume_off"
return audio.volume < 0.5 ? "volume_down" : "volume_up"
}
}
Rectangle {
Layout.fillWidth: true
height: 6
radius: 3
color: Colors.muted
Rectangle {
height: parent.height
radius: 3
color: Colors.primary
width: parent.width * (osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0)
Behavior on width { NumberAnimation { duration: Appearance.animationDuration } }
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
import Quickshell
import "./modules/bar"
import "./modules/osd"
ShellRoot {
Variants {
model: Quickshell.screens
Bar {
required property var modelData
screen: modelData
}
}
VolumeOsd {}
}