This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -0,0 +1,163 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.Compositor
/**
* Detects which screen the cursor is currently on by creating a temporary
* invisible PanelWindow. Use withCurrentScreen() to get the screen asynchronously.
*
* Usage:
* CurrentScreenDetector {
* id: screenDetector
* }
*
* function doSomething() {
* screenDetector.withCurrentScreen(function(screen) {
* // screen is the ShellScreen where cursor is
* })
* }
*/
Item {
id: root
// Pending callback to execute once screen is detected
property var pendingCallback: null
property bool pendingSkipBarCheck: false
// Detected screen
property var detectedScreen: null
// Signal emitted when screen is detected from the PanelWindow
signal screenDetected(var detectedScreen)
onScreenDetected: function (detectedScreen) {
root.detectedScreen = detectedScreen;
screenDetectorDebounce.restart();
}
/**
* Find a fallback screen that has a bar configured.
* Prioritizes the screen at position 0x0 (likely the primary screen).
*/
function findScreenWithBar(): var {
const monitors = Settings.data.bar.monitors || [];
let primaryCandidate = null;
let firstWithBar = null;
for (let i = 0; i < Quickshell.screens.length; i++) {
const s = Quickshell.screens[i];
const hasBar = monitors.length === 0 || monitors.includes(s.name);
if (hasBar) {
// Check if this is at 0x0 (primary position)
if (s.x === 0 && s.y === 0) {
primaryCandidate = s;
}
// Track first screen with bar as fallback
if (!firstWithBar) {
firstWithBar = s;
}
}
}
// Prefer primary (0x0), then first with bar, then just first screen
return primaryCandidate || firstWithBar || Quickshell.screens[0];
}
/**
* Execute callback with the screen where the cursor currently is.
* On single-monitor setups, executes immediately.
* On multi-monitor setups, briefly opens an invisible window to detect the screen.
*/
function withCurrentScreen(callback: var, skipBarCheck: bool) {
if (root.pendingCallback) {
Logger.w("CurrentScreenDetector", "Another detection is pending, ignoring new call");
return;
}
// Single monitor setup can execute immediately
if (Quickshell.screens.length === 1) {
callback(Quickshell.screens[0]);
return;
}
// Try compositor-specific focused monitor detection first
let screen = CompositorService.getFocusedScreen();
if (screen) {
// Apply the bar check if configured (skip for overlay launcher etc.)
if (!skipBarCheck && !Settings.data.general.allowPanelsOnScreenWithoutBar) {
const monitors = Settings.data.bar.monitors || [];
const hasBar = monitors.length === 0 || monitors.includes(screen.name);
if (!hasBar) {
screen = findScreenWithBar();
}
}
Logger.d("CurrentScreenDetector", "Using compositor-detected screen:", screen.name);
callback(screen);
return;
}
// Fallback: Multi-monitor setup needs async detection via invisible PanelWindow
root.detectedScreen = null;
root.pendingCallback = callback;
root.pendingSkipBarCheck = !!skipBarCheck;
screenDetectorLoader.active = true;
}
Timer {
id: screenDetectorDebounce
running: false
interval: 40
onTriggered: {
Logger.d("CurrentScreenDetector", "Screen debounced to:", root.detectedScreen?.name || "null");
// Execute pending callback if any
if (root.pendingCallback) {
if (!root.pendingSkipBarCheck && !Settings.data.general.allowPanelsOnScreenWithoutBar) {
// If we explicitly disabled panels on screen without bar, check if bar is configured
// for this screen, and fallback to primary screen if necessary
var monitors = Settings.data.bar.monitors || [];
const hasBar = monitors.length === 0 || monitors.includes(root.detectedScreen?.name);
if (!hasBar) {
root.detectedScreen = findScreenWithBar();
}
}
Logger.d("CurrentScreenDetector", "Executing callback on screen:", root.detectedScreen.name);
// Store callback locally and clear pendingCallback first to prevent deadlock
// if the callback throws an error
var callback = root.pendingCallback;
root.pendingCallback = null;
root.pendingSkipBarCheck = false;
try {
callback(root.detectedScreen);
} catch (e) {
Logger.e("CurrentScreenDetector", "Callback failed:", e);
}
}
// Clean up
screenDetectorLoader.active = false;
}
}
// Invisible dummy PanelWindow to detect which screen should receive the action
Loader {
id: screenDetectorLoader
active: false
sourceComponent: PanelWindow {
implicitWidth: 0
implicitHeight: 0
color: "transparent"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-screen-detector"
mask: Region {}
onScreenChanged: root.screenDetected(screen)
}
}
}
@@ -0,0 +1,318 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI
Singleton {
id: root
// Must be called at startup to force singleton instantiation,
// which registers the IpcHandler with the IPC system.
function init() {
Logger.i("CustomButtonIPCService", "Service started");
}
// Registry to store references to active custom buttons by their user-defined identifier
property var customButtonRegistry: ({})
// Register a custom button instance
function registerButton(button) {
if (!button || !button.ipcIdentifier) {
Logger.w("CustomButtonIPCService", "Cannot register button without ipcIdentifier");
return false;
}
customButtonRegistry[button.ipcIdentifier] = button;
Logger.d("CustomButtonIPCService", `Registered button with identifier: ${button.ipcIdentifier}`);
return true;
}
// Unregister a custom button instance
function unregisterButton(button) {
if (!button || !button.ipcIdentifier) {
return false;
}
if (customButtonRegistry[button.ipcIdentifier] === button) {
delete customButtonRegistry[button.ipcIdentifier];
Logger.d("CustomButtonIPCService", `Unregistered button with identifier: ${button.ipcIdentifier}`);
return true;
}
return false;
}
// Find a button by identifier
function findButton(identifier) {
return customButtonRegistry[identifier] || null;
}
// Find button config from Settings for when the live widget is not loaded
function findButtonConfig(identifier) {
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
var widgets = Settings.getBarWidgetsForScreen(screens[i].name);
var config = _searchWidgetsForIdentifier(widgets, identifier);
if (config)
return config;
}
// Also check global widgets as a final fallback
var globalConfig = _searchWidgetsForIdentifier(Settings.data.bar.widgets, identifier);
if (globalConfig)
return globalConfig;
return null;
}
function _searchWidgetsForIdentifier(widgets, identifier) {
var sections = ["left", "center", "right"];
for (var s = 0; s < sections.length; s++) {
var list = widgets[sections[s]];
if (!list)
continue;
for (var j = 0; j < list.length; j++) {
var w = list[j];
if (w.id === "CustomButton" && w.ipcIdentifier === identifier) {
return w;
}
}
}
return null;
}
// Resolve a command property from config with fallback to widgetMetadata defaults
function resolveCommand(config, prop) {
if (config[prop])
return config[prop];
var meta = BarWidgetRegistry.widgetMetadata["CustomButton"];
return meta ? (meta[prop] || "") : "";
}
// Substitute $delta expressions in a command string
function substituteWheelDelta(command, delta) {
var normalizedDelta = delta > 0 ? 1 : -1;
return command.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
if (operation) {
try {
var operator = operation.charAt(0);
var operand = parseInt(operation.substring(1));
var result;
switch (operator) {
case '+':
result = normalizedDelta + operand;
break;
case '-':
result = normalizedDelta - operand;
break;
case '*':
result = normalizedDelta * operand;
break;
case '/':
result = Math.floor(normalizedDelta / operand);
break;
default:
result = normalizedDelta;
}
return result.toString();
} catch (e) {
return normalizedDelta.toString();
}
} else {
return normalizedDelta.toString();
}
});
}
// IpcHandler for custom button commands using short alias 'cb'
IpcHandler {
target: "cb"
// Handle left click: cb left "identifier"
function left(identifier: string) {
const button = findButton(identifier);
if (button) {
if (button.leftClickExec || button.textCommand) {
button.clicked();
Logger.i("CustomButtonIPCService", `Triggered left click on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no left click action configured`);
}
return;
}
// Fallback: read from Settings
const config = findButtonConfig(identifier);
if (!config) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
const cmd = resolveCommand(config, "leftClickExec");
if (cmd) {
Quickshell.execDetached(["sh", "-lc", cmd]);
Logger.i("CustomButtonIPCService", `Triggered left click on button '${identifier}' (from settings)`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no left click action configured`);
}
}
// Handle right click: cb right "identifier"
function right(identifier: string) {
const button = findButton(identifier);
if (button) {
if (button.rightClickExec) {
button.rightClicked();
Logger.i("CustomButtonIPCService", `Triggered right click on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no right click action configured`);
}
return;
}
const config = findButtonConfig(identifier);
if (!config) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
const cmd = resolveCommand(config, "rightClickExec");
if (cmd) {
Quickshell.execDetached(["sh", "-lc", cmd]);
Logger.i("CustomButtonIPCService", `Triggered right click on button '${identifier}' (from settings)`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no right click action configured`);
}
}
// Handle middle click: cb middle "identifier"
function middle(identifier: string) {
const button = findButton(identifier);
if (button) {
if (button.middleClickExec) {
button.middleClicked();
Logger.i("CustomButtonIPCService", `Triggered middle click on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no middle click action configured`);
}
return;
}
const config = findButtonConfig(identifier);
if (!config) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
const cmd = resolveCommand(config, "middleClickExec");
if (cmd) {
Quickshell.execDetached(["sh", "-lc", cmd]);
Logger.i("CustomButtonIPCService", `Triggered middle click on button '${identifier}' (from settings)`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no middle click action configured`);
}
}
// Handle wheel up: cb up "identifier"
function up(identifier: string) {
const button = findButton(identifier);
if (button) {
if (button.wheelMode === "separate" && button.wheelUpExec) {
button.wheeled(1);
Logger.i("CustomButtonIPCService", `Triggered wheel up on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel up action configured or is not in separate mode`);
}
return;
}
const config = findButtonConfig(identifier);
if (!config) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
const mode = config.wheelMode || BarWidgetRegistry.widgetMetadata["CustomButton"].wheelMode;
const cmd = resolveCommand(config, "wheelUpExec");
if (mode === "separate" && cmd) {
const resolved = substituteWheelDelta(cmd, 1);
Quickshell.execDetached(["sh", "-lc", resolved]);
Logger.i("CustomButtonIPCService", `Triggered wheel up on button '${identifier}' (from settings)`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel up action configured or is not in separate mode`);
}
}
// Handle wheel down: cb down "identifier"
function down(identifier: string) {
const button = findButton(identifier);
if (button) {
if (button.wheelMode === "separate" && button.wheelDownExec) {
button.wheeled(-1);
Logger.i("CustomButtonIPCService", `Triggered wheel down on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel down action configured or is not in separate mode`);
}
return;
}
const config = findButtonConfig(identifier);
if (!config) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
const mode = config.wheelMode || BarWidgetRegistry.widgetMetadata["CustomButton"].wheelMode;
const cmd = resolveCommand(config, "wheelDownExec");
if (mode === "separate" && cmd) {
const resolved = substituteWheelDelta(cmd, -1);
Quickshell.execDetached(["sh", "-lc", resolved]);
Logger.i("CustomButtonIPCService", `Triggered wheel down on button '${identifier}' (from settings)`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel down action configured or is not in separate mode`);
}
}
// Handle wheel action: cb wheel "identifier"
function wheel(identifier: string) {
const button = findButton(identifier);
if (button) {
if (button.wheelMode === "unified" && button.wheelExec) {
button.wheeled(1);
Logger.i("CustomButtonIPCService", `Triggered wheel action on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no unified wheel action configured or is not in unified mode`);
}
return;
}
const config = findButtonConfig(identifier);
if (!config) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
const mode = config.wheelMode || BarWidgetRegistry.widgetMetadata["CustomButton"].wheelMode;
const cmd = resolveCommand(config, "wheelExec");
if (mode === "unified" && cmd) {
const resolved = substituteWheelDelta(cmd, 1);
Quickshell.execDetached(["sh", "-lc", resolved]);
Logger.i("CustomButtonIPCService", `Triggered wheel action on button '${identifier}' (from settings)`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no unified wheel action configured or is not in unified mode`);
}
}
// Handle refresh: cb refresh "identifier"
function refresh(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button '${identifier}' is not currently loaded refresh requires a live widget instance`);
return;
}
if (button.textCommand && button.textCommand.length > 0 && !button.textStream) {
button.runTextCommand();
Logger.i("CustomButtonIPCService", `Triggered refresh (text command) on button '${identifier}'`);
} else if (button.textStream) {
Logger.w("CustomButtonIPCService", `Button '${identifier}' uses streaming, manual refresh disabled`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no text command to refresh`);
}
}
}
}
@@ -0,0 +1,326 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Power
import qs.Services.Theming
import qs.Services.UI
Singleton {
id: root
// Hook connections for automatic script execution
Connections {
target: Settings.data.colorSchemes
function onDarkModeChanged() {
executeDarkModeHook(Settings.data.colorSchemes.darkMode);
}
}
// Pending wallpaper hook when waiting for color generation
property var pendingWallpaperHook: null
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
// Check if we need to wait for color generation
if (Settings.data.colorSchemes.useWallpaperColors) {
var effectiveMonitor = Settings.data.colorSchemes.monitorForColors;
if (effectiveMonitor === "" || effectiveMonitor === undefined) {
effectiveMonitor = Screen.name;
}
if (screenName === effectiveMonitor) {
// Store pending hook and wait for colors to be generated
root.pendingWallpaperHook = {
path: path,
screenName: screenName
};
return;
}
}
// No color generation, execute immediately
executeWallpaperHook(path, screenName);
}
}
Connections {
target: TemplateProcessor
function onColorsGenerated() {
// Execute pending wallpaper hook after colors are ready
if (root.pendingWallpaperHook) {
const hook = root.pendingWallpaperHook;
root.pendingWallpaperHook = null;
executeWallpaperHook(hook.path, hook.screenName);
}
executeColorGenerationHook();
}
}
// Track lock screen state for unlock hook
property bool wasLocked: false
Connections {
target: PanelService
function onLockScreenChanged() {
if (PanelService.lockScreen) {
lockScreenActiveConnection.target = PanelService.lockScreen;
}
}
}
Connections {
id: lockScreenActiveConnection
target: PanelService.lockScreen
function onActiveChanged() {
// Detect lock: was unlocked, now locked
if (!wasLocked && PanelService.lockScreen.active) {
executeLockHook();
}
// Detect unlock: was locked, now not locked
if (wasLocked && !PanelService.lockScreen.active) {
executeUnlockHook();
}
wasLocked = PanelService.lockScreen.active;
}
}
// Track performance mode state for hooks
property bool wasPerformanceModeEnabled: false
Connections {
target: PowerProfileService
function onNoctaliaPerformanceModeChanged() {
const isEnabled = PowerProfileService.noctaliaPerformanceMode;
// Detect enabled: was disabled, now enabled
if (!wasPerformanceModeEnabled && isEnabled) {
executePerformanceModeEnabledHook();
}
// Detect disabled: was enabled, now disabled
if (wasPerformanceModeEnabled && !isEnabled) {
executePerformanceModeDisabledHook();
}
wasPerformanceModeEnabled = isEnabled;
}
}
// Execute wallpaper change hook
function executeWallpaperHook(wallpaperPath, screenName) {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.wallpaperChange;
if (!script || script === "") {
return;
}
try {
const theme = Settings.data.colorSchemes.darkMode ? "dark" : "light";
let command = script.replace(/\$1/g, wallpaperPath);
command = command.replace(/\$2/g, screenName || "");
command = command.replace(/\$3/g, theme);
Quickshell.execDetached(["sh", "-lc", command]);
Logger.d("HooksService", `Executed wallpaper hook: ${command}`);
} catch (e) {
Logger.e("HooksService", `Failed to execute wallpaper hook: ${e}`);
}
}
// Execute dark mode change hook
function executeDarkModeHook(isDarkMode) {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.darkModeChange;
if (!script || script === "") {
return;
}
try {
const command = script.replace(/\$1/g, isDarkMode ? "true" : "false");
Quickshell.execDetached(["sh", "-lc", command]);
Logger.d("HooksService", `Executed dark mode hook: ${command}`);
} catch (e) {
Logger.e("HooksService", `Failed to execute dark mode hook: ${e}`);
}
}
// Execute screen lock hook
function executeLockHook() {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.screenLock;
if (!script || script === "") {
return;
}
try {
// Pass "lock" as $1 via shell arguments so the script receives it
Quickshell.execDetached(["sh", "-lc", script, "lock-hook", "lock"]);
Logger.d("HooksService", `Executed screen lock hook: ${script}`);
} catch (e) {
Logger.e("HooksService", `Failed to execute screen lock hook: ${e}`);
}
}
// Execute screen unlock hook
function executeUnlockHook() {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.screenUnlock;
if (!script || script === "") {
return;
}
try {
// Pass "unlock" as $1 via shell arguments so the script receives it
Quickshell.execDetached(["sh", "-lc", script, "unlock-hook", "unlock"]);
Logger.d("HooksService", `Executed screen unlock hook: ${script}`);
} catch (e) {
Logger.e("HooksService", `Failed to execute screen unlock hook: ${e}`);
}
}
// Execute performance mode enabled hook
function executePerformanceModeEnabledHook() {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.performanceModeEnabled;
if (!script || script === "") {
return;
}
try {
Quickshell.execDetached(["sh", "-lc", script]);
} catch (e) {
Logger.e("HooksService", `Failed to execute performance mode enabled hook: ${e}`);
}
}
// Execute performance mode disabled hook
function executePerformanceModeDisabledHook() {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.performanceModeDisabled;
if (!script || script === "") {
return;
}
try {
Quickshell.execDetached(["sh", "-lc", script]);
} catch (e) {
Logger.e("HooksService", `Failed to execute performance mode disabled hook: ${e}`);
}
}
// Execute color generation hook
function executeColorGenerationHook() {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.colorGeneration;
if (!script || script === "") {
return;
}
try {
const theme = Settings.data.colorSchemes.darkMode ? "dark" : "light";
const command = script.replace(/\$1/g, theme);
Quickshell.execDetached(["sh", "-lc", command]);
Logger.d("HooksService", `Executed color generation hook: ${command}`);
} catch (e) {
Logger.e("HooksService", `Failed to execute color generation hook: ${e}`);
}
}
// Blocking power hook infrastructure
property var pendingPowerCallback: null
Process {
id: powerHookProcess
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
Logger.w("HooksService", `Power hook failed with exit code ${exitCode}`);
}
if (pendingPowerCallback !== null) {
const callback = pendingPowerCallback;
pendingPowerCallback = null;
callback();
}
}
}
function runPowerHook(script, callback) {
pendingPowerCallback = callback;
powerHookProcess.command = ["sh", "-lc", script];
powerHookProcess.running = true;
}
function executeSessionHook(action, callback) {
if (!Settings.data.hooks?.enabled) {
callback();
return;
}
const script = Settings.data.hooks?.session;
if (!script) {
callback();
return;
}
Logger.i("HooksService", `Executing session hook for ${action}`);
runPowerHook(`${script} ${action}`, callback);
}
// Execute startup hook
function executeStartupHook() {
if (!Settings.data.hooks?.enabled) {
return;
}
const script = Settings.data.hooks?.startup;
if (!script || script === "") {
return;
}
try {
Quickshell.execDetached(["sh", "-lc", script]);
Logger.d("HooksService", `Executed startup hook: ${script}`);
} catch (e) {
Logger.e("HooksService", `Failed to execute startup hook: ${e}`);
}
}
// Initialize the service
function init() {
Logger.i("HooksService", "Service started");
// Initialize lock screen state tracking
Qt.callLater(() => {
if (PanelService.lockScreen) {
wasLocked = PanelService.lockScreen.active;
lockScreenActiveConnection.target = PanelService.lockScreen;
}
// Initialize performance mode state tracking
wasPerformanceModeEnabled = PowerProfileService.noctaliaPerformanceMode;
// Execute startup hook
executeStartupHook();
});
}
}
@@ -0,0 +1,940 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Modules.Panels.Settings
import qs.Services.Compositor
import qs.Services.Hardware
import qs.Services.Location
import qs.Services.Media
import qs.Services.Networking
import qs.Services.Noctalia
import qs.Services.Power
import qs.Services.System
import qs.Services.Theming
import qs.Services.UI
Singleton {
id: root
// Screen detector, set via init()
property var screenDetector: null
function init(detector) {
root.screenDetector = detector;
Logger.i("IPCService", "Service started");
}
// Helper for index-based notification lookups in IPC
function _getNotificationByIndex(index: string, funcName: string) {
var idx = index === "" ? 0 : parseInt(index);
if (isNaN(idx)) {
Logger.w("IPC", "Argument to ipc call '" + funcName + "' must be a number");
return null;
}
if (idx < 0 || idx >= NotificationService.popupModel.count) {
Logger.w("IPC", "Notification index out of range: " + idx);
return null;
}
return NotificationService.popupModel.get(idx);
}
IpcHandler {
target: "bar"
function toggle() {
BarService.toggleVisibility();
}
function hideBar() {
BarService.hide();
}
function showBar() {
BarService.show();
}
function peek() {
BarService.peek();
}
function setDisplayMode(mode: string, screen: string) {
if (mode === "always_visible" || mode === "non_exclusive" || mode === "auto_hide") {
if (!screen || screen === "all") {
Settings.data.bar.displayMode = mode;
} else {
Settings.setScreenOverride(screen, "displayMode", mode);
}
}
}
function setPosition(position: string, screen: string) {
var valid = position === "top" || position === "bottom" || position === "left" || position === "right";
if (!valid) {
Logger.w("IPC", "Invalid bar position: " + position + ". Valid: top, bottom, left, right");
return;
}
if (!screen || screen === "all") {
Settings.data.bar.position = position;
} else {
Settings.setScreenOverride(screen, "position", position);
}
}
}
// Settings IPC helpers (outside IpcHandler to avoid QVariant IPC warnings)
readonly property var _settingsTabMap: ({
"about": SettingsPanel.Tab.About,
"audio": SettingsPanel.Tab.Audio,
"bar": SettingsPanel.Tab.Bar,
"colorscheme": SettingsPanel.Tab.ColorScheme,
"lockscreen": SettingsPanel.Tab.LockScreen,
"controlcenter": SettingsPanel.Tab.ControlCenter,
"desktopwidgets": SettingsPanel.Tab.DesktopWidgets,
"osd": SettingsPanel.Tab.OSD,
"display": SettingsPanel.Tab.Display,
"dock": SettingsPanel.Tab.Dock,
"general": SettingsPanel.Tab.General,
"hooks": SettingsPanel.Tab.Hooks,
"launcher": SettingsPanel.Tab.Launcher,
"location": SettingsPanel.Tab.Location,
"connections": SettingsPanel.Tab.Connections,
"notifications": SettingsPanel.Tab.Notifications,
"plugins": SettingsPanel.Tab.Plugins,
"sessionmenu": SettingsPanel.Tab.SessionMenu,
"system": SettingsPanel.Tab.System,
"systemmonitor": SettingsPanel.Tab.System,
"userinterface": SettingsPanel.Tab.UserInterface,
"wallpaper": SettingsPanel.Tab.Wallpaper
})
function _parseSettingsTabArg(tabArg) {
var parts = tabArg.split("/");
var tabId = _resolveSettingsTab(parts[0]);
var subTabId = parts.length > 1 ? parseInt(parts[1]) : -1;
return {
"tab": tabId,
"subTab": isNaN(subTabId) ? -1 : subTabId
};
}
function _resolveSettingsTab(tabName) {
if (!tabName)
return SettingsPanel.Tab.General;
var key = tabName.toLowerCase().replace(/[-_]/g, "");
if (key in _settingsTabMap)
return _settingsTabMap[key];
Logger.w("IPC", "Unknown settings tab: " + tabName);
return SettingsPanel.Tab.General;
}
function _settingsToggle(tabId, subTabId) {
root.screenDetector.withCurrentScreen(screen => {
SettingsPanelService.toggle(tabId, subTabId, screen);
});
}
function _settingsOpen(tabId, subTabId) {
root.screenDetector.withCurrentScreen(screen => {
SettingsPanelService.openToTab(tabId, subTabId, screen);
});
}
IpcHandler {
target: "settings"
function toggle() {
root._settingsToggle(SettingsPanel.Tab.General, -1);
}
function toggleTab(tab: string) {
var parsed = root._parseSettingsTabArg(tab);
root._settingsToggle(parsed.tab, parsed.subTab);
}
function open() {
root._settingsOpen(SettingsPanel.Tab.General, -1);
}
function openTab(tab: string) {
var parsed = root._parseSettingsTabArg(tab);
root._settingsOpen(parsed.tab, parsed.subTab);
}
}
IpcHandler {
target: "calendar"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var clockPanel = PanelService.getPanel("clockPanel", screen);
clockPanel?.toggle(null, "Clock");
});
}
}
IpcHandler {
target: "notifications"
function toggleHistory() {
// Will attempt to open the panel next to the bar button if any.
root.screenDetector.withCurrentScreen(screen => {
var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen);
notificationHistoryPanel.toggle(null, "NotificationHistory");
});
}
function toggleDND() {
NotificationService.doNotDisturb = !NotificationService.doNotDisturb;
}
function enableDND() {
NotificationService.doNotDisturb = true;
}
function disableDND() {
NotificationService.doNotDisturb = false;
}
function clear() {
NotificationService.clearHistory();
}
function dismissOldest() {
NotificationService.dismissOldestPopup();
}
function removeOldestHistory() {
NotificationService.removeOldestHistory();
}
function dismissAll() {
NotificationService.dismissAllPopups();
}
function getHistory(): string {
return JSON.stringify(NotificationService.getHistorySnapshot(), null, 2);
}
function removeFromHistory(id: string): bool {
return NotificationService.removeFromHistory(id);
}
function invokeDefault(index: string): bool {
var notif = root._getNotificationByIndex(index, "notifications invokeDefault");
if (!notif)
return false;
var actions = JSON.parse(notif.actionsJson || "[]");
if (actions.length === 0)
return false;
var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier;
return NotificationService.invokeAction(notif.id, actionId);
}
function invokeDefaultAndDismiss(index: string): bool {
var notif = root._getNotificationByIndex(index, "notifications invokeDefaultAndDismiss");
if (!notif)
return false;
var actions = JSON.parse(notif.actionsJson || "[]");
if (actions.length === 0) {
NotificationService.dismissPopup(notif.id);
return false;
}
var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier;
var result = NotificationService.invokeAction(notif.id, actionId);
NotificationService.dismissPopup(notif.id);
return result;
}
function invokeAction(id: string, actionId: string): bool {
if (!id || !actionId) {
Logger.w("IPC", "Both 'id' and 'actionId' are required for 'notifications invokeAction'");
return false;
}
return NotificationService.invokeAction(id, actionId);
}
function getActions(index: string): string {
var notif = root._getNotificationByIndex(index, "notifications getActions");
if (!notif)
return "[]";
return notif.actionsJson || "[]";
}
}
IpcHandler {
target: "toast"
function send(json: string) {
try {
var data = JSON.parse(json);
var title = data.title || "";
var body = data.body || "";
var icon = data.icon || "";
var type = data.type || "notice";
var duration = data.duration;
switch (type) {
case "warning":
ToastService.showWarning(title, body, duration ?? 4000);
break;
case "error":
ToastService.showError(title, body, duration ?? 6000);
break;
default:
ToastService.showNotice(title, body, icon, duration ?? 3000);
}
} catch (error) {
Logger.e("IPC", "Failed to parse toast JSON: " + error);
}
}
function dismiss() {
ToastService.dismissToast();
}
}
// Idle Inhibitor / Keep Awake
IpcHandler {
target: "idleInhibitor"
function toggle() {
IdleInhibitorService.manualToggle();
}
function enable() {
IdleInhibitorService.addManualInhibitor(null);
}
function disable() {
IdleInhibitorService.removeManualInhibitor();
}
function enableFor(seconds: string) {
var secs = parseInt(seconds);
if (isNaN(secs) || secs <= 0) {
Logger.w("IPC", "Argument to 'idleInhibitor enableFor' must be a positive number");
return;
}
IdleInhibitorService.addManualInhibitor(secs);
}
}
IpcHandler {
target: "launcher"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var searchText = PanelService.getLauncherSearchText(screen);
var isInAppMode = !searchText.startsWith(">");
if (!PanelService.isLauncherOpen(screen)) {
// Closed -> open in app mode
PanelService.openLauncherWithSearch(screen, "");
} else if (isInAppMode) {
// Already in app mode -> close
PanelService.closeLauncher(screen);
} else {
// In another mode -> switch to app mode
PanelService.setLauncherSearchText(screen, "");
}
}, Settings.data.appLauncher.overviewLayer);
}
function clipboard() {
root.screenDetector.withCurrentScreen(screen => {
var searchText = PanelService.getLauncherSearchText(screen);
var isInClipMode = searchText.startsWith(">clip");
if (!PanelService.isLauncherOpen(screen)) {
// Closed -> open in clipboard mode
PanelService.openLauncherWithSearch(screen, ">clip ");
} else if (isInClipMode) {
// Already in clipboard mode -> close
PanelService.closeLauncher(screen);
} else {
// In another mode -> switch to clipboard mode
PanelService.setLauncherSearchText(screen, ">clip ");
}
}, Settings.data.appLauncher.overviewLayer);
}
function command() {
root.screenDetector.withCurrentScreen(screen => {
var searchText = PanelService.getLauncherSearchText(screen);
var isInCmdMode = searchText.startsWith(">cmd");
if (!PanelService.isLauncherOpen(screen)) {
PanelService.openLauncherWithSearch(screen, ">cmd ");
} else if (isInCmdMode) {
PanelService.closeLauncher(screen);
} else {
PanelService.setLauncherSearchText(screen, ">cmd ");
}
}, Settings.data.appLauncher.overviewLayer);
}
function emoji() {
root.screenDetector.withCurrentScreen(screen => {
var searchText = PanelService.getLauncherSearchText(screen);
var isInEmojiMode = searchText.startsWith(">emoji");
if (!PanelService.isLauncherOpen(screen)) {
// Closed -> open in emoji mode
PanelService.openLauncherWithSearch(screen, ">emoji ");
} else if (isInEmojiMode) {
// Already in emoji mode -> close
PanelService.closeLauncher(screen);
} else {
// In another mode -> switch to emoji mode
PanelService.setLauncherSearchText(screen, ">emoji ");
}
}, Settings.data.appLauncher.overviewLayer);
}
function windows() {
root.screenDetector.withCurrentScreen(screen => {
var searchText = PanelService.getLauncherSearchText(screen);
var isInWindowsMode = searchText.startsWith(">win");
if (!PanelService.isLauncherOpen(screen)) {
PanelService.openLauncherWithSearch(screen, ">win ");
} else if (isInWindowsMode) {
PanelService.closeLauncher(screen);
} else {
PanelService.setLauncherSearchText(screen, ">win ");
}
}, Settings.data.appLauncher.overviewLayer);
}
function settings() {
root.screenDetector.withCurrentScreen(screen => {
var searchText = PanelService.getLauncherSearchText(screen);
var isInSettingsMode = searchText.startsWith(">settings");
if (!PanelService.isLauncherOpen(screen)) {
PanelService.openLauncherWithSearch(screen, ">settings ");
} else if (isInSettingsMode) {
PanelService.closeLauncher(screen);
} else {
PanelService.setLauncherSearchText(screen, ">settings ");
}
}, Settings.data.appLauncher.overviewLayer);
}
}
IpcHandler {
target: "lockScreen"
// New preferred method - lock the screen
function lock() {
// Only lock if not already locked (prevents the red screen issue)
if (!PanelService.lockScreen.active) {
CompositorService.lock();
}
}
}
IpcHandler {
target: "brightness"
function increase() {
BrightnessService.increaseBrightness();
}
function decrease() {
BrightnessService.decreaseBrightness();
}
function set(value: string) {
var val = parseFloat(value);
if (isNaN(val))
return;
// Normalize logic: heuristic handling of 0-100 vs 0-1
if (val > 1.0)
val = val / 100.0;
// Clamp
val = Math.max(0.0, Math.min(1.0, val));
BrightnessService.setBrightness(val);
}
}
IpcHandler {
target: "monitors"
function on() {
CompositorService.turnOnMonitors();
}
function off() {
CompositorService.turnOffMonitors();
}
}
IpcHandler {
target: "darkMode"
function toggle() {
Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode;
}
function setDark() {
Settings.data.colorSchemes.darkMode = true;
}
function setLight() {
Settings.data.colorSchemes.darkMode = false;
}
}
IpcHandler {
target: "nightLight"
function toggle() {
if (!ProgramCheckerService.wlsunsetAvailable) {
Logger.w("IPC", "wlsunset not available, cannot toggle night light");
return;
}
if (Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = false;
} else {
if (Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = false;
} else {
Settings.data.nightLight.forced = true;
Settings.data.nightLight.enabled = true;
}
}
}
}
IpcHandler {
target: "colorScheme"
function set(schemeName: string) {
ColorSchemeService.setPredefinedScheme(schemeName);
}
function setGenerationMethod(method: string) {
var valid = false;
for (var i = 0; i < TemplateProcessor.schemeTypes.length; i++) {
if (TemplateProcessor.schemeTypes[i].key === method) {
valid = true;
break;
}
}
if (valid) {
Settings.data.colorSchemes.generationMethod = method;
} else {
Logger.w("IPC", "Invalid generation method received: " + method);
}
}
}
IpcHandler {
target: "volume"
function increase() {
AudioService.increaseVolume();
}
function decrease() {
AudioService.decreaseVolume();
}
function muteOutput() {
AudioService.setOutputMuted(!AudioService.muted);
}
function increaseInput() {
AudioService.increaseInputVolume();
}
function decreaseInput() {
AudioService.decreaseInputVolume();
}
function muteInput() {
AudioService.setInputMuted(!AudioService.inputMuted);
}
function togglePanel() {
root.screenDetector.withCurrentScreen(screen => {
var panel = PanelService.getPanel("audioPanel", screen);
panel?.toggle(null, "Volume");
});
}
function openPanel() {
root.screenDetector.withCurrentScreen(screen => {
var panel = PanelService.getPanel("audioPanel", screen);
panel?.open(null, "Volume");
});
}
function closePanel() {
root.screenDetector.withCurrentScreen(screen => {
var panel = PanelService.getPanel("audioPanel", screen);
panel?.close(null, "Volume");
});
}
}
IpcHandler {
target: "sessionMenu"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen);
// Session Menu is never open near the bar
sessionMenuPanel?.toggle();
});
}
function lock() {
if (!PanelService.lockScreen.active) {
CompositorService.lock();
}
}
function lockAndSuspend() {
// Only lock and suspend if not already locked
if (!PanelService.lockScreen.active) {
CompositorService.lockAndSuspend();
}
}
}
IpcHandler {
target: "controlCenter"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
if (Settings.data.controlCenter.position === "close_to_bar_button") {
// Will attempt to open the panel next to the bar button if any.
controlCenterPanel?.toggle(null, "ControlCenter");
} else {
controlCenterPanel?.toggle();
}
});
}
}
IpcHandler {
target: "dock"
function toggle() {
Settings.data.dock.enabled = !Settings.data.dock.enabled;
}
}
// Wallpaper IPC: trigger a new random wallpaper
IpcHandler {
target: "wallpaper"
function toggle() {
if (Settings.data.wallpaper.enabled) {
root.screenDetector.withCurrentScreen(screen => {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen);
wallpaperPanel?.toggle();
});
}
}
function random(screen: string) {
if (Settings.data.wallpaper.enabled) {
if (!screen || screen === "all" || screen.trim().length === 0) {
screen = undefined;
}
WallpaperService.setRandomWallpaper(screen);
}
}
function get(screen: string): string {
if (screen === "all" || screen === "") {
if (Quickshell.screens.length > 1) {
return JSON.stringify(WallpaperService.getWallpapersEffectiveMap());
}
return WallpaperService.getWallpaper(Quickshell.screens[0].name) ?? "";
} else {
var found = Quickshell.screens.find(s => s.name === screen);
if (!found) {
Logger.w("IPC", "wallpaper get: unknown screen: " + screen);
return "";
}
return WallpaperService.getWallpaper(screen) ?? "";
}
}
function set(path: string, screen: string) {
if (screen === "all" || screen === "") {
screen = undefined;
}
WallpaperService.changeWallpaper(path, screen);
}
function refresh() {
WallpaperService.refreshWallpapersList();
}
function toggleAutomation() {
Settings.data.wallpaper.automationEnabled = !Settings.data.wallpaper.automationEnabled;
}
function disableAutomation() {
Settings.data.wallpaper.automationEnabled = false;
}
function enableAutomation() {
Settings.data.wallpaper.automationEnabled = true;
}
}
IpcHandler {
target: "wifi"
function toggle() {
NetworkService.setWifiEnabled(!NetworkService.wifiEnabled);
}
function enable() {
NetworkService.setWifiEnabled(true);
}
function disable() {
NetworkService.setWifiEnabled(false);
}
}
IpcHandler {
target: "network"
function togglePanel() {
root.screenDetector.withCurrentScreen(screen => {
var networkPanel = PanelService.getPanel("networkPanel", screen);
networkPanel?.toggle(null, "Network");
});
}
}
IpcHandler {
target: "bluetooth"
function toggle() {
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled);
}
function enable() {
BluetoothService.setBluetoothEnabled(true);
}
function disable() {
BluetoothService.setBluetoothEnabled(false);
}
function togglePanel() {
root.screenDetector.withCurrentScreen(screen => {
var bluetoothPanel = PanelService.getPanel("bluetoothPanel", screen);
bluetoothPanel?.toggle(null, "Bluetooth");
});
}
function toggleAutoConnect() {
Settings.data.network.bluetoothAutoConnect = !Settings.data.network.bluetoothAutoConnect;
}
function enableAutoConnect() {
Settings.data.network.bluetoothAutoConnect = true;
}
function disableAutoConnect() {
Settings.data.network.bluetoothAutoConnect = false;
}
}
IpcHandler {
target: "airplaneMode"
function toggle() {
NetworkService.setAirplaneMode(!NetworkService.airplaneModeEnabled);
}
function enable() {
NetworkService.setAirplaneMode(true);
}
function disable() {
NetworkService.setAirplaneMode(false);
}
}
IpcHandler {
target: "battery"
function togglePanel() {
root.screenDetector.withCurrentScreen(screen => {
var batteryPanel = PanelService.getPanel("batteryPanel", screen);
batteryPanel?.toggle(null, "Battery");
});
}
}
IpcHandler {
target: "powerProfile"
function cycle() {
PowerProfileService.cycleProfile();
}
function cycleReverse() {
PowerProfileService.cycleProfileReverse();
}
function set(mode: string) {
switch (mode) {
case "performance":
PowerProfileService.setProfile(2);
break;
case "balanced":
PowerProfileService.setProfile(1);
break;
case "powersaver":
PowerProfileService.setProfile(0);
break;
}
}
function toggleNoctaliaPerformance() {
PowerProfileService.toggleNoctaliaPerformance();
}
function enableNoctaliaPerformance() {
PowerProfileService.setNoctaliaPerformance(true);
}
function disableNoctaliaPerformance() {
PowerProfileService.setNoctaliaPerformance(false);
}
}
IpcHandler {
target: "media"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var panel = PanelService.getPanel("mediaPlayerPanel", screen);
panel?.toggle(null, "MediaMini");
});
}
function playPause() {
MediaService.playPause();
}
function play() {
MediaService.play();
}
function stop() {
MediaService.stop();
}
function pause() {
MediaService.pause();
}
function next() {
MediaService.next();
}
function previous() {
MediaService.previous();
}
function seekRelative(offset: string) {
var offsetVal = parseFloat(offset);
if (Number.isNaN(offsetVal)) {
Logger.w("Media", "Argument to ipc call 'media seekRelative' must be a number");
return;
}
MediaService.seekRelative(offsetVal);
}
function seekByRatio(position: string) {
var positionVal = parseFloat(position);
if (Number.isNaN(positionVal)) {
Logger.w("Media", "Argument to ipc call 'media seekByRatio' must be a number");
return;
}
MediaService.seekByRatio(positionVal);
}
}
IpcHandler {
target: "state"
// Returns all settings and shell state as JSON
function all(): string {
try {
var snapshot = ShellState.buildStateSnapshot();
if (!snapshot) {
throw new Error("State snapshot unavailable");
}
return JSON.stringify(snapshot, null, 2);
} catch (error) {
Logger.e("IPC", "Failed to serialize state:", error);
return JSON.stringify({
"error": "Failed to serialize state: " + error
}, null, 2);
}
}
}
IpcHandler {
target: "desktopWidgets"
function toggle() {
Settings.data.desktopWidgets.enabled = !Settings.data.desktopWidgets.enabled;
}
function disable() {
Settings.data.desktopWidgets.enabled = false;
}
function enable() {
Settings.data.desktopWidgets.enabled = true;
}
function edit() {
DesktopWidgetRegistry.editMode = !DesktopWidgetRegistry.editMode;
}
}
IpcHandler {
target: "location"
function get(): string {
return Settings.data.location.name;
}
function set(name: string) {
Settings.data.location.name = name;
LocationService.update();
}
}
IpcHandler {
target: "systemMonitor"
function toggle() {
root.screenDetector.withCurrentScreen(screen => {
var panel = PanelService.getPanel("systemStatsPanel", screen);
panel?.toggle(null, "SystemMonitor");
});
}
}
IpcHandler {
target: "plugin"
function openSettings(key: string) {
var manifest = PluginRegistry.getPluginManifest(key);
if (!manifest) {
Logger.w("IPC", "Plugin not found:", key);
return;
}
if (!manifest.entryPoints?.settings) {
Logger.w("IPC", "Plugin has no settings entry point:", key);
return;
}
root.screenDetector.withCurrentScreen(screen => {
BarService.openPluginSettings(screen, manifest);
});
}
function openPanel(key: string) {
var manifest = PluginRegistry.getPluginManifest(key);
if (!manifest) {
Logger.w("IPC", "Plugin not found:", key);
return;
}
if (!manifest.entryPoints?.panel) {
Logger.w("IPC", "Plugin has no panel entry point:", key);
return;
}
root.screenDetector.withCurrentScreen(screen => {
PluginService.openPluginPanel(key, screen, null);
});
}
function closePanel(key: string) {
var manifest = PluginRegistry.getPluginManifest(key);
if (!manifest) {
Logger.w("IPC", "Plugin not found:", key);
return;
}
if (!manifest.entryPoints?.panel) {
Logger.w("IPC", "Plugin has no panel entry point:", key);
return;
}
root.screenDetector.withCurrentScreen(screen => {
var api = PluginService.getPluginAPI(key);
if (api) {
api.closePanel(screen);
}
});
}
function togglePanel(key: string) {
var manifest = PluginRegistry.getPluginManifest(key);
if (!manifest) {
Logger.w("IPC", "Plugin not found:", key);
return;
}
if (!manifest.entryPoints?.panel) {
Logger.w("IPC", "Plugin has no panel entry point:", key);
return;
}
root.screenDetector.withCurrentScreen(screen => {
PluginService.togglePluginPanel(key, screen, null);
});
}
}
}