mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
Merge branch 'main' into feat/calendar-events
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -71,6 +71,12 @@ Singleton {
|
||||
setProfile(PowerProfile.Balanced)
|
||||
}
|
||||
|
||||
function isDefault() {
|
||||
if (!available)
|
||||
return true
|
||||
return (profile === PowerProfile.Balanced)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: powerProfiles
|
||||
function onProfileChanged() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"}`
|
||||
|
||||
Reference in New Issue
Block a user