From ec3bff68acd874bb82a3c0a13e1b44c4ae4d71e9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 23:42:02 -0400 Subject: [PATCH] Bring backs most services --- Modules/Background/Background.qml | 0 Modules/Background/Overview.qml | 0 Modules/Background/WallpaperPicker.qml | 0 Modules/Bar/Bar.qml | 14 +- Modules/DemoPanel/DemoPanel.qml | 21 +- Services/Location.qml | 2 + Services/MediaPlayer.qml | 169 ++++++++++++ Services/Network.qml | 345 +++++++++++++++++++++++++ Services/Niri.qml | 140 ++++++++++ Services/Style.qml | 4 +- Services/SysInfo.qml | 47 ++++ Services/Wallpapers.qml | 136 ++++++++++ Services/Workspaces.qml | 156 +++++++++++ Widgets/NCalendar.qml | 6 +- Widgets/NDivider.qml | 10 + Widgets/NIconButton.qml | 1 - 16 files changed, 1038 insertions(+), 13 deletions(-) create mode 100644 Modules/Background/Background.qml create mode 100644 Modules/Background/Overview.qml create mode 100644 Modules/Background/WallpaperPicker.qml create mode 100644 Services/Location.qml create mode 100644 Services/MediaPlayer.qml create mode 100644 Services/Network.qml create mode 100644 Services/Niri.qml create mode 100644 Services/SysInfo.qml create mode 100644 Services/Wallpapers.qml create mode 100644 Services/Workspaces.qml create mode 100644 Widgets/NDivider.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Background/WallpaperPicker.qml b/Modules/Background/WallpaperPicker.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9ce9a32..e4c7f3b 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -25,7 +25,9 @@ PanelWindow { Item { anchors.fill: parent + clip: true + // Background fill Rectangle { id: bar anchors.fill: parent @@ -35,9 +37,10 @@ PanelWindow { Row { id: leftSection - anchors.left: bar.left + height: parent.height + anchors.left: parent.left anchors.leftMargin: Style.marginMedium * scaling - anchors.verticalCenter: bar.verticalCenter + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginMedium * scaling NText { @@ -47,9 +50,11 @@ PanelWindow { Row { id: centerSection + height: parent.height anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: bar.verticalCenter + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginMedium * scaling + NText { text: "Center" } @@ -57,7 +62,7 @@ PanelWindow { Row { id: rightSection - + height: parent.height anchors.right: bar.right anchors.rightMargin: Style.marginMedium * scaling anchors.verticalCenter: bar.verticalCenter @@ -65,6 +70,7 @@ PanelWindow { NText { text: "Right" + Layout.alignment: Qt.AlignVCenter } Clock {} diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index e1f336e..89fec51 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -22,7 +22,7 @@ NPanel { border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 500 * scaling - height: 300 + height: 400 anchors.centerIn: parent @@ -33,15 +33,15 @@ NPanel { ColumnLayout { anchors.fill: parent - anchors.margins: 16 * scaling - spacing: 12 * scaling - + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling // NIconButton ColumnLayout { spacing: 16 * scaling NText { text: "NIconButton" + color: Colors.accentSecondary } NIconButton { @@ -54,15 +54,18 @@ NPanel { myTooltip.hide(); } } + + NDivider {Layout.fillWidth: true} } // NToggle ColumnLayout { - spacing: 16 * scaling + spacing: Style.marginLarge * scaling uniformCellSizes: true NText { text: "NToggle + NTooltip" + color: Colors.accentSecondary } NToggle { @@ -79,16 +82,24 @@ NPanel { positionAbove: false text: "Hello world" } + NDivider { + Layout.fillWidth: true + } } // NSlider ColumnLayout { spacing: 16 * scaling + NText { text: "NSlider" + color: Colors.accentSecondary } NSlider {} + NDivider { + Layout.fillWidth: true + } } } diff --git a/Services/Location.qml b/Services/Location.qml new file mode 100644 index 0000000..9458435 --- /dev/null +++ b/Services/Location.qml @@ -0,0 +1,2 @@ +// Weather logic and caching +// Calendar Hollidays logic and caching \ No newline at end of file diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml new file mode 100644 index 0000000..b9d17d0 --- /dev/null +++ b/Services/MediaPlayer.qml @@ -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 +// } diff --git a/Services/Network.qml b/Services/Network.qml new file mode 100644 index 0000000..7427a2a --- /dev/null +++ b/Services/Network.qml @@ -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() + } +} diff --git a/Services/Niri.qml b/Services/Niri.qml new file mode 100644 index 0000000..bb7f1c4 --- /dev/null +++ b/Services/Niri.qml @@ -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) + } + } + } + } +} diff --git a/Services/Style.qml b/Services/Style.qml index 407af8a..12b0b6e 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -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 diff --git a/Services/SysInfo.qml b/Services/SysInfo.qml new file mode 100644 index 0000000..39bef22 --- /dev/null +++ b/Services/SysInfo.qml @@ -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) + } + } + } + } +} diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml new file mode 100644 index 0000000..54c1cfb --- /dev/null +++ b/Services/Wallpapers.qml @@ -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 + } +} diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml new file mode 100644 index 0000000..fc1ab9b --- /dev/null +++ b/Services/Workspaces.qml @@ -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") + } + } +} diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index 59c1519..d1518e8 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -18,7 +18,7 @@ NPanel { border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 340 * scaling - height: 300 + height: 320 // TBC anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Style.marginTiny * scaling @@ -66,6 +66,10 @@ NPanel { } } + NDivider { + Layout.fillWidth: true + } + DayOfWeekRow { Layout.fillWidth: true spacing: 0 diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml new file mode 100644 index 0000000..bd47353 --- /dev/null +++ b/Widgets/NDivider.qml @@ -0,0 +1,10 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Services + +Rectangle { + width: parent.width + height: Math.max(1, Style.borderThin * scaling) + color: Colors.outline +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index a5bc375..6155345 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -22,7 +22,6 @@ Rectangle { color: root.hovering ? Colors.accentPrimary : "transparent" Text { - id: iconText anchors.centerIn: parent text: root.icon font.family: "Material Symbols Outlined"