Bring backs most services

This commit is contained in:
quadbyte 2025-08-09 23:42:02 -04:00
parent 71cb79b08b
commit ec3bff68ac
16 changed files with 1038 additions and 13 deletions

2
Services/Location.qml Normal file
View file

@ -0,0 +1,2 @@
// Weather logic and caching
// Calendar Hollidays logic and caching

169
Services/MediaPlayer.qml Normal file
View file

@ -0,0 +1,169 @@
// pragma Singleton
// import QtQuick
// import Quickshell
// import Quickshell.Services.Mpris
// import qs.Services
// Singleton {
// id: manager
// property var currentPlayer: null
// property real currentPosition: 0
// property int selectedPlayerIndex: 0
// property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
// property string trackTitle: currentPlayer ? (currentPlayer.trackTitle
// || "Unknown Track") : ""
// property string trackArtist: currentPlayer ? (currentPlayer.trackArtist
// || "Unknown Artist") : ""
// property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum
// || "Unknown Album") : ""
// property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl
// || "") : ""
// property real trackLength: currentPlayer ? currentPlayer.length : 0
// property bool canPlay: currentPlayer ? currentPlayer.canPlay : false
// property bool canPause: currentPlayer ? currentPlayer.canPause : false
// property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false
// property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false
// property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
// property bool hasPlayer: getAvailablePlayers().length > 0
// Item {
// Component.onCompleted: {
// updateCurrentPlayer()
// }
// }
// function getAvailablePlayers() {
// if (!Mpris.players || !Mpris.players.values) {
// return []
// }
// let allPlayers = Mpris.players.values
// let controllablePlayers = []
// for (var i = 0; i < allPlayers.length; i++) {
// let player = allPlayers[i]
// if (player && player.canControl) {
// controllablePlayers.push(player)
// }
// }
// return controllablePlayers
// }
// function findActivePlayer() {
// let availablePlayers = getAvailablePlayers()
// if (availablePlayers.length === 0) {
// return null
// }
// if (selectedPlayerIndex < availablePlayers.length) {
// return availablePlayers[selectedPlayerIndex]
// } else {
// selectedPlayerIndex = 0
// return availablePlayers[0]
// }
// }
// // Switch to the most recently active player
// function updateCurrentPlayer() {
// let newPlayer = findActivePlayer()
// if (newPlayer !== currentPlayer) {
// currentPlayer = newPlayer
// currentPosition = currentPlayer ? currentPlayer.position : 0
// }
// }
// function playPause() {
// if (currentPlayer) {
// if (currentPlayer.isPlaying) {
// currentPlayer.pause()
// } else {
// currentPlayer.play()
// }
// }
// }
// function play() {
// if (currentPlayer && currentPlayer.canPlay) {
// currentPlayer.play()
// }
// }
// function pause() {
// if (currentPlayer && currentPlayer.canPause) {
// currentPlayer.pause()
// }
// }
// function next() {
// if (currentPlayer && currentPlayer.canGoNext) {
// currentPlayer.next()
// }
// }
// function previous() {
// if (currentPlayer && currentPlayer.canGoPrevious) {
// currentPlayer.previous()
// }
// }
// function seek(position) {
// if (currentPlayer && currentPlayer.canSeek) {
// currentPlayer.position = position
// currentPosition = position
// }
// }
// // Seek to position based on ratio (0.0 to 1.0)
// function seekByRatio(ratio) {
// if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
// let seekPosition = ratio * currentPlayer.length
// currentPlayer.position = seekPosition
// currentPosition = seekPosition
// }
// }
// // Update progress bar every second while playing
// Timer {
// id: positionTimer
// interval: 1000
// running: currentPlayer && currentPlayer.isPlaying
// && currentPlayer.length > 0
// && currentPlayer.playbackState === MprisPlaybackState.Playing
// repeat: true
// onTriggered: {
// if (currentPlayer && currentPlayer.isPlaying
// && currentPlayer.playbackState === MprisPlaybackState.Playing) {
// currentPosition = currentPlayer.position
// } else {
// running = false
// }
// }
// }
// // Reset position when switching to inactive player
// onCurrentPlayerChanged: {
// if (!currentPlayer || !currentPlayer.isPlaying
// || currentPlayer.playbackState !== MprisPlaybackState.Playing) {
// currentPosition = 0
// }
// }
// // Update current player when available players change
// Connections {
// target: Mpris.players
// function onValuesChanged() {
// updateCurrentPlayer()
// }
// }
// Cava {
// id: cava
// count: 44
// }
// // Expose cava values
// property alias cavaValues: cava.values
// }

345
Services/Network.qml Normal file
View file

@ -0,0 +1,345 @@
import QtQuick
import Quickshell.Io
QtObject {
id: root
property var networks: ({})
property string connectingSsid: ""
property string connectStatus: ""
property string connectStatusSsid: ""
property string connectError: ""
property string detectedInterface: ""
function signalIcon(signal) {
if (signal >= 80)
return "network_wifi"
if (signal >= 60)
return "network_wifi_3_bar"
if (signal >= 40)
return "network_wifi_2_bar"
if (signal >= 20)
return "network_wifi_1_bar"
return "wifi_0_bar"
}
function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--"
}
function refreshNetworks() {
existingNetwork.running = true
}
function connectNetwork(ssid, security) {
pendingConnect = {
"ssid": ssid,
"security": security,
"password": ""
}
doConnect()
}
function submitPassword(ssid, password) {
pendingConnect = {
"ssid": ssid,
"security": networks[ssid].security,
"password": password
}
doConnect()
}
function disconnectNetwork(ssid) {
disconnectProfileProcess.connectionName = ssid
disconnectProfileProcess.running = true
}
property var pendingConnect: null
function doConnect() {
const params = pendingConnect
if (!params)
return
connectingSsid = params.ssid
connectStatus = ""
connectStatusSsid = params.ssid
const targetNetwork = networks[params.ssid]
if (targetNetwork && targetNetwork.existing) {
upConnectionProcess.profileName = params.ssid
upConnectionProcess.running = true
pendingConnect = null
return
}
if (params.security && params.security !== "--") {
getInterfaceProcess.running = true
return
}
connectProcess.security = params.security
connectProcess.ssid = params.ssid
connectProcess.password = params.password
connectProcess.running = true
pendingConnect = null
}
property int refreshInterval: 25000
// Only refresh when we have an active connection
property bool hasActiveConnection: {
for (const net in networks) {
if (networks[net].connected) {
return true
}
}
return false
}
property Timer refreshTimer: Timer {
interval: root.refreshInterval
// Only run timer when we're connected to a network
running: root.hasActiveConnection
repeat: true
onTriggered: root.refreshNetworks()
}
// Force a refresh when menu is opened
function onMenuOpened() {
refreshNetworks()
}
function onMenuClosed() {// No need to do anything special on close
}
property Process disconnectProfileProcess: Process {
id: disconnectProfileProcess
property string connectionName: ""
running: false
command: ["nmcli", "connection", "down", connectionName]
onRunningChanged: {
if (!running) {
root.refreshNetworks()
}
}
}
property Process existingNetwork: Process {
id: existingNetwork
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n")
const networksMap = {}
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim()
if (!line)
continue
const parts = line.split(":")
if (parts.length < 2) {
console.warn("Malformed nmcli output line:", line)
continue
}
const ssid = parts[0]
const type = parts[1]
if (ssid) {
networksMap[ssid] = {
"ssid": ssid,
"type": type
}
}
}
scanProcess.existingNetwork = networksMap
scanProcess.running = true
}
}
}
property Process scanProcess: Process {
id: scanProcess
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
property var existingNetwork
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n")
const networksMap = {}
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim()
if (!line)
continue
const parts = line.split(":")
if (parts.length < 4) {
console.warn("Malformed nmcli output line:", line)
continue
}
const ssid = parts[0]
const security = parts[1]
const signal = parseInt(parts[2])
const inUse = parts[3] === "*"
if (ssid) {
if (!networksMap[ssid]) {
networksMap[ssid] = {
"ssid": ssid,
"security": security,
"signal": signal,
"connected": inUse,
"existing": ssid in scanProcess.existingNetwork
}
} else {
const existingNet = networksMap[ssid]
if (inUse) {
existingNet.connected = true
}
if (signal > existingNet.signal) {
existingNet.signal = signal
existingNet.security = security
}
}
}
}
root.networks = networksMap
scanProcess.existingNetwork = {}
}
}
}
property Process connectProcess: Process {
id: connectProcess
property string ssid: ""
property string password: ""
property string security: ""
running: false
command: {
if (password) {
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]
} else {
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]
}
}
stdout: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "success"
root.connectStatusSsid = connectProcess.ssid
root.connectError = ""
root.refreshNetworks()
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "error"
root.connectStatusSsid = connectProcess.ssid
root.connectError = text
}
}
}
property Process getInterfaceProcess: Process {
id: getInterfaceProcess
running: false
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
onStreamFinished: {
var lines = text.split("\n")
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].split(":")
if (parts[1] === "wifi" && parts[2] !== "unavailable") {
root.detectedInterface = parts[0]
break
}
}
if (root.detectedInterface) {
var params = root.pendingConnect
addConnectionProcess.ifname = root.detectedInterface
addConnectionProcess.ssid = params.ssid
addConnectionProcess.password = params.password
addConnectionProcess.profileName = params.ssid
addConnectionProcess.security = params.security
addConnectionProcess.running = true
} else {
root.connectStatus = "error"
root.connectStatusSsid = root.pendingConnect.ssid
root.connectError = "No Wi-Fi interface found."
root.connectingSsid = ""
root.pendingConnect = null
}
}
}
}
property Process addConnectionProcess: Process {
id: addConnectionProcess
property string ifname: ""
property string ssid: ""
property string password: ""
property string profileName: ""
property string security: ""
running: false
command: {
var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid]
if (security && security !== "--") {
cmd.push("wifi-sec.key-mgmt")
cmd.push("wpa-psk")
cmd.push("wifi-sec.psk")
cmd.push(password)
}
return cmd
}
stdout: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName
upConnectionProcess.running = true
}
}
stderr: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName
upConnectionProcess.running = true
}
}
}
property Process upConnectionProcess: Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "success"
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : ""
root.connectError = ""
root.pendingConnect = null
root.refreshNetworks()
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "error"
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : ""
root.connectError = text
root.pendingConnect = null
}
}
}
Component.onCompleted: {
refreshNetworks()
}
}

140
Services/Niri.qml Normal file
View file

@ -0,0 +1,140 @@
pragma Singleton
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property var workspaces: []
property var windows: []
property int focusedWindowIndex: -1
property bool inOverview: false
property string focusedWindowTitle: "(No active window)"
function updateFocusedWindowTitle() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
focusedWindowTitle = windows[focusedWindowIndex].title
|| "(Unnamed window)"
} else {
focusedWindowTitle = "(No active window)"
}
}
onWindowsChanged: updateFocusedWindowTitle()
onFocusedWindowIndexChanged: updateFocusedWindowTitle()
Component.onCompleted: {
eventStream.running = true
}
Process {
id: workspaceProcess
running: false
command: ["niri", "msg", "--json", "workspaces"]
stdout: SplitParser {
onRead: function (line) {
try {
const workspacesData = JSON.parse(line)
const workspacesList = []
for (const ws of workspacesData) {
workspacesList.push({
"id": ws.id,
"idx": ws.idx,
"name": ws.name || "",
"output": ws.output || "",
"isFocused": ws.is_focused === true,
"isActive": ws.is_active === true,
"isUrgent": ws.is_urgent === true,
"isOccupied": ws.active_window_id ? true : false
})
}
workspacesList.sort((a, b) => {
if (a.output !== b.output) {
return a.output.localeCompare(b.output)
}
return a.id - b.id
})
root.workspaces = workspacesList
} catch (e) {
console.error("Failed to parse workspaces:", e, line)
}
}
}
}
Process {
id: eventStream
running: false
command: ["niri", "msg", "--json", "event-stream"]
stdout: SplitParser {
onRead: data => {
try {
const event = JSON.parse(data.trim())
if (event.WorkspacesChanged) {
workspaceProcess.running = true
} else if (event.WindowsChanged) {
try {
const windowsData = event.WindowsChanged.windows
const windowsList = []
for (const win of windowsData) {
windowsList.push({
"id": win.id,
"title": win.title || "",
"appId": win.app_id || "",
"workspaceId": win.workspace_id || null,
"isFocused": win.is_focused === true
})
}
windowsList.sort((a, b) => a.id - b.id)
root.windows = windowsList
for (var i = 0; i < windowsList.length; i++) {
if (windowsList[i].isFocused) {
root.focusedWindowIndex = i
break
}
}
} catch (e) {
console.error("Error parsing windows event:", e)
}
} else if (event.WorkspaceActivated) {
workspaceProcess.running = true
} else if (event.WindowFocusChanged) {
try {
const focusedId = event.WindowFocusChanged.id
if (focusedId) {
root.focusedWindowIndex = root.windows.findIndex(
w => w.id === focusedId)
if (root.focusedWindowIndex < 0) {
root.focusedWindowIndex = 0
}
} else {
root.focusedWindowIndex = -1
}
} catch (e) {
console.error("Error parsing window focus event:", e)
}
} else if (event.OverviewOpenedOrClosed) {
try {
root.inOverview = event.OverviewOpenedOrClosed.is_open === true
} catch (e) {
console.error("Error parsing overview state:", e)
}
}
} catch (e) {
console.error("Error parsing event stream:", e, data)
}
}
}
}
}

View file

@ -55,8 +55,8 @@ Singleton {
property int marginTiny: 4
property int marginSmall: 8
property int marginMedium: 12
property int marginLarge: 18
property int marginXL: 24
property int marginLarge: 16
property int marginXL: 20
// Opacity
property real opacityLight: 0.25

47
Services/SysInfo.qml Normal file
View file

@ -0,0 +1,47 @@
pragma Singleton
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
Singleton {
id: manager //TBC
property string updateInterval: "2s"
property string cpuUsageStr: ""
property string cpuTempStr: ""
property string memoryUsageStr: ""
property string memoryUsagePerStr: ""
property real cpuUsage: 0
property real memoryUsage: 0
property real cpuTemp: 0
property real diskUsage: 0
property real memoryUsagePer: 0
property string diskUsageStr: ""
Process {
id: zigstatProcess
running: true
command: [Quickshell.shellDir + "/Programs/zigstat", updateInterval]
stdout: SplitParser {
onRead: function (line) {
try {
const data = JSON.parse(line)
cpuUsage = +data.cpu
cpuTemp = +data.cputemp
memoryUsage = +data.mem
memoryUsagePer = +data.memper
diskUsage = +data.diskper
cpuUsageStr = data.cpu + "%"
cpuTempStr = data.cputemp + "°C"
memoryUsageStr = data.mem + "G"
memoryUsagePerStr = data.memper + "%"
diskUsageStr = data.diskper + "%"
} catch (e) {
console.error("Failed to parse zigstat output:", e)
}
}
}
}
}

136
Services/Wallpapers.qml Normal file
View file

@ -0,0 +1,136 @@
pragma Singleton
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
Singleton {
id: manager //TBC
Item {
Component.onCompleted: {
loadWallpapers()
setCurrentWallpaper(currentWallpaper, true)
toggleRandomWallpaper()
}
}
property var wallpaperList: []
property string currentWallpaper: Settings.settings.currentWallpaper
property bool scanning: false
property string transitionType: Settings.settings.transitionType
property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"]
function loadWallpapers() {
scanning = true
wallpaperList = []
folderModel.folder = ""
folderModel.folder = "file://" + (Settings.settings.wallpaperFolder
!== undefined ? Settings.settings.wallpaperFolder : "")
}
function changeWallpaper(path) {
setCurrentWallpaper(path)
}
function setCurrentWallpaper(path, isInitial) {
currentWallpaper = path
if (!isInitial) {
Settings.settings.currentWallpaper = path
}
if (Settings.settings.useSWWW) {
if (Settings.settings.transitionType === "random") {
transitionType = randomChoices[Math.floor(Math.random(
) * randomChoices.length)]
} else {
transitionType = Settings.settings.transitionType
}
changeWallpaperProcess.running = true
}
if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart()
}
generateTheme()
}
function setRandomWallpaper() {
var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex]
if (!randomPath) {
return
}
setCurrentWallpaper(randomPath)
}
function toggleRandomWallpaper() {
if (Settings.settings.randomWallpaper && !randomWallpaperTimer.running) {
randomWallpaperTimer.start()
setRandomWallpaper()
} else if (!Settings.settings.randomWallpaper
&& randomWallpaperTimer.running) {
randomWallpaperTimer.stop()
}
}
function restartRandomWallpaperTimer() {
if (Settings.settings.randomWallpaper) {
randomWallpaperTimer.stop()
randomWallpaperTimer.start()
}
}
function generateTheme() {
if (Settings.settings.useWallpaperTheme) {
generateThemeProcess.running = true
}
}
Timer {
id: randomWallpaperTimer
interval: Settings.settings.wallpaperInterval * 1000
running: false
repeat: true
onTriggered: setRandomWallpaper()
triggeredOnStart: false
}
FolderListModel {
id: folderModel
// Swww supports many images format but Quickshell only support a subset of those.
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
showDirs: false
sortField: FolderListModel.Name
onStatusChanged: {
if (status === FolderListModel.Ready) {
var files = []
var filesSwww = []
for (var i = 0; i < count; i++) {
var filepath = (Settings.settings.wallpaperFolder
!== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get(
i, "fileName")
files.push(filepath)
}
wallpaperList = files
scanning = false
}
}
}
Process {
id: changeWallpaperProcess
command: ["swww", "img", "--resize", Settings.settings.wallpaperResize, "--transition-fps", Settings.settings.transitionFps.toString(
), "--transition-type", transitionType, "--transition-duration", Settings.settings.transitionDuration.toString(
), currentWallpaper]
running: false
}
Process {
id: generateThemeProcess
command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Templates"]
workingDirectory: Quickshell.shellDir
running: false
}
}

156
Services/Workspaces.qml Normal file
View file

@ -0,0 +1,156 @@
pragma Singleton
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Services
Singleton {
id: root
property ListModel workspaces: ListModel {}
property bool isHyprland: false
property bool isNiri: false
property var hlWorkspaces: Hyprland.workspaces.values
// Detect which compositor we're using
Component.onCompleted: {
console.log("WorkspaceManager initializing...")
detectCompositor()
}
function detectCompositor() {
try {
try {
if (Hyprland.eventSocketPath) {
console.log("Detected Hyprland compositor")
isHyprland = true
isNiri = false
initHyprland()
return
}
} catch (e) {
console.log("Hyprland not available:", e)
}
if (typeof Niri !== "undefined") {
console.log("Detected Niri service")
isHyprland = false
isNiri = true
initNiri()
return
}
console.log("No supported compositor detected")
} catch (e) {
console.error("Error detecting compositor:", e)
}
}
// Initialize Hyprland integration
function initHyprland() {
try {
// Fixes the odd workspace issue.
Hyprland.refreshWorkspaces()
// hlWorkspaces = Hyprland.workspaces.values;
// updateHyprlandWorkspaces();
return true
} catch (e) {
console.error("Error initializing Hyprland:", e)
isHyprland = false
return false
}
}
onHlWorkspacesChanged: {
updateHyprlandWorkspaces()
}
Connections {
target: Hyprland.workspaces
function onValuesChanged() {
updateHyprlandWorkspaces()
}
}
Connections {
target: Hyprland
function onRawEvent(event) {
updateHyprlandWorkspaces()
}
}
function updateHyprlandWorkspaces() {
workspaces.clear()
try {
for (var i = 0; i < hlWorkspaces.length; i++) {
const ws = hlWorkspaces[i]
workspaces.append({
"id": i,
"idx": ws.id,
"name": ws.name || "",
"output": ws.monitor?.name || "",
"isActive": ws.active === true,
"isFocused": ws.focused === true,
"isUrgent": ws.urgent === true
})
}
workspacesChanged()
} catch (e) {
console.error("Error updating Hyprland workspaces:", e)
}
}
function initNiri() {
updateNiriWorkspaces()
}
Connections {
target: Niri
function onWorkspacesChanged() {
updateNiriWorkspaces()
}
}
function updateNiriWorkspaces() {
const niriWorkspaces = Niri.workspaces || []
workspaces.clear()
for (var i = 0; i < niriWorkspaces.length; i++) {
const ws = niriWorkspaces[i]
workspaces.append({
"id": ws.id,
"idx": ws.idx || 1,
"name": ws.name || "",
"output": ws.output || "",
"isFocused": ws.isFocused === true,
"isActive": ws.isActive === true,
"isUrgent": ws.isUrgent === true,
"isOccupied": ws.isOccupied === true
})
}
workspacesChanged()
}
function switchToWorkspace(workspaceId) {
if (isHyprland) {
try {
Hyprland.dispatch(`workspace ${workspaceId}`)
} catch (e) {
console.error("Error switching Hyprland workspace:", e)
}
} else if (isNiri) {
try {
Quickshell.execDetached(
["niri", "msg", "action", "focus-workspace", workspaceId.toString(
)])
} catch (e) {
console.error("Error switching Niri workspace:", e)
}
} else {
console.warn("No supported compositor detected for workspace switching")
}
}
}