quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,168 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.modules.functions
import qs.config
/*
This registry is only used to get app details for wm classes.
*/
Singleton {
id: registry
property var apps: []
property var classToIcon: ({})
property var desktopIdToIcon: ({})
property var nameToIcon: ({})
signal ready()
function iconForDesktopIcon(icon) {
if (!icon) return ""
// If it's already a URL, keep it
if (icon.startsWith("file://") || icon.startsWith("qrc:/"))
return icon
// Absolute filesystem path → convert to file URL
if (icon.startsWith("/"))
return "file://" + icon
// Otherwise treat as theme icon name
return Quickshell.iconPath(icon)
}
// Try very aggressive matching so the running app always gets the same icon as launcher
function iconForClass(id) {
if (!id) return ""
const lower = id.toLowerCase()
// direct hits first
if (classToIcon[lower])
return iconForDesktopIcon(classToIcon[lower])
if (desktopIdToIcon[lower])
return iconForDesktopIcon(desktopIdToIcon[lower])
if (nameToIcon[lower])
return iconForDesktopIcon(nameToIcon[lower])
// fuzzy contains match against wmClass map
for (let key in classToIcon) {
if (lower.includes(key) || key.includes(lower))
return iconForDesktopIcon(classToIcon[key])
}
// fuzzy against desktop ids
for (let key in desktopIdToIcon) {
if (lower.includes(key) || key.includes(lower))
return iconForDesktopIcon(desktopIdToIcon[key])
}
// fuzzy against names
for (let key in nameToIcon) {
if (lower.includes(key) || key.includes(lower))
return iconForDesktopIcon(nameToIcon[key])
}
// final fallback to theme resolution
const resolved = FileUtils.resolveIcon(id)
return iconForDesktopIcon(resolved)
}
// Extra helper: resolve icon using any metadata we might have (Hyprland, Niri, etc.)
function iconForAppMeta(meta) {
if (!meta) return Quickshell.iconPath("application-x-executable")
const candidates = [
meta.appId,
meta.class,
meta.initialClass,
meta.desktopId,
meta.title,
meta.name
]
for (let c of candidates) {
const icon = iconForClass(c)
if (icon !== "")
return icon
}
// fallback: try compositor provided icon name
if (meta.icon)
return iconForDesktopIcon(meta.icon)
// hard fallback icons (guaranteed to exist in most themes)
const fallbacks = [
"application-x-executable",
"application-default-icon",
"window"
]
for (let f of fallbacks) {
const resolved = Quickshell.iconPath(f)
if (resolved)
return resolved
}
return ""
}
function registerApp(displayName, comment, icon, exec, wmClass, desktopId) {
const entry = {
name: displayName,
comment: comment,
icon: icon,
exec: exec,
wmClass: wmClass,
desktopId: desktopId
}
apps.push(entry)
if (wmClass)
classToIcon[wmClass.toLowerCase()] = icon
if (desktopId)
desktopIdToIcon[desktopId.toLowerCase()] = icon
if (displayName)
nameToIcon[displayName.toLowerCase()] = icon
// Hard aliases for apps with messy WM_CLASS values
if (displayName.toLowerCase().includes("visual studio code") ||
icon.toLowerCase().includes("code")) {
classToIcon["code"] = icon
classToIcon["code-oss"] = icon
classToIcon["code-url-handler"] = icon
desktopIdToIcon["code.desktop"] = icon
desktopIdToIcon["code-oss.desktop"] = icon
}
}
function buildRegistry() {
const entries = DesktopEntries.applications.values
for (let entry of entries) {
if (entry.noDisplay)
continue
registry.registerApp(
entry.name || "",
entry.comment || "",
entry.icon || "",
entry.execString || "",
entry.startupWMClass || "",
entry.id || ""
)
}
registry.ready()
}
Component.onCompleted: buildRegistry()
}

View File

@@ -0,0 +1,30 @@
pragma Singleton
import "../modules/functions/fuzzy/fuzzysort.js" as Fuzzy
import Quickshell
Singleton {
id: root
readonly property list<DesktopEntry> list: DesktopEntries.applications.values.filter(a => !a.noDisplay).sort((a, b) => a.name.localeCompare(b.name))
readonly property list<var> preppedApps: list.map(a => ({
name: Fuzzy.prepare(a.name),
comment: Fuzzy.prepare(a.comment),
entry: a
}))
function fuzzyQuery(search: string): var { // idk why list<DesktopEntry> doesn't work
return Fuzzy.go(search, preppedApps, {
all: true,
keys: ["name", "comment"],
scoreFn: r => r[0].score > 0 ? r[0].score * 0.9 + r[1].score * 0.1 : 0
}).map(r => r.obj.entry);
}
function launch(entry: DesktopEntry): void {
if (entry.execString.startsWith("sh -c"))
Quickshell.execDetached(["sh", "-c", `app2unit -- ${entry.execString}`]);
else
Quickshell.execDetached(["sh", "-c", `app2unit -- '${entry.id}.desktop' || app2unit -- ${entry.execString}`]);
}
}

View File

@@ -0,0 +1,24 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Bluetooth
Singleton {
id: root
readonly property BluetoothAdapter defaultAdapter: Bluetooth.defaultAdapter
readonly property list<BluetoothDevice> devices: defaultAdapter?.devices?.values ?? []
readonly property BluetoothDevice activeDevice: devices.find(d => d.connected) ?? null
readonly property string icon: {
if (!defaultAdapter?.enabled)
return "bluetooth_disabled"
if (activeDevice)
return "bluetooth_connected"
return defaultAdapter.discovering
? "bluetooth_searching"
: "bluetooth"
}
}

View File

@@ -0,0 +1,142 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import QtQuick
// from github.com/end-4/dots-hyprland
Singleton {
id: root
signal brightnessChanged()
property var ddcMonitors: []
readonly property list<BrightnessMonitor> monitors: Quickshell.screens.map(screen => monitorComp.createObject(root, { screen }))
function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.screen === screen)
}
function increaseBrightness(): void {
const focusedName = Hyprland.focusedMonitor.name
const monitor = monitors.find(m => focusedName === m.screen.name)
if (monitor)
monitor.setBrightness(monitor.brightness + 0.05)
}
function decreaseBrightness(): void {
const focusedName = Hyprland.focusedMonitor.name
const monitor = monitors.find(m => focusedName === m.screen.name)
if (monitor)
monitor.setBrightness(monitor.brightness - 0.05)
}
reloadableId: "brightness"
onMonitorsChanged: {
ddcMonitors = []
ddcProc.running = true
}
Process {
id: ddcProc
command: ["ddcutil", "detect", "--brief"]
stdout: SplitParser {
splitMarker: "\n\n"
onRead: data => {
if (data.startsWith("Display ")) {
const lines = data.split("\n").map(l => l.trim())
root.ddcMonitors.push({
model: lines.find(l => l.startsWith("Monitor:")).split(":")[2],
busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1]
})
}
}
}
onExited: root.ddcMonitorsChanged()
}
Process { id: setProc }
component BrightnessMonitor: QtObject {
id: monitor
required property ShellScreen screen
readonly property bool isDdc: {
const match = root.ddcMonitors.find(m => m.model === screen.model &&
!root.monitors.slice(0, root.monitors.indexOf(this))
.some(mon => mon.busNum === m.busNum))
return !!match
}
readonly property string busNum: {
const match = root.ddcMonitors.find(m => m.model === screen.model &&
!root.monitors.slice(0, root.monitors.indexOf(this))
.some(mon => mon.busNum === m.busNum))
return match?.busNum ?? ""
}
property int rawMaxBrightness: 100
property real brightness
property real brightnessMultiplier: 1.0
property real multipliedBrightness: Math.max(0, Math.min(1, brightness * brightnessMultiplier))
property bool ready: false
property bool animateChanges: !monitor.isDdc
onBrightnessChanged: {
if (!monitor.ready) return
root.brightnessChanged()
if (monitor.animateChanges)
syncBrightness()
else
setTimer.restart()
}
property var setTimer: Timer {
id: setTimer
interval: monitor.isDdc ? 300 : 0
onTriggered: syncBrightness()
}
function initialize() {
monitor.ready = false
initProc.command = isDdc
? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
: ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`]
initProc.running = true
}
readonly property Process initProc: Process {
stdout: SplitParser {
onRead: data => {
const [, , , current, max] = data.split(" ")
monitor.rawMaxBrightness = parseInt(max)
monitor.brightness = parseInt(current) / monitor.rawMaxBrightness
monitor.ready = true
}
}
}
function syncBrightness() {
const brightnessValue = Math.max(Math.min(monitor.multipliedBrightness, 1), 0)
const rawValueRounded = Math.max(Math.floor(brightnessValue * monitor.rawMaxBrightness), 1)
setProc.command = isDdc
? ["ddcutil", "-b", busNum, "setvcp", "10", rawValueRounded]
: ["brightnessctl", "set", rawValueRounded.toString()]
setProc.startDetached()
}
function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value))
monitor.brightness = value
}
Component.onCompleted: initialize()
onBusNumChanged: initialize()
}
Component { id: monitorComp; BrightnessMonitor {} }
}

View File

@@ -0,0 +1,89 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
Singleton {
id: root
// compositor stuff
property string detectedCompositor: ""
readonly property var backend: {
if (detectedCompositor === "niri")
return Niri
if (detectedCompositor === "hyprland")
return Hyprland
return null
}
function require(compositors) { // This function can be effectively used to detect check requirements for a feature (also supports multiple compositors)
if (Array.isArray(compositors)) {
return compositors.includes(detectedCompositor);
}
return compositors === detectedCompositor;
}
// Unified api
property string title: backend?.title ?? ""
property bool isFullscreen: backend?.isFullscreen ?? false
property string layout: backend?.layout ?? "Tiled"
property int focusedWorkspaceId: backend?.focusedWorkspaceId ?? 1
property var workspaces: backend?.workspaces ?? []
property var windowList: backend?.windowList ?? []
property bool initialized: backend?.initialized ?? true
property int workspaceCount: backend?.workspaceCount ?? 0
property real screenW: backend?.screenW ?? 0
property real screenH: backend?.screenH ?? 0
property real screenScale: backend?.screenScale ?? 1
readonly property Toplevel activeToplevel: ToplevelManager.activeToplevel
function changeWorkspace(id) {
backend?.changeWorkspace?.(id)
}
function changeWorkspaceRelative(delta) {
backend?.changeWorkspaceRelative?.(delta)
}
function isWorkspaceOccupied(id) {
return backend?.isWorkspaceOccupied?.(id) ?? false
}
function focusedWindowForWorkspace(id) {
return backend?.focusedWindowForWorkspace?.(id) ?? null
}
// process to detect compositor
Process {
command: ["sh", "-c", "echo \"$XDG_CURRENT_DESKTOP $XDG_SESSION_DESKTOP\""]
running: true
stdout: SplitParser {
onRead: data => {
if (!data)
return
const val = data.trim().toLowerCase()
if (val.includes("hyprland")) {
root.detectedCompositor = "hyprland"
} else if (val.includes("niri")) {
root.detectedCompositor = "niri"
}
}
}
}
signal stateChanged()
Connections {
target: backend
function onStateChanged() {
root.stateChanged()
}
}
}

View File

@@ -0,0 +1,34 @@
import QtQuick
import Quickshell
import qs.config
pragma Singleton
/*
This service primarily resolves configs for widgets that are customizable per monitor.
*/
Singleton {
function bar(displayName) {
const displays = Config.runtime.monitors;
const fallback = Config.runtime.bar;
if (!displays || !displays[displayName] || !displays[displayName].bar || displayName === "")
return fallback;
return displays[displayName].bar;
}
function getBarConfigurableHandle(displayName) { // returns prefField string
const displays = Config.runtime.monitors;
if (!displays || !displays[displayName] || !displays[displayName].bar || displayName === "")
return "bar";
return "monitors." + displayName + ".bar";
}
}

View File

@@ -0,0 +1,69 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import qs.config
QtObject {
// Power menu
property url powerMenu: Qt.resolvedUrl("../modules/interface/powermenu/Powermenu.qml")
property bool overriddenPowerMenu: false
function overridePowerMenu() {
overriddenPowerMenu = true
}
// Bar
property url bar: Qt.resolvedUrl("../modules/interface/bar/Bar.qml")
property bool overriddenBar: false
function overrideBar() {
overriddenBar = true
}
// App launcher
property url launcher: Qt.resolvedUrl("../modules/interface/launcher/Launcher.qml")
property bool overriddenLauncher: false
function overrideLauncher() {
overriddenLauncher = true
}
// Lock screen
property url lockScreen: Qt.resolvedUrl("../modules/interface/lockscreen/LockScreen.qml")
property bool overriddenLockScreen: false
function overrideLockScreen() {
overriddenLockScreen = true
}
// Desktop background / wallpaper handler
property url background: Qt.resolvedUrl("../modules/interface/background/Background.qml")
property bool overriddenBackground: false
function overrideBackground() {
overriddenBackground = true
}
// Notifications UI
property url notifications: Qt.resolvedUrl("../modules/interface/notifications/Notifications.qml")
property bool overriddenNotifications: false
function overrideNotifications() {
overriddenNotifications = true
}
// Global overlays (OSD, volume, brightness, etc.)
property url overlays: Qt.resolvedUrl("../modules/interface/overlays/Overlays.qml")
property bool overriddenOverlays: false
function overrideOverlays() {
overriddenOverlays = true
}
// Right sidebar
property url sidebarRight: !overriddenSidebarRight ? Qt.resolvedUrl("../modules/interface/sidebarRight/SidebarRight.qml") : "" // Force override
property bool overriddenSidebarRight: false
function overrideSidebarRight() {
overriddenSidebarRight = true
}
// Left sidebar
property url sidebarLeft: !overriddenSidebarLeft ? Qt.resolvedUrl("../modules/interface/sidebarLeft/SidebarLeft.qml") : "" // Force override
property bool overriddenSidebarLeft: false
function overrideSidebarLeft() {
overriddenSidebarLeft = true
}
}

View File

@@ -0,0 +1,216 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.Wayland
Singleton {
id: root
// true if Hyprland is running, false otherwise
readonly property bool isHyprland: Compositor.require("hyprland")
// reactive Hyprland data, only valid if Hyprland is running
signal stateChanged()
readonly property var toplevels: isHyprland ? Hyprland.toplevels : []
readonly property var workspaces: isHyprland ? Hyprland.workspaces : []
readonly property var monitors: isHyprland ? Hyprland.monitors : []
readonly property Toplevel activeToplevel: isHyprland ? ToplevelManager.activeToplevel : null
readonly property HyprlandWorkspace focusedWorkspace: isHyprland ? Hyprland.focusedWorkspace : null
readonly property HyprlandMonitor focusedMonitor: isHyprland ? Hyprland.focusedMonitor : null
readonly property int focusedWorkspaceId: focusedWorkspace?.id ?? 1
property real screenW: focusedMonitor ? focusedMonitor.width : 0
property real screenH: focusedMonitor ? focusedMonitor.height : 0
property real screenScale: focusedMonitor ? focusedMonitor.scale : 1
// parsed hyprctl data, defaults are empty
property var windowList: []
property var windowByAddress: ({})
property var addresses: []
property var layers: ({})
property var monitorsInfo: []
property var workspacesInfo: []
property var workspaceById: ({})
property var workspaceIds: []
property var activeWorkspaceInfo: null
property string keyboardLayout: "?"
// dispatch a command to Hyprland, no-op if not running
function dispatch(request: string): void {
if (!isHyprland) return
Hyprland.dispatch(request)
}
// switch workspace safely
function changeWorkspace(targetWorkspaceId) {
if (!isHyprland || !targetWorkspaceId) return
root.dispatch("workspace " + targetWorkspaceId)
}
// find most recently focused window in a workspace
function focusedWindowForWorkspace(workspaceId) {
if (!isHyprland) return null
const wsWindows = root.windowList.filter(w => w.workspace.id === workspaceId)
if (wsWindows.length === 0) return null
return wsWindows.reduce((best, win) => {
const bestFocus = best?.focusHistoryID ?? Infinity
const winFocus = win?.focusHistoryID ?? Infinity
return winFocus < bestFocus ? win : best
}, null)
}
// check if a workspace has any windows
function isWorkspaceOccupied(id: int): bool {
if (!isHyprland) return false
return Hyprland.workspaces.values.find(w => w?.id === id)?.lastIpcObject.windows > 0 || false
}
// update all hyprctl processes
function updateAll() {
if (!isHyprland) return
getClients.running = true
getLayers.running = true
getMonitors.running = true
getWorkspaces.running = true
getActiveWorkspace.running = true
}
// largest window in a workspace
function biggestWindowForWorkspace(workspaceId) {
if (!isHyprland) return null
const windowsInThisWorkspace = root.windowList.filter(w => w.workspace.id === workspaceId)
return windowsInThisWorkspace.reduce((maxWin, win) => {
const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0)
const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0)
return winArea > maxArea ? win : maxWin
}, null)
}
// refresh keyboard layout
function refreshKeyboardLayout() {
if (!isHyprland) return
hyprctlDevices.running = true
}
// only create hyprctl processes if Hyprland is running
Component.onCompleted: {
if (isHyprland) {
updateAll()
refreshKeyboardLayout()
}
}
// process to get keyboard layout
Process {
id: hyprctlDevices
running: false
command: ["hyprctl", "devices", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
const devices = JSON.parse(this.text)
const keyboard = devices.keyboards.find(k => k.main) || devices.keyboards[0]
root.keyboardLayout = keyboard?.active_keymap?.toUpperCase()?.slice(0, 2) ?? "?"
} catch (err) {
console.error("Failed to parse keyboard layout:", err)
root.keyboardLayout = "?"
}
}
}
}
Process {
id: getClients
running: false
command: ["hyprctl", "clients", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.windowList = JSON.parse(this.text)
let tempWinByAddress = {}
for (let win of root.windowList) tempWinByAddress[win.address] = win
root.windowByAddress = tempWinByAddress
root.addresses = root.windowList.map(w => w.address)
} catch (e) {
console.error("Failed to parse clients:", e)
}
}
}
}
Process {
id: getMonitors
running: false
command: ["hyprctl", "monitors", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try { root.monitorsInfo = JSON.parse(this.text) }
catch (e) { console.error("Failed to parse monitors:", e) }
}
}
}
Process {
id: getLayers
running: false
command: ["hyprctl", "layers", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try { root.layers = JSON.parse(this.text) }
catch (e) { console.error("Failed to parse layers:", e) }
}
}
}
Process {
id: getWorkspaces
running: false
command: ["hyprctl", "workspaces", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.workspacesInfo = JSON.parse(this.text)
let map = {}
for (let ws of root.workspacesInfo) map[ws.id] = ws
root.workspaceById = map
root.workspaceIds = root.workspacesInfo.map(ws => ws.id)
} catch (e) { console.error("Failed to parse workspaces:", e) }
}
}
}
Process {
id: getActiveWorkspace
running: false
command: ["hyprctl", "activeworkspace", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try { root.activeWorkspaceInfo = JSON.parse(this.text) }
catch (e) { console.error("Failed to parse active workspace:", e) }
}
}
}
// only connect to Hyprland events if running
Connections {
target: isHyprland ? Hyprland : null
function onRawEvent(event) {
if (!isHyprland || event.name.endsWith("v2")) return
if (event.name.includes("activelayout"))
refreshKeyboardLayout()
else if (event.name.includes("mon"))
Hyprland.refreshMonitors()
else if (event.name.includes("workspace") || event.name.includes("window"))
Hyprland.refreshWorkspaces()
else
Hyprland.refreshToplevels()
updateAll()
root.stateChanged()
}
}
}

View File

@@ -0,0 +1,136 @@
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
pragma Singleton
Singleton {
id: root
property alias activePlayer: instance.activePlayer
property bool isPlaying: activePlayer ? activePlayer.playbackState === MprisPlaybackState.Playing : false
property string title: activePlayer ? activePlayer.trackTitle : "No Media"
property string artist: activePlayer ? activePlayer.trackArtist : ""
property string album: activePlayer ? activePlayer.trackAlbum : ""
property string artUrl: activePlayer ? activePlayer.trackArtUrl : ""
property double position: 0
property double length: activePlayer ? activePlayer.length : 0
property var _players: Mpris.players.values
property int playerCount: _players.length
property var playerList: {
let list = [];
for (let p of _players) {
list.push({
"identity": p.identity || p.desktopEntry || "Unknown",
"desktopEntry": p.desktopEntry || "",
"player": p
});
}
return list;
}
property string currentPlayerName: activePlayer ? (activePlayer.identity || activePlayer.desktopEntry || "Unknown") : ""
property bool manualSelection: false
function setPosition(pos) {
if (activePlayer)
activePlayer.position = pos;
}
function selectPlayer(player) {
if (player) {
instance.activePlayer = player;
manualSelection = true;
}
}
function selectNextPlayer() {
const players = Mpris.players.values;
if (players.length <= 1)
return ;
const currentIndex = players.indexOf(activePlayer);
const nextIndex = (currentIndex + 1) % players.length;
selectPlayer(players[nextIndex]);
}
function selectPreviousPlayer() {
const players = Mpris.players.values;
if (players.length <= 1)
return ;
const currentIndex = players.indexOf(activePlayer);
const prevIndex = (currentIndex - 1 + players.length) % players.length;
selectPlayer(players[prevIndex]);
}
function updateActivePlayer() {
const players = Mpris.players.values;
if (manualSelection && instance.activePlayer && players.includes(instance.activePlayer))
return ;
if (manualSelection && instance.activePlayer && !players.includes(instance.activePlayer))
manualSelection = false;
const playing = players.find((p) => {
return p.playbackState === MprisPlaybackState.Playing;
});
if (playing) {
instance.activePlayer = playing;
} else if (players.length > 0) {
if (!instance.activePlayer || !players.includes(instance.activePlayer))
instance.activePlayer = players[0];
} else {
instance.activePlayer = null;
}
}
function playPause() {
if (activePlayer && activePlayer.canTogglePlaying)
activePlayer.togglePlaying();
}
function next() {
if (activePlayer && activePlayer.canGoNext)
activePlayer.next();
}
function previous() {
if (activePlayer && activePlayer.canGoPrevious)
activePlayer.previous();
}
Component.onCompleted: updateActivePlayer()
QtObject {
id: instance
property var players: Mpris.players.values
property var activePlayer: null
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
updateActivePlayer();
if (activePlayer)
root.position = activePlayer.position;
}
}
Connections {
function onValuesChanged() {
root._players = Mpris.players.values;
updateActivePlayer();
}
target: Mpris.players
}
}

View File

@@ -0,0 +1,309 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property list<Connection> connections: []
readonly property list<string> savedNetworks: []
readonly property Connection active: connections.find(c => c.active) ?? null
property bool wifiEnabled: true
readonly property bool scanning: rescanProc.running
property string lastNetworkAttempt: ""
property string lastErrorMessage: ""
property string message: ""
readonly property string icon: {
if (!active) return "signal_wifi_off";
if (active.type === "ethernet") return "settings_ethernet";
if (active.strength >= 75) return "network_wifi";
else if (active.strength >= 50) return "network_wifi_3_bar";
else if (active.strength >= 25) return "network_wifi_2_bar";
else return "network_wifi_1_bar";
}
readonly property string wifiLabel: {
const activeWifi = connections.find(c => c.active && c.type === "wifi");
if (activeWifi) return activeWifi.name;
return "Wi-Fi";
}
readonly property string wifiStatus: {
const activeWifi = connections.find(c => c.active && c.type === "wifi");
if (activeWifi) return "Connected";
if (wifiEnabled) return "On";
return "Off";
}
readonly property string label: {
if (active) return active.name;
if (wifiEnabled) return "Wi-Fi";
return "Wi-Fi";
}
readonly property string status: {
if (active) return "Connected";
if (wifiEnabled) return "On";
return "Off";
}
function enableWifi(enabled: bool): void {
enableWifiProc.exec(["nmcli", "radio", "wifi", enabled ? "on" : "off"]);
}
function toggleWifi(): void {
enableWifi(!wifiEnabled);
}
function rescan(): void {
rescanProc.running = true;
}
function connect(connection: Connection, password: string): void {
if (connection.type === "wifi") {
root.lastNetworkAttempt = connection.name;
root.lastErrorMessage = "";
root.message = "";
if (password && password.length > 0) {
connectProc.exec(["nmcli", "dev", "wifi", "connect", connection.name, "password", password]);
} else {
connectProc.exec(["nmcli", "dev", "wifi", "connect", connection.name]);
}
} else if (connection.type === "ethernet") {
ethConnectProc.exec(["nmcli", "connection", "up", connection.uuid]);
}
}
function disconnect(): void {
if (!active) return;
if (active.type === "wifi") {
disconnectProc.exec(["nmcli", "connection", "down", active.name]);
} else if (active.type === "ethernet") {
ethDisconnectProc.exec(["nmcli", "connection", "down", active.uuid]);
}
}
Process {
running: true
command: ["nmcli", "m"]
stdout: SplitParser {
onRead: {
getWifiStatus();
updateConnections();
}
}
}
function getWifiStatus(): void {
wifiStatusProc.running = true;
}
function updateConnections(): void {
getWifiNetworks.running = true;
getEthConnections.running = true;
getSavedNetworks.running = true;
}
Process {
id: wifiStatusProc
running: true
command: ["nmcli", "radio", "wifi"]
environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" })
stdout: StdioCollector {
onStreamFinished: {
root.wifiEnabled = text.trim() === "enabled";
if (!root.wifiEnabled) {
root.lastErrorMessage = "";
root.message = "";
root.lastNetworkAttempt = "";
}
}
}
}
Process {
id: enableWifiProc
onExited: updateConnections()
}
Process {
id: rescanProc
command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
onExited: updateConnections()
}
Process {
id: connectProc
stdout: StdioCollector { }
stderr: StdioCollector {
onStreamFinished: {
if (text.includes("Error") || text.includes("incorrect")) {
root.lastErrorMessage = "Incorrect password";
}
}
}
onExited: {
if (exitCode === 0) {
root.message = "ok";
root.lastErrorMessage = "";
} else {
root.message = root.lastErrorMessage !== "" ? root.lastErrorMessage : "Connection failed";
}
updateConnections();
}
}
Process {
id: disconnectProc
onExited: updateConnections()
}
Process {
id: ethConnectProc
onExited: updateConnections()
}
Process {
id: ethDisconnectProc
onExited: updateConnections()
}
Process {
id: getSavedNetworks
running: true
command: ["nmcli", "-g", "NAME,TYPE", "connection", "show"]
environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" })
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
const wifiConnections = lines
.map(line => line.split(":"))
.filter(parts => parts[1] === "802-11-wireless")
.map(parts => parts[0]);
root.savedNetworks = wifiConnections;
}
}
}
Process {
id: getWifiNetworks
running: true
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" })
stdout: StdioCollector {
onStreamFinished: {
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
const rep = new RegExp("\\\\:", "g");
const rep2 = new RegExp(PLACEHOLDER, "g");
const allNetworks = text.trim().split("\n").map(n => {
const net = n.replace(rep, PLACEHOLDER).split(":");
return {
type: "wifi",
active: net[0] === "yes",
strength: parseInt(net[1]),
frequency: parseInt(net[2]),
name: net[3]?.replace(rep2, ":") ?? "",
bssid: net[4]?.replace(rep2, ":") ?? "",
security: net[5] ?? "",
saved: root.savedNetworks.includes(net[3] ?? ""),
uuid: "",
device: ""
};
}).filter(n => n.name && n.name.length > 0);
const networkMap = new Map();
for (const network of allNetworks) {
const existing = networkMap.get(network.name);
if (!existing) {
networkMap.set(network.name, network);
} else {
if (network.active && !existing.active) {
networkMap.set(network.name, network);
} else if (!network.active && !existing.active && network.strength > existing.strength) {
networkMap.set(network.name, network);
}
}
}
mergeConnections(Array.from(networkMap.values()), "wifi");
}
}
}
Process {
id: getEthConnections
running: true
command: ["nmcli", "-g", "NAME,UUID,TYPE,DEVICE,STATE", "connection", "show"]
environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" })
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
const ethConns = lines
.map(line => line.split(":"))
.filter(parts => parts[2] === "802-3-ethernet" || parts[2] === "gsm" || parts[2] === "bluetooth")
.map(parts => ({
type: "ethernet",
name: parts[0],
uuid: parts[1],
device: parts[3],
active: parts[4] === "activated",
strength: 100,
frequency: 0,
bssid: "",
security: "",
saved: true
}));
mergeConnections(ethConns, "ethernet");
}
}
}
function mergeConnections(newConns: var, connType: string): void {
const rConns = root.connections;
const destroyed = rConns.filter(rc => rc.type === connType && !newConns.find(nc =>
connType === "wifi" ? (nc.frequency === rc.frequency && nc.name === rc.name && nc.bssid === rc.bssid)
: nc.uuid === rc.uuid
));
for (const conn of destroyed)
rConns.splice(rConns.indexOf(conn), 1).forEach(c => c.destroy());
for (const conn of newConns) {
const match = rConns.find(c =>
connType === "wifi" ? (c.frequency === conn.frequency && c.name === conn.name && c.bssid === conn.bssid)
: c.uuid === conn.uuid
);
if (match) {
match.lastIpcObject = conn;
} else {
rConns.push(connComp.createObject(root, { lastIpcObject: conn }));
}
}
}
component Connection: QtObject {
required property var lastIpcObject
readonly property string type: lastIpcObject.type
readonly property string name: lastIpcObject.name
readonly property string uuid: lastIpcObject.uuid
readonly property string device: lastIpcObject.device
readonly property bool active: lastIpcObject.active
readonly property int strength: lastIpcObject.strength
readonly property int frequency: lastIpcObject.frequency
readonly property string bssid: lastIpcObject.bssid
readonly property string security: lastIpcObject.security
readonly property bool isSecure: security.length > 0
readonly property bool saved: lastIpcObject.saved
}
Component { id: connComp; Connection { } }
}

View File

@@ -0,0 +1,242 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
pragma Singleton
Singleton {
id: niriItem
signal stateChanged()
property string title: ""
property bool isFullscreen: false
property string layout: "Tiled"
property int focusedWorkspaceId: 1
property var workspaces: []
property var workspaceCache: ({})
property var windows: [] // tracked windows
property bool initialized: false
property int screenW: 0
property int screenH: 0
property real screenScale: 1
function changeWorkspace(id) {
sendSocketCommand(niriCommandSocket, {
"Action": {
"focus_workspace": {
"reference": { "Id": id }
}
}
})
dispatchProc.command = ["niri", "msg", "action", "focus-workspace", id.toString()]
dispatchProc.running = true
}
function changeWorkspaceRelative(delta) {
const cmd = delta > 0 ? "focus-workspace-down" : "focus-workspace-up"
dispatchProc.command = ["niri", "msg", "action", cmd]
dispatchProc.running = true
}
function isWorkspaceOccupied(id) {
for (const ws of workspaces)
if (ws.id === id) return true
return false
}
function focusedWindowForWorkspace(id) {
// focused window in workspace
for (const win of windows) {
if (win.workspaceId === id && win.isFocused) {
return { class: win.appId, title: win.title }
}
}
// fallback: any window in workspace
for (const win of windows) {
if (win.workspaceId === id) {
return { class: win.appId, title: win.title }
}
}
return null
}
function sendSocketCommand(sock, command) {
if (sock.connected)
sock.write(JSON.stringify(command) + "\n")
}
function startEventStream() {
sendSocketCommand(niriEventStream, "EventStream")
}
function updateWorkspaces() {
sendSocketCommand(niriCommandSocket, "Workspaces")
}
function updateWindows() {
sendSocketCommand(niriCommandSocket, "Windows")
}
function updateFocusedWindow() {
sendSocketCommand(niriCommandSocket, "FocusedWindow")
}
function recollectWorkspaces(workspacesData) {
const list = []
workspaceCache = {}
for (const ws of workspacesData) {
const data = {
id: ws.idx !== undefined ? ws.idx + 1 : ws.id,
internalId: ws.id,
idx: ws.idx,
name: ws.name || "",
output: ws.output || "",
isFocused: ws.is_focused === true,
isActive: ws.is_active === true
}
list.push(data)
workspaceCache[ws.id] = data
if (data.isFocused)
focusedWorkspaceId = data.id
}
list.sort((a, b) => a.id - b.id)
workspaces = list
stateChanged()
}
function recollectWindows(windowsData) {
const list = []
for (const win of windowsData) {
list.push({
appId: win.app_id || "",
title: win.title || "",
workspaceId: win.workspace_id,
isFocused: win.is_focused === true
})
}
windows = list
stateChanged()
}
function recollectFocusedWindow(win) {
if (win && win.title) {
title = win.title
isFullscreen = win.is_fullscreen || false
layout = "Tiled"
} else {
title = "~"
isFullscreen = false
layout = "Tiled"
}
stateChanged()
}
Component.onCompleted: {
if (Quickshell.env("NIRI_SOCKET")) {
niriCommandSocket.connected = true
niriEventStream.connected = true
initialized = true
}
}
Process {
id: niriOutputsProc
command: ["niri", "msg", "outputs"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const lines = this.text.split("\n")
let sizeRe = /Logical size:\s*(\d+)x(\d+)/
let scaleRe = /Scale:\s*([\d.]+)/
for (const line of lines) {
let m
if ((m = sizeRe.exec(line))) {
screenW = parseInt(m[1], 10)
screenH = parseInt(m[2], 10)
} else if ((m = scaleRe.exec(line))) {
screenScale = parseFloat(m[1])
}
}
stateChanged()
}
}
}
Socket {
id: niriCommandSocket
path: Quickshell.env("NIRI_SOCKET") || ""
connected: false
onConnectedChanged: {
if (connected) {
updateWorkspaces()
updateWindows()
updateFocusedWindow()
}
}
parser: SplitParser {
onRead: (line) => {
if (!line.trim()) return
try {
const data = JSON.parse(line)
if (data?.Ok) {
const res = data.Ok
if (res.Workspaces) recollectWorkspaces(res.Workspaces)
else if (res.Windows) recollectWindows(res.Windows)
else if (res.FocusedWindow) recollectFocusedWindow(res.FocusedWindow)
}
} catch (e) {
console.warn("Niri socket parse error:", e)
}
}
}
}
Socket {
id: niriEventStream
path: Quickshell.env("NIRI_SOCKET") || ""
connected: false
onConnectedChanged: {
if (connected)
startEventStream()
}
parser: SplitParser {
onRead: (data) => {
if (!data.trim()) return
try {
const event = JSON.parse(data.trim())
if (event.WorkspacesChanged)
recollectWorkspaces(event.WorkspacesChanged.workspaces)
else if (event.WorkspaceActivated)
updateWorkspaces()
else if (
event.WindowFocusChanged ||
event.WindowOpenedOrChanged ||
event.WindowClosed
)
updateWindows()
} catch (e) {
console.warn("Niri event stream parse error:", e)
}
}
}
}
Process { id: dispatchProc }
}

View File

@@ -0,0 +1,110 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import QtQuick
import qs.config
// from github.com/end-4/dots-hyprland with modifications
Singleton {
id: root
property list<Notif> data: []
property list<Notif> popups: data.filter(n => n.popup && !n.tracked)
property list<Notif> history: data
Loader {
active: Config.initialized && Config.runtime.notifications.enabled
sourceComponent: NotificationServer {
keepOnReload: false
actionsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
onNotification: notif => {
notif.tracked = true;
root.data.push(notifComp.createObject(root, {
popup: true,
notification: notif,
shown: false
}));
}
}
}
function removeById(id) {
const i = data.findIndex(n => n.notification.id === id);
if (i >= 0) {
data.splice(i, 1);
}
}
component Notif: QtObject {
id: notif
property bool popup
readonly property date time: new Date()
readonly property string timeStr: {
const diff = Time.date.getTime() - time.getTime();
const m = Math.floor(diff / 60000);
const h = Math.floor(m / 60);
if (h < 1 && m < 1)
return "now";
if (h < 1)
return `${m}m`;
return `${h}h`;
}
property bool shown: false
required property Notification notification
readonly property string summary: notification.summary
readonly property string body: notification.body
readonly property string appIcon: notification.appIcon
readonly property string appName: notification.appName
readonly property string image: notification.image
readonly property int urgency: notification.urgency
readonly property list<NotificationAction> actions: notification.actions
readonly property Timer timer: Timer {
running: notif.actions.length >= 0
interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : 5000
onTriggered: {
if (true)
notif.popup = false;
}
}
readonly property Connections conn: Connections {
target: notif.notification.Retainable
function onDropped(): void {
root.data.splice(root.data.indexOf(notif), 1);
}
function onAboutToDestroy(): void {
notif.destroy();
}
}
readonly property Connections conn2: Connections {
target: notif.notification
function onClosed(reason) {
root.data.splice(root.data.indexOf(notif), 1)
}
}
}
Component {
id: notifComp
Notif {}
}
}

View File

@@ -0,0 +1,22 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import Quickshell.Services.Polkit
import QtQuick
Singleton {
id: root
property alias isActive: polkit.isActive
property alias isRegistered: polkit.isRegistered
property alias flow: polkit.flow
property alias path: polkit.path
Component.onCompleted: {
}
PolkitAgent {
id: polkit
}
}

View File

@@ -0,0 +1,394 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
pragma Singleton
Singleton {
id: root
property string hostname: ""
property string username: ""
property string osIcon: ""
property string osName: ""
property string kernelVersion: ""
property string architecture: ""
property string uptime: ""
property string qsVersion: ""
property string swapUsage: "—"
property real swapPercent: 0
property string ipAddress: "—"
property int runningProcesses: 0
property int loggedInUsers: 0
property string ramUsage: "—"
property real ramPercent: 0
property string cpuLoad: "—"
property real cpuPercent: 0
property string diskUsage: "—"
property real diskPercent: 0
property string cpuTemp: "—"
property string keyboardLayout: "none"
property real cpuTempPercent: 0
property int prevIdle: -1
property int prevTotal: -1
readonly property var osIcons: ({
"almalinux": "",
"alpine": "",
"arch": "󰣇",
"archcraft": "",
"arcolinux": "",
"artix": "",
"centos": "",
"debian": "",
"devuan": "",
"elementary": "",
"endeavouros": "",
"fedora": "",
"freebsd": "",
"garuda": "",
"gentoo": "",
"hyperbola": "",
"kali": "",
"linuxmint": "󰣭",
"mageia": "",
"openmandriva": "",
"manjaro": "",
"neon": "",
"nixos": "",
"opensuse": "",
"suse": "",
"sles": "",
"sles_sap": "",
"opensuse-tumbleweed": "",
"parrot": "",
"pop": "",
"raspbian": "",
"rhel": "",
"rocky": "",
"slackware": "",
"solus": "",
"steamos": "",
"tails": "",
"trisquel": "",
"ubuntu": "",
"vanilla": "",
"void": "",
"zorin": "",
"opensuse": "",
})
FileView { id: cpuStat; path: "/proc/stat" }
FileView { id: memInfo; path: "/proc/meminfo" }
FileView { id: uptimeFile; path: "/proc/uptime" }
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
cpuStat.reload()
memInfo.reload()
uptimeFile.reload()
/* CPU */
const cpuLine = cpuStat.text().split("\n")[0].trim().split(/\s+/)
const cpuUser = parseInt(cpuLine[1])
const cpuNice = parseInt(cpuLine[2])
const cpuSystem = parseInt(cpuLine[3])
const cpuIdle = parseInt(cpuLine[4])
const cpuIowait = parseInt(cpuLine[5])
const cpuIrq = parseInt(cpuLine[6])
const cpuSoftirq = parseInt(cpuLine[7])
const cpuIdleAll = cpuIdle + cpuIowait
const cpuTotal =
cpuUser + cpuNice + cpuSystem + cpuIrq + cpuSoftirq + cpuIdleAll
if (root.prevTotal >= 0) {
const totalDiff = cpuTotal - root.prevTotal
const idleDiff = cpuIdleAll - root.prevIdle
if (totalDiff > 0)
root.cpuPercent = (totalDiff - idleDiff) / totalDiff
}
root.prevTotal = cpuTotal
root.prevIdle = cpuIdleAll
root.cpuLoad = Math.round(root.cpuPercent * 100) + "%"
/* RAM */
const memLines = memInfo.text().split("\n")
let memTotal = 0
let memAvailable = 0
for (let line of memLines) {
if (line.startsWith("MemTotal"))
memTotal = parseInt(line.match(/\d+/)[0])
if (line.startsWith("MemAvailable"))
memAvailable = parseInt(line.match(/\d+/)[0])
}
if (memTotal > 0) {
const memUsed = memTotal - memAvailable
root.ramPercent = memUsed / memTotal
root.ramUsage =
`${Math.round(memUsed/1024)}/${Math.round(memTotal/1024)} MB`
}
/* Uptime */
const uptimeSeconds =
parseFloat(uptimeFile.text().split(" ")[0])
const d = Math.floor(uptimeSeconds / 86400)
const h = Math.floor((uptimeSeconds % 86400) / 3600)
const m = Math.floor((uptimeSeconds % 3600) / 60)
let upString = "Up "
if (d > 0) upString += d + "d "
if (h > 0) upString += h + "h "
upString += m + "m"
root.uptime = upString
cpuTempProc.running = true
diskProc.running = true
ipProc.running = true
procCountProc.running = true
swapProc.running = true
keyboardLayoutProc.running = true
loggedUsersProc.running = true
}
}
/* CPU Temperature */
Process {
id: cpuTempProc
command: [
"sh","-c",
"for f in /sys/class/hwmon/hwmon*/temp*_input; do cat $f && exit; done"
]
stdout: StdioCollector {
onStreamFinished: {
const raw = parseInt(text.trim())
if (isNaN(raw)) return
const c = raw / 1000
root.cpuTemp = `${Math.round(c)}°C`
const min = 30
const max = 95
root.cpuTempPercent =
Math.max(0, Math.min(1,(c-min)/(max-min)))
}
}
}
/* Disk */
Process {
id: diskProc
command: ["df","-h","/"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n")
if (lines.length < 2) return
const parts = lines[1].split(/\s+/)
const used = parts[2]
const total = parts[1]
const percent = parseInt(parts[4]) / 100
root.diskPercent = percent
root.diskUsage = `${used}/${total}`
}
}
}
/* Swap */
Process {
id: swapProc
command: ["sh","-c","free -m | grep Swap"]
stdout: StdioCollector {
onStreamFinished: {
const parts = text.trim().split(/\s+/)
if (parts.length < 3) return
const total = parseInt(parts[1])
const used = parseInt(parts[2])
root.swapPercent = used / total
root.swapUsage = `${used}/${total} MB`
}
}
}
/* IP */
Process {
id: ipProc
command: ["sh","-c","hostname -I | awk '{print $1}'"]
stdout: StdioCollector {
onStreamFinished: root.ipAddress = text.trim()
}
}
/* Process Count */
Process {
id: procCountProc
command: ["sh","-c","ps -e --no-headers | wc -l"]
stdout: StdioCollector {
onStreamFinished: root.runningProcesses = parseInt(text.trim())
}
}
/* Logged Users */
Process {
id: loggedUsersProc
command: ["who","-q"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n")
if (lines.length > 0)
root.loggedInUsers =
parseInt(lines[lines.length-1].replace("# users=",""))
}
}
}
/* Keyboard Layout */
Process {
id: keyboardLayoutProc
command: [
"sh","-c",
"hyprctl devices -j | jq -r '.keyboards[] | .layout' | head -n1"
]
stdout: StdioCollector {
onStreamFinished: {
const layout = text.trim()
if (layout)
root.keyboardLayout = layout
}
}
}
/* OS Info */
Process {
running: true
command: [
"sh","-c",
"source /etc/os-release && echo \"$PRETTY_NAME|$ID\""
]
stdout: StdioCollector {
onStreamFinished: {
const parts = text.trim().split("|")
root.osName = parts[0]
root.osIcon = root.osIcons[parts[1]] || ""
}
}
}
/* Quickshell Version */
Process {
running: true
command: ["qs","--version"]
stdout: StdioCollector {
onStreamFinished: {
root.qsVersion =
text.trim().split(',')[0]
.replace("quickshell ","")
}
}
}
/* Static system info */
Process {
running: true
command: ["whoami"]
stdout: StdioCollector {
onStreamFinished: root.username = text.trim()
}
}
Process {
running: true
command: ["hostname"]
stdout: StdioCollector {
onStreamFinished: root.hostname = text.trim()
}
}
Process {
running: true
command: ["uname","-r"]
stdout: StdioCollector {
onStreamFinished: root.kernelVersion = text.trim()
}
}
Process {
running: true
command: ["uname","-m"]
stdout: StdioCollector {
onStreamFinished: root.architecture = text.trim()
}
}
}

View File

@@ -0,0 +1,54 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
pragma Singleton
Singleton {
id: root
property var map: ({
})
function notifyMissingVariant(theme, variant) {
Quickshell.execDetached(["notify-send", "Nucleus Shell", `Theme '${theme}' does not have a ${variant} variant.`, "--urgency=normal", "--expire-time=5000"]);
}
Timer {
interval: 5000
repeat: true
running: true
onTriggered: loadThemes.running = true
}
Process {
id: loadThemes
command: ["ls", Directories.shellConfig + "/colorschemes"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const map = {
};
text.split("\n").map((t) => {
return t.trim();
}).filter((t) => {
return t.endsWith(".json");
}).forEach((t) => {
const name = t.replace(/\.json$/, "");
const parts = name.split("-");
const variant = parts.pop();
const base = parts.join("-");
if (!map[base])
map[base] = {
};
map[base][variant] = name;
});
root.map = map;
}
}
}
}

View File

@@ -0,0 +1,20 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property alias date: clock.date // expose raw date/time
readonly property SystemClock clock: clock
SystemClock {
id: clock
precision: SystemClock.Seconds
}
// Helper function if you still want formatting ability:
function format(fmt) {
return Qt.formatDateTime(clock.date, fmt)
}
}

View File

@@ -0,0 +1,136 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
Item {
// I have to make such services because quickshell services like Quickshell.Services.UPower don't work and are messy.
id: root
// Battery
property int percentage: 0
property string state: "unknown"
property string iconName: ""
property bool onBattery: false
property bool charging: false
property bool batteryPresent: false
property bool rechargeable: false
// Energy metrics
property real energyWh: 0
property real energyFullWh: 0
property real energyRateW: 0
property real capacityPercent: 0
// AC / system
property bool acOnline: false
property bool lidClosed: false
property string battIcon: {
const b = percentage;
if (b > 80)
return "battery_6_bar";
if (b > 60)
return "battery_5_bar";
if (b > 50)
return "battery_4_bar";
if (b > 40)
return "battery_3_bar";
if (b > 30)
return "battery_2_bar";
if (b > 20)
return "battery_1_bar";
if (b > 10)
return "battery_alert";
return "battery_0_bar";
}
Timer {
interval: 2000
running: true
repeat: true
onTriggered: upowerProc.running = true
}
Process {
id: upowerProc
command: ["upower", "-d"]
running: true
stdout: StdioCollector {
onStreamFinished: {
// ---------- DisplayDevice (preferred) ----------
// ---------- Rechargeable ----------
// ---------- Physical battery (extra info) ----------
// ---------- Daemon / system ----------
// ---------- AC adapter ----------
const t = text;
let m;
m = t.match(/DisplayDevice[\s\S]*?present:\s+(yes|no)/);
if (m) {
root.batteryPresent = (m[1] === "yes");
} else {
// fallback: physical battery
m = t.match(/battery_BAT\d+[\s\S]*?present:\s+(yes|no)/);
if (m)
root.batteryPresent = (m[1] === "yes");
}
m = t.match(/DisplayDevice[\s\S]*?rechargeable:\s+(yes|no)/);
if (m)
root.rechargeable = (m[1] === "yes");
m = t.match(/DisplayDevice[\s\S]*?percentage:\s+(\d+)%/);
if (m)
root.percentage = parseInt(m[1]);
m = t.match(/DisplayDevice[\s\S]*?state:\s+([a-z\-]+)/);
if (m) {
root.state = m[1];
root.charging = (m[1].includes("charge"));
}
m = t.match(/DisplayDevice[\s\S]*?icon-name:\s+'([^']+)'/);
if (m)
root.iconName = m[1];
m = t.match(/DisplayDevice[\s\S]*?energy:\s+([\d.]+)\s+Wh/);
if (m)
root.energyWh = parseFloat(m[1]);
m = t.match(/DisplayDevice[\s\S]*?energy-full:\s+([\d.]+)\s+Wh/);
if (m)
root.energyFullWh = parseFloat(m[1]);
m = t.match(/DisplayDevice[\s\S]*?energy-rate:\s+([\d.]+)\s+W/);
if (m)
root.energyRateW = parseFloat(m[1]);
m = t.match(/capacity:\s+([\d.]+)%/);
if (m)
root.capacityPercent = parseFloat(m[1]);
m = t.match(/on-battery:\s+(yes|no)/);
if (m)
root.onBattery = (m[1] === "yes");
m = t.match(/lid-is-closed:\s+(yes|no)/);
if (m)
root.lidClosed = (m[1] === "yes");
m = t.match(/line-power[\s\S]*?online:\s+(yes|no)/);
if (m)
root.acOnline = (m[1] === "yes");
}
}
}
}

View File

@@ -0,0 +1,100 @@
import Qt.labs.platform
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
Item {
id: updater
// Add 'v' arg to default local version because it is not stored
// as vX.Y.Z but X.Y.Z while on github its published as vX.Y.Z
property string currentVersion: ""
property string latestVersion: ""
property bool notified: false
property string channel: Config.runtime.shell.releaseChannel || "stable"
function readLocalVersion() {
currentVersion = "v" + (Config.runtime.shell.version || "");
}
function fetchLatestVersion() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
try {
const json = JSON.parse(xhr.responseText);
if (channel === "stable") {
// /releases/latest returns a single object
if (json.tag_name) {
latestVersion = json.tag_name;
compareVersions();
} else {
console.warn("Stable update check returned unexpected response:", json);
}
} else if (channel === "edge") {
// /releases returns an array, newest first
for (var i = 0; i < json.length; i++) {
if (json[i].prerelease === true) {
latestVersion = json[i].tag_name;
compareVersions();
return;
}
}
console.warn("Edge channel: no pre-release found.");
}
} catch (e) {
console.warn("Update check JSON parse failed:", xhr.responseText);
}
}
};
if (channel === "stable") {
xhr.open(
"GET",
"https://api.github.com/repos/xzepyx/nucleus-shell/releases/latest"
);
} else {
xhr.open(
"GET",
"https://api.github.com/repos/xzepyx/nucleus-shell/releases"
);
}
xhr.send();
}
function compareVersions() {
if (!currentVersion || !latestVersion)
return;
if (currentVersion !== latestVersion && !notified) {
notifyUpdate();
notified = true;
}
}
function notifyUpdate() {
Quickshell.execDetached([
"notify-send",
"-a", "Nucleus Shell",
"Update Available",
"Installed: " + currentVersion +
"\nLatest (" + channel + "): " + latestVersion
]);
}
visible: false
Timer {
interval: 24 * 60 * 60 * 1000 // 24 hours
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
readLocalVersion();
fetchLatestVersion();
}
}
}

View File

@@ -0,0 +1,50 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
Singleton {
PwObjectTracker {
objects: [
Pipewire.defaultAudioSource,
Pipewire.defaultAudioSink,
Pipewire.nodes,
Pipewire.links
]
}
property var sinks: Pipewire.nodes.values.filter(node => node.isSink && !node.isStream && node.audio)
property PwNode defaultSink: Pipewire.defaultAudioSink
property var sources: Pipewire.nodes.values.filter(node => !node.isSink && !node.isStream && node.audio)
property PwNode defaultSource: Pipewire.defaultAudioSource
property real volume: defaultSink?.audio?.volume ?? 0
property bool muted: defaultSink?.audio?.muted ?? false
function setVolume(to: real): void {
if (defaultSink?.ready && defaultSink?.audio) {
defaultSink.audio.muted = false;
defaultSink.audio.volume = Math.max(0, Math.min(1, to));
}
}
function setSourceVolume(to: real): void {
if (defaultSource?.ready && defaultSource?.audio) {
defaultSource.audio.muted = false;
defaultSource.audio.volume = Math.max(0, Math.min(1, to));
}
}
function setDefaultSink(sink: PwNode): void {
Pipewire.preferredDefaultAudioSink = sink;
}
function setDefaultSource(source: PwNode): void {
Pipewire.preferredDefaultAudioSource = source;
}
function init() {
}
}

View File

@@ -0,0 +1,140 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
Singleton {
id: root
property var wallpapers: []
property bool scanning: false
property string currentFolder: Config.initialized ? Config.runtime.appearance.background.slideshow.folder : "";
property int intervalMinutes: Config.initialized ? Config.runtime.appearance.background.slideshow.interval : 5
property bool enabled: Config.initialized ? Config.runtime.appearance.background.slideshow.enabled : false
property bool includeSubfolders: Config.initialized ? Config.runtime.appearance.background.slideshow.includeSubfolders : true
property bool initializedOnce: false
property bool hydratingFromConfig: true
signal wallpaperChanged(string path)
function nextWallpaper() {
if (wallpapers.length === 0) {
console.warn("WallpaperSlideshow: No wallpapers found in folder");
return;
}
const randomIndex = Math.floor(Math.random() * wallpapers.length);
const selectedPath = "file://" + wallpapers[randomIndex];
Config.updateKey("appearance.background.path", selectedPath);
wallpaperChanged(selectedPath);
// Regenerate colors
if (Config.runtime.appearance.colors.autogenerated) {
Quickshell.execDetached([
"nucleus", "ipc", "call", "global", "regenColors"
]);
}
}
function scanFolder() {
if (!currentFolder || currentFolder === "") {
wallpapers = [];
return;
}
scanning = true;
scanProcess.running = true;
}
// Timer for automatic wallpaper rotation
Timer {
id: slideshowTimer
interval: root.intervalMinutes * 60 * 1000 // Convert minutes into miliseconds
repeat: true
running: root.enabled && root.wallpapers.length > 0 && root.initializedOnce
onTriggered: root.nextWallpaper()
}
// Process to scan folder for images
Process {
id: scanProcess
command: root.includeSubfolders
? ["find", root.currentFolder, "-type", "f", "-iregex", ".*\\.\\(jpg\\|jpeg\\|png\\|webp\\|bmp\\|svg\\)$"]
: ["find", root.currentFolder, "-maxdepth", "1", "-type", "f", "-iregex", ".*\\.\\(jpg\\|jpeg\\|png\\|webp\\|bmp\\|svg\\)$"]
stdout: SplitParser {
splitMarker: ""
onRead: data => {
const lines = data.trim().split("\n").filter(line => line.length > 0);
root.wallpapers = lines;
root.scanning = false;
}
}
onExited: (exitCode, exitStatus) => {
root.scanning = false;
if (exitCode !== 0) {
console.warn("WallpaperSlideshow: Failed to scan folder");
root.wallpapers = [];
}
}
}
// Watch for folder changes - rescan and immediately change wallpaper
onCurrentFolderChanged: {
if (root.hydratingFromConfig)
return;
if (!currentFolder || currentFolder === "")
return;
scanning = true;
folderChangeScanProcess.running = true;
}
// Separate process for folder change scan (triggers immediate wallpaper change)
Process {
id: folderChangeScanProcess
command: root.includeSubfolders
? ["find", root.currentFolder, "-type", "f", "-iregex", ".*\\.\\(jpg\\|jpeg\\|png\\|webp\\|bmp\\|svg\\)$"]
: ["find", root.currentFolder, "-maxdepth", "1", "-type", "f", "-iregex", ".*\\.\\(jpg\\|jpeg\\|png\\|webp\\|bmp\\|svg\\)$"]
stdout: SplitParser {
splitMarker: ""
onRead: data => {
const lines = data.trim().split("\n").filter(line => line.length > 0);
root.wallpapers = lines;
root.scanning = false;
// Immediately change wallpaper when folder is changed
if (root.wallpapers.length > 0) {
root.nextWallpaper();
}
}
}
onExited: (exitCode, exitStatus) => {
root.scanning = false;
if (exitCode !== 0) {
console.warn("WallpaperSlideshow: Failed to scan folder");
root.wallpapers = [];
}
}
}
// Watch for includeSubfolders changes
onIncludeSubfoldersChanged: {
if (currentFolder && currentFolder !== "") {
scanFolder();
}
}
// Initial scan when config is loaded
Connections {
target: Config
function onInitializedChanged() {
if (Config.initialized && root.currentFolder && root.currentFolder !== "") {
root.scanFolder();
}
}
}
}

View File

@@ -0,0 +1,55 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
/*
Why use this service? Good question.
:- Cause xrandr works with all compositors to fetch monitor data.
*/
Item {
id: root
// Array of monitor objects: { name, width, height, x, y, physWidth, physHeight }
property var monitors: []
// Refresh monitors every 5 seconds
Timer {
interval: 5000
running: true
repeat: true
onTriggered: xrandrProc.running = true
}
Process {
id: xrandrProc
command: ["bash", "-c", "xrandr --query | grep ' connected '"]
running: true
stdout: StdioCollector {
onStreamFinished: { // I don't even know wtf is this I can't explain shit
const lines = text.trim().split("\n")
root.monitors = lines.map(line => {
const m = line.match(/^(\S+)\sconnected\s(\d+)x(\d+)\+(\d+)\+(\d+).*?(\d+)mm\sx\s(\d+)mm$/)
if (!m) return null
return {
name: m[1],
width: parseInt(m[2]),
height: parseInt(m[3]),
x: parseInt(m[4]),
y: parseInt(m[5]),
physWidth: parseInt(m[6]),
physHeight: parseInt(m[7])
}
}).filter(m => m)
}
}
}
function getMonitor(name) {
return monitors.find(m => m.name === name) || null
}
}

View File

@@ -0,0 +1,82 @@
// Zenith.properties
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
import qs.modules.functions
Singleton {
// current state and shit
property string currentChat: "default"
property string currentModel: "gpt-4o-mini"
property string pendingInput: ""
property bool loading: zenithProcess.running
// signals (needed for ui loading)
signal chatsListed(string text)
signal chatLoaded(string text)
signal aiReply(string text)
// process to load data and talk to zenith
Timer {
interval: 1000
repeat: true
running: true
onTriggered: listChatsProcess.running = true;
}
Process {
id: listChatsProcess
command: ["ls", FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats"]
running: true
stdout: StdioCollector {
onStreamFinished: chatsListed(text)
}
}
Process {
id: chatLoadProcess
stdout: StdioCollector {
onStreamFinished: chatLoaded(text)
}
}
Process {
id: zenithProcess
command: [
"zenith",
"--api", Config.runtime.misc.intelligence.apiKey,
"--chat", currentChat,
"-a",
"--model", currentModel,
pendingInput
]
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() !== "")
aiReply(text.trim())
}
}
}
// api shit
function loadChat(chatName) {
chatLoadProcess.command = [
"cat",
FileUtils.trimFileProtocol(Directories.config)
+ "/zenith/chats/" + chatName + ".txt"
]
chatLoadProcess.running = true
}
function send() {
zenithProcess.running = true
}
}