Merge branch 'main' into feat/calendar-events

This commit is contained in:
Kainoa Kanter
2025-10-12 10:39:03 -07:00
122 changed files with 4714 additions and 1125 deletions
+7 -1
View File
@@ -41,6 +41,12 @@ Singleton {
"path": "~/.config/qt6ct/colors/noctalia.conf"
}]
},
"kcolorscheme": {
"input": "kcolorscheme.colors",
"outputs": [{
"path": "~/.local/share/color-schemes/noctalia.colors"
}]
},
"fuzzel": {
"input": "fuzzel.conf",
"outputs": [{
@@ -55,7 +61,7 @@ Singleton {
}],
"postProcess": () => `${colorsApplyScript} pywalfox\n`
},
"vesktop": {
"discord_vesktop": {
"input": "vesktop.css",
"outputs": [{
"path": "~/.config/vesktop/themes/noctalia.theme.css"
+8
View File
@@ -111,6 +111,14 @@ Singleton {
}
}
function increaseInputVolume() {
setInputVolume(inputVolume + stepVolume)
}
function decreaseInputVolume() {
setInputVolume(inputVolume - stepVolume)
}
function setInputVolume(newVolume: real) {
if (source?.ready && source?.audio) {
// Clamp it accordingly
+9 -2
View File
@@ -43,7 +43,8 @@ Singleton {
"showIcon": true,
"autoHide": false,
"scrollingMode": "hover",
"width": 145
"width": 145,
"colorizeIcons": false
},
"Battery": {
"allowUserSettings": true,
@@ -114,7 +115,13 @@ Singleton {
"Taskbar": {
"allowUserSettings": true,
"onlySameOutput": true,
"onlyActiveWorkspaces": true
"onlyActiveWorkspaces": true,
"colorizeIcons": false
},
"Tray": {
"allowUserSettings": true,
"blacklist": [],
"colorizeIcons": false
},
"Workspace": {
"allowUserSettings": true,
+2 -1
View File
@@ -161,7 +161,8 @@ Singleton {
// Check if any Matugen templates are enabled
function hasEnabledMatugenTemplates() {
return Settings.data.templates.gtk || Settings.data.templates.qt || Settings.data.templates.kitty || Settings.data.templates.ghostty || Settings.data.templates.foot || Settings.data.templates.fuzzel || Settings.data.templates.vesktop || Settings.data.templates.pywalfox
return Settings.data.templates.gtk || Settings.data.templates.qt || Settings.data.templates.kitty || Settings.data.templates.ghostty || Settings.data.templates.foot || Settings.data.templates.fuzzel || Settings.data.templates.discord || Settings.data.templates.discord_vesktop || Settings.data.templates.discord_webcord
|| Settings.data.templates.discord_armcord || Settings.data.templates.discord_equibop || Settings.data.templates.discord_lightcord || Settings.data.templates.discord_dorion || Settings.data.templates.pywalfox
}
// Writer to colors.json using a JsonAdapter for safety
+90 -1
View File
@@ -2,6 +2,7 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
@@ -18,6 +19,10 @@ Singleton {
property ListModel windows: ListModel {}
property int focusedWindowIndex: -1
// Display scale data
property var displayScales: ({})
property bool displayScalesLoaded: false
// Generic events
signal workspaceChanged
signal activeWindowChanged
@@ -26,7 +31,18 @@ Singleton {
// Backend service loader
property var backend: null
// Cache file path
property string displayCachePath: ""
Component.onCompleted: {
// Setup cache path (needs Settings to be available)
Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
displayCachePath = Settings.cacheDir + "display.json"
displayCacheFileView.path = displayCachePath
}
})
detectCompositor()
}
@@ -69,6 +85,31 @@ Singleton {
}
}
// Cache FileView for display scales
FileView {
id: displayCacheFileView
printErrors: false
watchChanges: false
adapter: JsonAdapter {
id: displayCacheAdapter
property var displays: ({})
}
onLoaded: {
// Load cached display scales
displayScales = displayCacheAdapter.displays || {}
displayScalesLoaded = true
// Logger.log("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales))
}
onLoadFailed: {
// Cache doesn't exist yet, will be created on first update
displayScalesLoaded = true
// Logger.log("CompositorService", "No display cache found, will create on first update")
}
}
// Hyprland backend component
Component {
id: hyprlandComponent
@@ -151,6 +192,50 @@ Singleton {
windowListChanged()
}
// Update display scales from backend
function updateDisplayScales() {
if (!backend || !backend.queryDisplayScales) {
Logger.warn("CompositorService", "Backend does not support display scale queries")
return
}
backend.queryDisplayScales()
}
// Called by backend when display scales are ready
function onDisplayScalesUpdated(scales) {
displayScales = scales
saveDisplayScalesToCache()
displayScalesChanged()
Logger.log("CompositorService", "Display scales updated")
}
// Save display scales to cache
function saveDisplayScalesToCache() {
if (!displayCachePath) {
return
}
displayCacheAdapter.displays = displayScales
displayCacheFileView.writeAdapter()
}
// Public function to get scale for a specific display
function getDisplayScale(displayName) {
if (!displayName || !displayScales[displayName]) {
return 1.0
}
return displayScales[displayName].scale || 1.0
}
// Public function to get all display info for a specific display
function getDisplayInfo(displayName) {
if (!displayName || !displayScales[displayName]) {
return null
}
return displayScales[displayName]
}
// Get focused window
function getFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) {
@@ -162,7 +247,11 @@ Singleton {
// Get focused window title
function getFocusedWindowTitle() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) {
return windows.get(focusedWindowIndex).title || ""
var title = windows.get(focusedWindowIndex).title
if (title !== undefined) {
title = title.replace(/(\r\n|\n|\r)/g, "")
}
return title || ""
}
return ""
}
+75
View File
@@ -0,0 +1,75 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.ControlCenter.Widgets
Singleton {
id: root
// Widget registry object mapping widget names to components
property var widgets: ({
"Bluetooth": bluetoothComponent,
"Notifications": notificationsComponent,
"KeepAwake": keepAwakeComponent,
"NightLight": nightLightComponent,
"PowerProfile": powerProfileComponent,
"ScreenRecorder": screenRecorderComponent,
"WiFi": wiFiComponent,
"WallpaperSelector": wallpaperSelectorComponent
})
property var widgetMetadata: ({})
// Component definitions - these are loaded once at startup
property Component bluetoothComponent: Component {
Bluetooth {}
}
property Component notificationsComponent: Component {
Notifications {}
}
property Component keepAwakeComponent: Component {
KeepAwake {}
}
property Component nightLightComponent: Component {
NightLight {}
}
property Component powerProfileComponent: Component {
PowerProfile {}
}
property Component screenRecorderComponent: Component {
ScreenRecorder {}
}
property Component wiFiComponent: Component {
WiFi {}
}
property Component wallpaperSelectorComponent: Component {
WallpaperSelector {}
}
function init() {
Logger.log("ControlCenterWidgetRegistry", "Service started")
}
// ------------------------------
// Helper function to get widget component by name
function getWidget(id) {
return widgets[id] || null
}
// Helper function to check if widget exists
function hasWidget(id) {
return id in widgets
}
// Get list of available widget id
function getAvailableWidgets() {
return Object.keys(widgets)
}
// Helper function to check if widget has user settings
function widgetHasUserSettings(id) {
return (widgetMetadata[id] !== undefined) && (widgetMetadata[id].allowUserSettings === true)
}
}
+70 -1
View File
@@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Commons
Item {
@@ -15,6 +16,7 @@ Item {
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// Hyprland-specific properties
property bool initialized: false
@@ -40,6 +42,7 @@ Item {
Qt.callLater(() => {
safeUpdateWorkspaces()
safeUpdateWindows()
queryDisplayScales()
})
initialized = true
Logger.log("HyprlandService", "Initialized successfully")
@@ -48,6 +51,67 @@ Item {
}
}
// Query display scales
function queryDisplayScales() {
hyprlandMonitorsProcess.running = true
}
// Hyprland monitors process for display scale detection
// Hyprland monitors process for display scale detection
Process {
id: hyprlandMonitorsProcess
running: false
command: ["hyprctl", "monitors", "-j"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
// Accumulate lines instead of parsing each one
hyprlandMonitorsProcess.accumulatedOutput += line
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.error("HyprlandService", "Failed to query monitors, exit code:", exitCode)
accumulatedOutput = ""
return
}
try {
const monitorsData = JSON.parse(accumulatedOutput)
const scales = {}
for (const monitor of monitorsData) {
if (monitor.name) {
scales[monitor.name] = {
"name": monitor.name,
"scale": monitor.scale || 1.0,
"width": monitor.width || 0,
"height": monitor.height || 0,
"refresh_rate": monitor.refreshRate || 0,
"x": monitor.x || 0,
"y": monitor.y || 0,
"active_workspace": monitor.activeWorkspace ? monitor.activeWorkspace.id : -1,
"vrr": monitor.vrr || false,
"focused": monitor.focused || false
}
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
Logger.error("HyprlandService", "Failed to parse monitors:", e)
} finally {
// Clear accumulated output for next query
accumulatedOutput = ""
}
}
}
// Safe update wrapper
function safeUpdate() {
safeUpdateWindows()
@@ -188,7 +252,7 @@ Item {
"id": windowId,
"title": title,
"appId": appId,
"workspaceId": wsId,
"workspaceId": wsId || -1,
"isFocused": focused,
"output": output
}
@@ -268,6 +332,11 @@ Item {
safeUpdateWorkspaces()
workspaceChanged()
updateTimer.restart()
const monitorsEvents = ["configreloaded", "monitoradded", "monitorremoved", "monitoraddedv2", "monitorremovedv2"]
if (monitorsEvents.includes(event.name)) {
Qt.callLater(queryDisplayScales)
}
}
}
+7 -3
View File
@@ -112,10 +112,14 @@ Item {
function muteOutput() {
AudioService.setOutputMuted(!AudioService.muted)
}
function increaseInput() {
AudioService.increaseInputVolume()
}
function decreaseInput() {
AudioService.decreaseInputVolume()
}
function muteInput() {
if (AudioService.source?.ready && AudioService.source?.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
AudioService.setInputMuted(!AudioService.inputMuted)
}
}
+117 -48
View File
@@ -69,65 +69,134 @@ Singleton {
})
}
// Applications configuration
readonly property var applications: [{
"name": "gtk",
"templates": [{
"version": "gtk3",
"output": "~/.config/gtk-3.0/gtk.css"
}, {
"version": "gtk4",
"output": "~/.config/gtk-4.0/gtk.css"
}],
"input": "gtk.css",
"postHook": "gsettings set org.gnome.desktop.interface color-scheme prefer-{mode}"
}, {
"name": "qt",
"templates": [{
"version": "qt5",
"output": "~/.config/qt5ct/colors/noctalia.conf"
}, {
"version": "qt6",
"output": "~/.config/qt6ct/colors/noctalia.conf"
}],
"input": "qtct.conf"
}, {
"name": "kcolorscheme",
"templates": [{
"version": "kcolorscheme",
"output": "~/.local/share/color-schemes/noctalia.colors"
}],
"input": "kcolorscheme.colors"
}, {
"name": "fuzzel",
"templates": [{
"version": "fuzzel",
"output": "~/.config/fuzzel/themes/noctalia"
}],
"input": "fuzzel.conf",
"postHook": AppThemeService.colorsApplyScript + " fuzzel"
}, {
"name": "pywalfox",
"templates": [{
"version": "pywalfox",
"output": "~/.cache/wal/colors.json"
}],
"input": "pywalfox.json",
"postHook": AppThemeService.colorsApplyScript + " pywalfox"
}, {
"name": "discord_vesktop",
"templates": [{
"version": "discord_vesktop",
"output": "~/.config/vesktop/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}, {
"name": "discord_webcord",
"templates": [{
"version": "discord_webcord",
"output": "~/.config/webcord/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}, {
"name": "discord_armcord",
"templates": [{
"version": "discord_armcord",
"output": "~/.config/armcord/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}, {
"name": "discord_equibop",
"templates": [{
"version": "discord_equibop",
"output": "~/.config/equibop/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}, {
"name": "discord_lightcord",
"templates": [{
"version": "discord_lightcord",
"output": "~/.config/lightcord/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}, {
"name": "discord_dorion",
"templates": [{
"version": "discord_dorion",
"output": "~/.config/dorion/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}]
// --------------------------------
function addApplicationTemplates(lines, mode) {
var applications = [{
"name": "gtk",
"templates": [{
"version": "gtk3",
"output": "~/.config/gtk-3.0/gtk.css"
}, {
"version": "gtk4",
"output": "~/.config/gtk-4.0/gtk.css"
}],
"input": "gtk.css",
"postHook": "gsettings set org.gnome.desktop.interface color-scheme prefer-" + mode
}, {
"name": "qt",
"templates": [{
"version": "qt5",
"output": "~/.config/qt5ct/colors/noctalia.conf"
}, {
"version": "qt6",
"output": "~/.config/qt6ct/colors/noctalia.conf"
}],
"input": "qtct.conf"
}, {
"name": "fuzzel",
"templates": [{
"version": "fuzzel",
"output": "~/.config/fuzzel/themes/noctalia"
}],
"input": "fuzzel.conf",
"postHook": AppThemeService.colorsApplyScript + " fuzzel"
}, {
"name": "pywalfox",
"templates": [{
"version": "pywalfox",
"output": "~/.cache/wal/colors.json"
}],
"input": "pywalfox.json",
"postHook": AppThemeService.colorsApplyScript + " pywalfox"
}, {
"name": "vesktop",
"templates": [{
"version": "vesktop",
"output": "~/.config/vesktop/themes/noctalia.theme.css"
}],
"input": "vesktop.css"
}]
applications.forEach(function (app) {
if (Settings.data.templates[app.name]) {
// Check if app has a condition and if it's met
var shouldInclude = true
if (app.condition !== undefined) {
shouldInclude = app.condition
}
if (Settings.data.templates[app.name] && shouldInclude) {
app.templates.forEach(function (template) {
lines.push("\n[templates." + template.version + "]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/MatugenTemplates/' + app.input + '"')
lines.push('output_path = "' + template.output + '"')
if (app.postHook) {
lines.push('post_hook = "' + app.postHook + '"')
var postHook = app.postHook.replace("{mode}", mode)
lines.push('post_hook = "' + postHook + '"')
}
})
}
})
}
// Extract Discord clients from applications array
readonly property var discordClients: {
var clients = []
for (var i = 0; i < applications.length; i++) {
var app = applications[i]
if (app.name && app.name.startsWith("discord_")) {
var clientName = app.name.replace("discord_", "")
var themePath = app.templates[0].output
var configPath = themePath.replace("/themes/noctalia.theme.css", "")
clients.push({
"name": clientName,
"configPath": configPath,
"themePath": themePath
})
}
}
return clients
}
}
+125 -61
View File
@@ -14,7 +14,7 @@ Singleton {
property bool isSeeking: false
property int selectedPlayerIndex: 0
property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing || currentPlayer.isPlaying) : false
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : ""
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle !== undefined ? currentPlayer.trackTitle.replace(/(\r\n|\n|\r)/g, "") : "") : ""
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : ""
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : ""
property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
@@ -36,63 +36,126 @@ Singleton {
}
let allPlayers = Mpris.players.values
let controllablePlayers = []
let finalPlayers = []
const genericBrowsers = ["firefox", "chromium", "chrome"]
// Apply blacklist and controllable filter
const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : []
// Separate players into specific and generic lists
let specificPlayers = []
let genericPlayers = []
for (var i = 0; i < allPlayers.length; i++) {
let player = allPlayers[i]
if (!player)
continue
const identity = String(player.identity || "")
const busName = String(player.busName || "")
const desktop = String(player.desktopEntry || "")
const idKey = identity.toLowerCase()
const match = blacklist.find(b => {
const s = String(b || "").toLowerCase()
return s && (idKey.includes(s) || busName.toLowerCase().includes(s) || desktop.toLowerCase().includes(s))
})
if (match)
continue
if (player.canControl)
controllablePlayers.push(player)
const identity = String(allPlayers[i].identity || "").toLowerCase()
if (genericBrowsers.some(b => identity.includes(b))) {
genericPlayers.push(allPlayers[i])
} else {
specificPlayers.push(allPlayers[i])
}
}
let matchedGenericIndices = {}
// For each specific player, try to find and pair it with a generic partner
for (var i = 0; i < specificPlayers.length; i++) {
let specificPlayer = specificPlayers[i]
let title1 = String(specificPlayer.trackTitle || "").trim()
let wasMatched = false
if (title1) {
for (var j = 0; j < genericPlayers.length; j++) {
if (matchedGenericIndices[j])
continue
let genericPlayer = genericPlayers[j]
let title2 = String(genericPlayer.trackTitle || "").trim()
if (title2 && (title1.includes(title2) || title2.includes(title1))) {
let dataPlayer = genericPlayer
let identityPlayer = specificPlayer
let scoreSpecific = (specificPlayer.trackArtUrl ? 1 : 0)
let scoreGeneric = (genericPlayer.trackArtUrl ? 1 : 0)
if (scoreSpecific > scoreGeneric) {
dataPlayer = specificPlayer
}
let virtualPlayer = {
"identity": identityPlayer.identity,
"desktopEntry": identityPlayer.desktopEntry,
"trackTitle": dataPlayer.trackTitle,
"trackArtist": dataPlayer.trackArtist,
"trackAlbum": dataPlayer.trackAlbum,
"trackArtUrl": dataPlayer.trackArtUrl,
"length": dataPlayer.length || 0,
"position": dataPlayer.position || 0,
"playbackState": dataPlayer.playbackState,
"isPlaying": dataPlayer.isPlaying || false,
"canPlay": dataPlayer.canPlay || false,
"canPause": dataPlayer.canPause || false,
"canGoNext": dataPlayer.canGoNext || false,
"canGoPrevious": dataPlayer.canGoPrevious || false,
"canSeek": dataPlayer.canSeek || false,
"canControl": dataPlayer.canControl || false,
"_stateSource": dataPlayer,
"_controlTarget": identityPlayer
}
finalPlayers.push(virtualPlayer)
matchedGenericIndices[j] = true
wasMatched = true
break
}
}
}
if (!wasMatched) {
finalPlayers.push(specificPlayer)
}
}
// Add any generic players that were not matched
for (var i = 0; i < genericPlayers.length; i++) {
if (!matchedGenericIndices[i]) {
finalPlayers.push(genericPlayers[i])
}
}
// Filter for controllable players
let controllablePlayers = []
for (var i = 0; i < finalPlayers.length; i++) {
let player = finalPlayers[i]
if (player && player.canControl) {
controllablePlayers.push(player)
}
}
return controllablePlayers
}
function findActivePlayer() {
let availablePlayers = getAvailablePlayers()
if (availablePlayers.length === 0) {
//Logger.log("Media", "No active player found")
return null
}
// First, check if any player is currently playing
// Prioritize the actively playing player ---
for (var i = 0; i < availablePlayers.length; i++) {
const p = availablePlayers[i]
if (p.isPlaying && p.playbackState === MprisPlaybackState.Playing) {
if (availablePlayers[i] && availablePlayers[i].playbackState === MprisPlaybackState.Playing) {
Logger.log("Media", "Found actively playing player: " + availablePlayers[i].identity)
selectedPlayerIndex = i
return p
return availablePlayers[i]
}
}
// If no player is playing, use preferred player logic
// fallback if nothing is playing)
const preferred = (Settings.data.audio.preferredPlayer || "")
if (preferred !== "") {
for (var i = 0; i < availablePlayers.length; i++) {
const p = availablePlayers[i]
const identity = String(p.identity || "").toLowerCase()
const busName = String(p.busName || "").toLowerCase()
const desktop = String(p.desktopEntry || "").toLowerCase()
const pref = preferred.toLowerCase()
if (identity.includes(pref) || busName.includes(pref) || desktop.includes(pref)) {
if (identity.includes(pref)) {
selectedPlayerIndex = i
return p
}
}
}
// Fallback to selected index or first player
if (selectedPlayerIndex < availablePlayers.length) {
return availablePlayers[selectedPlayerIndex]
} else {
@@ -107,63 +170,64 @@ Singleton {
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer
currentPosition = currentPlayer ? currentPlayer.position : 0
Logger.log("Media", "Switching player")
}
}
function playPause() {
if (currentPlayer) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
let stateSource = currentPlayer._stateSource || currentPlayer
let controlTarget = currentPlayer._controlTarget || currentPlayer
if (stateSource.playbackState === MprisPlaybackState.Playing) {
controlTarget.pause()
} else {
currentPlayer.play()
controlTarget.play()
}
}
}
function play() {
if (currentPlayer && currentPlayer.canPlay) {
currentPlayer.play()
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canPlay) {
target.play()
}
}
function pause() {
if (currentPlayer && currentPlayer.canPause) {
currentPlayer.pause()
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canPause) {
target.pause()
}
}
function next() {
if (currentPlayer && currentPlayer.canGoNext) {
currentPlayer.next()
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canGoNext) {
target.next()
}
}
function previous() {
if (currentPlayer && currentPlayer.canGoPrevious) {
currentPlayer.previous()
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canGoPrevious) {
target.previous()
}
}
function seek(position) {
if (currentPlayer && currentPlayer.canSeek) {
currentPlayer.position = position
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canSeek) {
target.position = position
currentPosition = position
}
}
function seekRelative(offset) {
if (currentPlayer && currentPlayer.canSeek) {
var newPosition = currentPlayer.position + offset
currentPlayer.position = newPosition
currentPosition = newPosition
}
}
// 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
let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null
if (target && target.canSeek && target.length > 0) {
let seekPosition = ratio * target.length
target.position = seekPosition
currentPosition = seekPosition
}
}
@@ -205,15 +269,6 @@ Singleton {
}
}
// Update current player when available players change
Connections {
target: Mpris.players
function onValuesChanged() {
updateCurrentPlayer()
}
}
// Monitor playback state changes across all players to switch to playing ones
Timer {
id: playerStateMonitor
interval: 2000 // Check every 2 seconds
@@ -226,4 +281,13 @@ Singleton {
}
}
}
// Update current player when available players change
Connections {
target: Mpris.players
function onValuesChanged() {
Logger.log("Media", "Players changed")
updateCurrentPlayer()
}
}
}
+61 -1
View File
@@ -20,12 +20,14 @@ Item {
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// Initialization
function initialize() {
niriEventStream.running = true
updateWorkspaces()
updateWindows()
queryDisplayScales()
Logger.log("NiriService", "Initialized successfully")
}
@@ -39,6 +41,60 @@ Item {
niriWindowsProcess.running = true
}
// Query display scales
function queryDisplayScales() {
niriOutputsProcess.running = true
}
// Niri outputs process for display scale detection
Process {
id: niriOutputsProcess
running: false
command: ["niri", "msg", "--json", "outputs"]
stdout: SplitParser {
onRead: function (line) {
try {
const outputsData = JSON.parse(line)
const scales = {}
// Niri returns an object with display names as keys
for (const outputName in outputsData) {
const output = outputsData[outputName]
if (output && output.name) {
const logical = output.logical || {}
const currentModeIdx = output.current_mode || 0
const modes = output.modes || []
const currentMode = modes[currentModeIdx] || {}
scales[output.name] = {
"name": output.name,
"scale": logical.scale || 1.0,
"width": logical.width || 0,
"height": logical.height || 0,
"x": logical.x || 0,
"y": logical.y || 0,
"physical_width": (output.physical_size && output.physical_size[0]) || 0,
"physical_height": (output.physical_size && output.physical_size[1]) || 0,
"refresh_rate": currentMode.refresh_rate || 0,
"vrr_supported": output.vrr_supported || false,
"vrr_enabled": output.vrr_enabled || false,
"transform": logical.transform || "Normal"
}
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
Logger.error("NiriService", "Failed to parse outputs:", e, line)
}
}
}
}
// Niri workspace process
Process {
id: niriWorkspaceProcess
@@ -86,7 +142,7 @@ Item {
}
}
// Niri windows process (for initial load)
// Niri windows process
Process {
id: niriWindowsProcess
running: false
@@ -131,6 +187,10 @@ Item {
handleWindowLayoutsChanged(event.WindowLayoutsChanged)
} else if (event.OverviewOpenedOrClosed) {
handleOverviewOpenedOrClosed(event.OverviewOpenedOrClosed)
} else if (event.OutputsChanged) {
queryDisplayScales()
} else if (event.ConfigLoaded) {
queryDisplayScales()
}
} catch (e) {
Logger.error("NiriService", "Error parsing event stream:", e, data)
+6
View File
@@ -71,6 +71,12 @@ Singleton {
setProfile(PowerProfile.Balanced)
}
function isDefault() {
if (!available)
return true
return (profile === PowerProfile.Balanced)
}
Connections {
target: powerProfiles
function onProfileChanged() {
+72 -2
View File
@@ -16,13 +16,67 @@ Singleton {
property bool ghosttyAvailable: false
property bool footAvailable: false
property bool fuzzelAvailable: false
property bool vesktopAvailable: false
property bool gpuScreenRecorderAvailable: false
property bool wlsunsetAvailable: false
// Discord client auto-detection
property var availableDiscordClients: []
// Signal emitted when all checks are complete
signal checksCompleted
// Function to detect Discord client by checking config directories
function detectDiscordClient() {
// Build list of client names from MatugenTemplates
var clientNames = []
for (var i = 0; i < MatugenTemplates.discordClients.length; i++) {
clientNames.push(MatugenTemplates.discordClients[i].name)
}
// Use a Process to check directory existence for all clients
discordDetector.command = ["sh", "-c", "available_clients=\"\"; " + "for client in " + clientNames.join(" ") + "; do " + " if [ -d \"$HOME/.config/$client\" ]; then " + " available_clients=\"$available_clients $client\"; " + " fi; " + "done; " + "echo \"$available_clients\""]
discordDetector.running = true
}
// Process to detect Discord client directories
Process {
id: discordDetector
running: false
onExited: function (exitCode) {
availableDiscordClients = []
if (exitCode === 0) {
var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) {
return client.length > 0
})
if (detectedClients.length > 0) {
// Build list of available clients
for (var i = 0; i < detectedClients.length; i++) {
var clientName = detectedClients[i]
for (var j = 0; j < MatugenTemplates.discordClients.length; j++) {
var client = MatugenTemplates.discordClients[j]
if (client.name === clientName) {
availableDiscordClients.push(client)
break
}
}
}
Logger.log("ProgramChecker", "Detected Discord clients:", detectedClients.join(", "))
}
}
if (availableDiscordClients.length === 0) {
Logger.log("ProgramChecker", "No Discord clients detected")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Programs to check - maps property names to commands
readonly property var programsToCheck: ({
"matugenAvailable": ["which", "matugen"],
@@ -31,7 +85,6 @@ Singleton {
"ghosttyAvailable": ["which", "ghostty"],
"footAvailable": ["which", "foot"],
"fuzzelAvailable": ["which", "fuzzel"],
"vesktopAvailable": ["which", "vesktop"],
"gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"],
"wlsunsetAvailable": ["which", "wlsunset"]
})
@@ -59,6 +112,8 @@ Singleton {
// Check next program or emit completion signal
if (root.completedChecks >= root.totalChecks) {
// Run Discord client detection after all checks are complete
root.detectDiscordClient()
root.checksCompleted()
} else {
root.checkNextProgram()
@@ -113,6 +168,21 @@ Singleton {
checker.running = true
}
// Manual function to test Discord detection (for debugging)
function testDiscordDetection() {
Logger.log("ProgramChecker", "Testing Discord detection...")
Logger.log("ProgramChecker", "HOME:", Quickshell.env("HOME"))
// Test each client directory
for (var i = 0; i < MatugenTemplates.discordClients.length; i++) {
var client = MatugenTemplates.discordClients[i]
var configDir = client.configPath.replace("~", Quickshell.env("HOME"))
Logger.log("ProgramChecker", "Checking:", configDir)
}
detectDiscordClient()
}
// Initialize checks when service is created
Component.onCompleted: {
checkAllPrograms()
+68 -1
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.I3
import Quickshell.Wayland
import Quickshell.Io
import qs.Commons
Item {
@@ -16,6 +17,7 @@ Item {
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// I3-specific properties
property bool initialized: false
@@ -38,6 +40,7 @@ Item {
Qt.callLater(() => {
safeUpdateWorkspaces()
safeUpdateWindows()
queryDisplayScales()
})
initialized = true
Logger.log("SwayService", "Initialized successfully")
@@ -46,6 +49,66 @@ Item {
}
}
// Query display scales
function queryDisplayScales() {
swayOutputsProcess.running = true
}
// Sway outputs process for display scale detection
Process {
id: swayOutputsProcess
running: false
command: ["swaymsg", "-t", "get_outputs", "-r"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
swayOutputsProcess.accumulatedOutput += line
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.error("SwayService", "Failed to query outputs, exit code:", exitCode)
accumulatedOutput = ""
return
}
try {
const outputsData = JSON.parse(accumulatedOutput)
const scales = {}
for (const output of outputsData) {
if (output.name) {
scales[output.name] = {
"name": output.name,
"scale": output.scale || 1.0,
"width": output.current_mode ? output.current_mode.width : 0,
"height": output.current_mode ? output.current_mode.height : 0,
"refresh_rate": output.current_mode ? output.current_mode.refresh : 0,
"x": output.rect ? output.rect.x : 0,
"y": output.rect ? output.rect.y : 0,
"active": output.active || false,
"focused": output.focused || false,
"current_workspace": output.current_workspace || ""
}
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
Logger.error("SwayService", "Failed to parse outputs:", e)
} finally {
// Clear accumulated output for next query
accumulatedOutput = ""
}
}
}
// Safe update wrapper
function safeUpdate() {
safeUpdateWindows()
@@ -71,7 +134,7 @@ Item {
const wsData = {
"id": i,
"idx": ws.id,
"idx": ws.num,
"name": ws.name || "",
"output": (ws.monitor && ws.monitor.name) ? ws.monitor.name : "",
"isActive": ws.active === true,
@@ -197,6 +260,10 @@ Item {
safeUpdateWorkspaces()
workspaceChanged()
updateTimer.restart()
if (event.type === "output") {
Qt.callLater(queryDisplayScales)
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ Singleton {
id: root
// Public properties
property string baseVersion: "2.16.1"
property string baseVersion: "2.17.3"
property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`