Workspaces now show up on their appropriate monitors. (#681)

* Workspaces now show up on their appropriate monitors.

* Fixed undefined rules showing up.
This commit is contained in:
Jas Singh
2024-12-31 01:33:40 -08:00
committed by GitHub
parent d2e02f553a
commit 5f72b4f5e1
17 changed files with 576 additions and 425 deletions

View File

@@ -315,7 +315,6 @@ in
bar.workspaces.applicationIconEmptyWorkspace = mkStrOption ""; bar.workspaces.applicationIconEmptyWorkspace = mkStrOption "";
bar.workspaces.applicationIconFallback = mkStrOption "󰣆"; bar.workspaces.applicationIconFallback = mkStrOption "󰣆";
bar.workspaces.applicationIconOncePerWorkspace = mkBoolOption true; bar.workspaces.applicationIconOncePerWorkspace = mkBoolOption true;
bar.workspaces.hideUnoccupied = mkBoolOption true;
bar.workspaces.icons.active = mkStrOption ""; bar.workspaces.icons.active = mkStrOption "";
bar.workspaces.icons.available = mkStrOption ""; bar.workspaces.icons.available = mkStrOption "";
bar.workspaces.icons.occupied = mkStrOption ""; bar.workspaces.icons.occupied = mkStrOption "";

View File

@@ -13,13 +13,13 @@ import { layoutMap } from './layouts';
* This function parses the provided JSON string to extract the keyboard layout information. * This function parses the provided JSON string to extract the keyboard layout information.
* It returns the layout in the specified format, either as a code or a human-readable string. * It returns the layout in the specified format, either as a code or a human-readable string.
* *
* @param obj The JSON string containing the keyboard layout information. * @param layoutData The JSON string containing the keyboard layout information.
* @param format The format in which to return the layout, either 'code' or 'label'. * @param format The format in which to return the layout, either 'code' or 'label'.
* *
* @returns The keyboard layout in the specified format. If no keyboards are found, returns 'Unknown' or 'Unknown Layout'. * @returns The keyboard layout in the specified format. If no keyboards are found, returns 'Unknown' or 'Unknown Layout'.
*/ */
export const getKeyboardLayout = (obj: string, format: KbLabelType): LayoutKeys | LayoutValues => { export const getKeyboardLayout = (layoutData: string, format: KbLabelType): LayoutKeys | LayoutValues => {
const hyprctlDevices: HyprctlDeviceLayout = JSON.parse(obj); const hyprctlDevices: HyprctlDeviceLayout = JSON.parse(layoutData);
const keyboards = hyprctlDevices['keyboards']; const keyboards = hyprctlDevices['keyboards'];
if (keyboards.length === 0) { if (keyboards.length === 0) {

View File

@@ -4,13 +4,22 @@ import { Module } from '../../shared/Module';
import { inputHandler } from 'src/components/bar/utils/helpers'; import { inputHandler } from 'src/components/bar/utils/helpers';
import { getKeyboardLayout } from './helpers'; import { getKeyboardLayout } from './helpers';
import { BarBoxChild } from 'src/lib/types/bar'; import { BarBoxChild } from 'src/lib/types/bar';
import { bind, execAsync } from 'astal'; import { bind } from 'astal';
import { useHook } from 'src/lib/shared/hookHandler'; import { useHook } from 'src/lib/shared/hookHandler';
import { Astal } from 'astal/gtk3'; import { Astal } from 'astal/gtk3';
const { label, labelType, icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } = const { label, labelType, icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } =
options.bar.customModules.kbLayout; options.bar.customModules.kbLayout;
function setLabel(self: Astal.Label): void {
try {
const devices = hyprlandService.message('j/devices');
self.label = getKeyboardLayout(devices, labelType.get());
} catch (error) {
console.error(error);
}
}
export const KbInput = (): BarBoxChild => { export const KbInput = (): BarBoxChild => {
const keyboardModule = Module({ const keyboardModule = Module({
textIcon: bind(icon), textIcon: bind(icon),
@@ -20,25 +29,13 @@ export const KbInput = (): BarBoxChild => {
self, self,
hyprlandService, hyprlandService,
() => { () => {
execAsync('hyprctl devices -j') setLabel(self);
.then((obj) => {
self.label = getKeyboardLayout(obj, labelType.get());
})
.catch((err) => {
console.error(err);
});
}, },
'keyboard-layout', 'keyboard-layout',
); );
useHook(self, labelType, () => { useHook(self, labelType, () => {
execAsync('hyprctl devices -j') setLabel(self);
.then((obj) => {
self.label = getKeyboardLayout(obj, labelType.get());
})
.catch((err) => {
console.error(err);
});
}); });
}, },
boxClass: 'kblayout', boxClass: 'kblayout',

View File

@@ -1,350 +1,384 @@
import { exec, Variable } from 'astal'; import { Variable } from 'astal';
import AstalHyprland from 'gi://AstalHyprland?version=0.1'; import AstalHyprland from 'gi://AstalHyprland?version=0.1';
import { hyprlandService } from 'src/lib/constants/services'; import { hyprlandService } from 'src/lib/constants/services';
import { MonitorMap, WorkspaceMap, WorkspaceRule } from 'src/lib/types/workspace'; import { MonitorMap, WorkspaceMonitorMap, WorkspaceRule } from 'src/lib/types/workspace';
import { range } from 'src/lib/utils'; import { range } from 'src/lib/utils';
import options from 'src/options'; import options from 'src/options';
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces; const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
/** /**
* Retrieves the workspaces for a specific monitor. * A Variable that holds the current map of monitors to the workspace numbers assigned to them.
*
* This function checks if a given workspace is valid for a specified monitor based on the workspace rules.
*
* @param curWs - The current workspace number.
* @param wsRules - The workspace rules map.
* @param monitor - The monitor ID.
* @param workspaceList - The list of workspaces.
* @param monitorList - The list of monitors.
*
* @returns Whether the workspace is valid for the monitor.
*/ */
export const getWorkspacesForMonitor = ( export const workspaceRules = Variable(getWorkspaceMonitorMap());
curWs: number,
wsRules: WorkspaceMap, /**
monitor: number, * A Variable used to force UI or other updates when relevant workspace events occur.
*/
export const forceUpdater = Variable(true);
/**
* Retrieves the workspace numbers associated with a specific monitor.
*
* If only one monitor exists, this will simply return a list of all possible workspaces.
* Otherwise, it will consult the workspace rules to determine which workspace numbers
* belong to the specified monitor.
*
* @param monitorId - The numeric identifier of the monitor.
*
* @returns An array of workspace numbers belonging to the specified monitor.
*/
export function getWorkspacesForMonitor(monitorId: number): number[] {
const allMonitors = hyprlandService.get_monitors();
if (allMonitors.length === 1) {
return Array.from({ length: workspaces.get() }, (_, index) => index + 1);
}
const workspaceMonitorRules = getWorkspaceMonitorMap();
const monitorNameMap: MonitorMap = {};
allMonitors.forEach((monitorInstance) => {
monitorNameMap[monitorInstance.id] = monitorInstance.name;
});
const currentMonitorName = monitorNameMap[monitorId];
return workspaceMonitorRules[currentMonitorName];
}
/**
* Checks whether a given workspace is valid (assigned) for the specified monitor.
*
* This function inspects the workspace rules object to determine if the current workspace belongs
* to the target monitor. If no workspace rules exist, the function defaults to returning `true`.
*
* @param workspaceId - The number representing the current workspace.
* @param workspaceMonitorRules - The map of monitor names to assigned workspace numbers.
* @param monitorId - The numeric identifier for the monitor.
* @param workspaceList - A list of Hyprland workspace objects.
* @param monitorList - A list of Hyprland monitor objects.
*
* @returns `true` if the workspace is assigned to the monitor or if no rules exist. Otherwise, `false`.
*/
function isWorkspaceValidForMonitor(
workspaceId: number,
workspaceMonitorRules: WorkspaceMonitorMap,
monitorId: number,
workspaceList: AstalHyprland.Workspace[], workspaceList: AstalHyprland.Workspace[],
monitorList: AstalHyprland.Monitor[], monitorList: AstalHyprland.Monitor[],
): boolean => { ): boolean {
if (!wsRules || !Object.keys(wsRules).length) { const monitorNameMap: MonitorMap = {};
return true; const allWorkspaceInstances = workspaceList ?? [];
}
const monitorMap: MonitorMap = {}; const workspaceMonitorReferences = allWorkspaceInstances
.filter((workspaceInstance) => workspaceInstance !== null)
const wsList = workspaceList ?? []; .map((workspaceInstance) => {
return {
const workspaceMonitorList = wsList id: workspaceInstance.monitor?.id,
.filter((m) => m !== null) name: workspaceInstance.monitor?.name,
.map((m) => { };
return { id: m.monitor?.id, name: m.monitor?.name };
}); });
const monitors = [...new Map([...workspaceMonitorList, ...monitorList].map((item) => [item.id, item])).values()]; const mergedMonitorInstances = [
...new Map(
[...workspaceMonitorReferences, ...monitorList].map((monitorCandidate) => [
monitorCandidate.id,
monitorCandidate,
]),
).values(),
];
monitors.forEach((mon) => (monitorMap[mon.id] = mon.name)); mergedMonitorInstances.forEach((monitorInstance) => {
monitorNameMap[monitorInstance.id] = monitorInstance.name;
});
const currentMonitorName = monitorMap[monitor]; const currentMonitorName = monitorNameMap[monitorId];
const monitorWSRules = wsRules[currentMonitorName]; const currentMonitorWorkspaceRules = workspaceMonitorRules[currentMonitorName];
if (monitorWSRules === undefined) { if (currentMonitorWorkspaceRules === undefined) {
return true; return false;
} }
return monitorWSRules.includes(curWs);
}; return currentMonitorWorkspaceRules.includes(workspaceId);
}
/** /**
* Retrieves the workspace rules. * Fetches a map of monitors to the workspace numbers that belong to them.
* *
* This function fetches and parses the workspace rules from the Hyprland service. * This function communicates with the Hyprland service to retrieve workspace rules in JSON format.
* Those rules are parsed, and a map of monitor names to lists of assigned workspace numbers is constructed.
* *
* @returns The workspace rules map. * @returns An object where each key is a monitor name, and each value is an array of workspace numbers.
*/ */
export const getWorkspaceRules = (): WorkspaceMap => { function getWorkspaceMonitorMap(): WorkspaceMonitorMap {
try { try {
const rules = exec('hyprctl workspacerules -j'); const rulesResponse = hyprlandService.message('j/workspacerules');
const workspaceMonitorRules: WorkspaceMonitorMap = {};
const parsedWorkspaceRules = JSON.parse(rulesResponse);
const workspaceRules: WorkspaceMap = {}; parsedWorkspaceRules.forEach((rule: WorkspaceRule) => {
const workspaceNumber = parseInt(rule.workspaceString, 10);
JSON.parse(rules).forEach((rule: WorkspaceRule) => { if (rule.monitor === undefined || isNaN(workspaceNumber)) {
const workspaceNum = parseInt(rule.workspaceString, 10);
if (isNaN(workspaceNum)) {
return; return;
} }
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
workspaceRules[rule.monitor].push(workspaceNum); const doesMonitorExistInRules = Object.hasOwnProperty.call(workspaceMonitorRules, rule.monitor);
if (doesMonitorExistInRules) {
workspaceMonitorRules[rule.monitor].push(workspaceNumber);
} else { } else {
workspaceRules[rule.monitor] = [workspaceNum]; workspaceMonitorRules[rule.monitor] = [workspaceNumber];
} }
}); });
return workspaceRules; return workspaceMonitorRules;
} catch (err) { } catch (error) {
console.error(err); console.error(error);
return {}; return {};
} }
}; }
/** /**
* Retrieves the current monitor's workspaces. * Checks if a workspace number should be ignored based on a regular expression.
* *
* This function returns a list of workspace numbers for the specified monitor. * @param ignoredWorkspacesVariable - A Variable object containing a string pattern of ignored workspaces.
* @param workspaceNumber - The numeric representation of the workspace to check.
* *
* @param monitor - The monitor ID. * @returns `true` if the workspace should be ignored, otherwise `false`.
*
* @returns The list of workspace numbers.
*/ */
export const getCurrentMonitorWorkspaces = (monitor: number): number[] => { function isWorkspaceIgnored(ignoredWorkspacesVariable: Variable<string>, workspaceNumber: number): boolean {
if (hyprlandService.get_monitors().length === 1) { if (ignoredWorkspacesVariable.get() === '') {
return Array.from({ length: workspaces.get() }, (_, i) => i + 1); return false;
} }
const monitorWorkspaces = getWorkspaceRules(); const ignoredWorkspacesRegex = new RegExp(ignoredWorkspacesVariable.get());
const monitorMap: MonitorMap = {}; return ignoredWorkspacesRegex.test(workspaceNumber.toString());
hyprlandService.get_monitors().forEach((m) => (monitorMap[m.id] = m.name)); }
const currentMonitorName = monitorMap[monitor];
return monitorWorkspaces[currentMonitorName];
};
/** /**
* Checks if a workspace is ignored. * Changes the active workspace in the specified direction ('next' or 'prev').
* *
* This function determines if a given workspace number is in the ignored workspaces list. * This function uses the current monitor's set of active or assigned workspaces and
* * cycles through them in the chosen direction. It also respects the list of ignored
* @param ignoredWorkspaces - The ignored workspaces variable. * workspaces, skipping any that match the ignored pattern.
* @param workspaceNumber - The workspace number.
*
* @returns Whether the workspace is ignored.
*/
export const isWorkspaceIgnored = (ignoredWorkspaces: Variable<string>, workspaceNumber: number): boolean => {
if (ignoredWorkspaces.get() === '') return false;
const ignoredWsRegex = new RegExp(ignoredWorkspaces.get());
return ignoredWsRegex.test(workspaceNumber.toString());
};
/**
* Navigates to the next or previous workspace.
*
* This function changes the current workspace to the next or previous one, considering active and ignored workspaces.
* *
* @param direction - The direction to navigate ('next' or 'prev'). * @param direction - The direction to navigate ('next' or 'prev').
* @param currentMonitorWorkspaces - The current monitor's workspaces variable. * @param currentMonitorWorkspacesVariable - A Variable containing an array of workspace numbers for the current monitor.
* @param activeWorkspaces - Whether to consider only active workspaces. * @param onlyActiveWorkspaces - Whether to only include active (occupied) workspaces when navigating.
* @param ignoredWorkspaces - The ignored workspaces variable. * @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern.
*/ */
const navigateWorkspace = ( function navigateWorkspace(
direction: 'next' | 'prev', direction: 'next' | 'prev',
currentMonitorWorkspaces: Variable<number[]>, currentMonitorWorkspacesVariable: Variable<number[]>,
activeWorkspaces: boolean, onlyActiveWorkspaces: boolean,
ignoredWorkspaces: Variable<string>, ignoredWorkspacesVariable: Variable<string>,
): void => { ): void {
const hyprlandWorkspaces = hyprlandService.get_workspaces() || []; const allHyprlandWorkspaces = hyprlandService.get_workspaces() || [];
const occupiedWorkspaces = hyprlandWorkspaces
.filter((ws) => hyprlandService.focusedMonitor.id === ws.monitor?.id)
.map((ws) => ws.id);
const workspacesList = activeWorkspaces const activeWorkspaceIds = allHyprlandWorkspaces
? occupiedWorkspaces .filter((workspaceInstance) => hyprlandService.focusedMonitor.id === workspaceInstance.monitor?.id)
: currentMonitorWorkspaces.get() || Array.from({ length: workspaces.get() }, (_, i) => i + 1); .map((workspaceInstance) => workspaceInstance.id);
if (workspacesList.length === 0) return; const assignedOrOccupiedWorkspaces = onlyActiveWorkspaces
? activeWorkspaceIds
: currentMonitorWorkspacesVariable.get() || Array.from({ length: workspaces.get() }, (_, index) => index + 1);
const currentIndex = workspacesList.indexOf(hyprlandService.focusedWorkspace?.id); if (assignedOrOccupiedWorkspaces.length === 0) {
return;
}
const workspaceIndex = assignedOrOccupiedWorkspaces.indexOf(hyprlandService.focusedWorkspace?.id);
const step = direction === 'next' ? 1 : -1; const step = direction === 'next' ? 1 : -1;
let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length;
let newIndex = (workspaceIndex + step + assignedOrOccupiedWorkspaces.length) % assignedOrOccupiedWorkspaces.length;
let attempts = 0; let attempts = 0;
while (attempts < workspacesList.length) { while (attempts < assignedOrOccupiedWorkspaces.length) {
const targetWS = workspacesList[newIndex]; const targetWorkspaceNumber = assignedOrOccupiedWorkspaces[newIndex];
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) { if (!isWorkspaceIgnored(ignoredWorkspacesVariable, targetWorkspaceNumber)) {
hyprlandService.dispatch('workspace', targetWS.toString()); hyprlandService.dispatch('workspace', targetWorkspaceNumber.toString());
return; return;
} }
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length; newIndex = (newIndex + step + assignedOrOccupiedWorkspaces.length) % assignedOrOccupiedWorkspaces.length;
attempts++; attempts++;
} }
}; }
/** /**
* Navigates to the next workspace. * Navigates to the next workspace in the current monitor.
* *
* This function changes the current workspace to the next one. * @param currentMonitorWorkspacesVariable - A Variable containing workspace numbers for the current monitor.
* * @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
* @param currentMonitorWorkspaces - The current monitor's workspaces variable. * @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern.
* @param activeWorkspaces - Whether to consider only active workspaces.
* @param ignoredWorkspaces - The ignored workspaces variable.
*/ */
export const goToNextWS = ( export function goToNextWorkspace(
currentMonitorWorkspaces: Variable<number[]>, currentMonitorWorkspacesVariable: Variable<number[]>,
activeWorkspaces: boolean, onlyActiveWorkspaces: boolean,
ignoredWorkspaces: Variable<string>, ignoredWorkspacesVariable: Variable<string>,
): void => { ): void {
navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces); navigateWorkspace('next', currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignoredWorkspacesVariable);
}; }
/** /**
* Navigates to the previous workspace. * Navigates to the previous workspace in the current monitor.
* *
* This function changes the current workspace to the previous one. * @param currentMonitorWorkspacesVariable - A Variable containing workspace numbers for the current monitor.
* * @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
* @param currentMonitorWorkspaces - The current monitor's workspaces variable. * @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern.
* @param activeWorkspaces - Whether to consider only active workspaces.
* @param ignoredWorkspaces - The ignored workspaces variable.
*/ */
export const goToPrevWS = ( export function goToPreviousWorkspace(
currentMonitorWorkspaces: Variable<number[]>, currentMonitorWorkspacesVariable: Variable<number[]>,
activeWorkspaces: boolean, onlyActiveWorkspaces: boolean,
ignoredWorkspaces: Variable<string>, ignoredWorkspacesVariable: Variable<string>,
): void => { ): void {
navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces); navigateWorkspace('prev', currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignoredWorkspacesVariable);
}; }
/** /**
* Throttles a function to limit its execution rate. * Limits the execution rate of a given function to prevent it from being called too often.
* *
* This function ensures that the provided function is not called more often than the specified limit. * @param func - The function to be throttled.
* @param limit - The time limit (in milliseconds) during which calls to `func` are disallowed after the first call.
* *
* @param func - The function to throttle. * @returns The throttled version of the input function.
* @param limit - The time limit in milliseconds.
*
* @returns The throttled function.
*/ */
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T { export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
let inThrottle: boolean; let isThrottleActive: boolean;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) { return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (!inThrottle) { if (!isThrottleActive) {
func.apply(this, args); func.apply(this, args);
inThrottle = true; isThrottleActive = true;
setTimeout(() => { setTimeout(() => {
inThrottle = false; isThrottleActive = false;
}, limit); }, limit);
} }
} as T; } as T;
} }
/** /**
* Creates throttled scroll handlers for navigating workspaces. * Creates throttled scroll handlers that navigate workspaces upon scrolling, respecting the configured scroll speed.
* *
* This function returns handlers for scrolling up and down through workspaces, throttled by the specified scroll speed. * @param scrollSpeed - The factor by which the scroll navigation is throttled.
* @param currentMonitorWorkspacesVariable - A Variable containing the current monitor's workspace numbers.
* @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
* *
* @param scrollSpeed - The scroll speed. * @returns An object containing two functions (`throttledScrollUp` and `throttledScrollDown`), both throttled.
* @param currentMonitorWorkspaces - The current monitor's workspaces variable.
* @param activeWorkspaces - Whether to consider only active workspaces.
*
* @returns The throttled scroll handlers.
*/ */
export const createThrottledScrollHandlers = ( export function initThrottledScrollHandlers(
scrollSpeed: number, scrollSpeed: number,
currentMonitorWorkspaces: Variable<number[]>, currentMonitorWorkspacesVariable: Variable<number[]>,
activeWorkspaces: boolean = true, onlyActiveWorkspaces: boolean = true,
): ThrottledScrollHandlers => { ): ThrottledScrollHandlers {
const throttledScrollUp = throttle(() => { const throttledScrollUp = throttle(() => {
if (reverse_scroll.get()) { if (reverse_scroll.get()) {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored); goToPreviousWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored);
} else { } else {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored); goToNextWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored);
} }
}, 200 / scrollSpeed); }, 200 / scrollSpeed);
const throttledScrollDown = throttle(() => { const throttledScrollDown = throttle(() => {
if (reverse_scroll.get()) { if (reverse_scroll.get()) {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored); goToNextWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored);
} else { } else {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored); goToPreviousWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored);
} }
}, 200 / scrollSpeed); }, 200 / scrollSpeed);
return { throttledScrollUp, throttledScrollDown }; return { throttledScrollUp, throttledScrollDown };
}; }
/** /**
* Retrieves the workspaces to render. * Computes which workspace numbers should be rendered for a given monitor.
* *
* This function returns a list of workspace numbers to render based on the total workspaces, workspace list, rules, and monitor. * This function consolidates both active and all possible workspaces (based on rules),
* then filters them by the selected monitor if `isMonitorSpecific` is set to `true`.
* *
* @param totalWorkspaces - The total number of workspaces. * @param totalWorkspaces - The total number of workspaces (a fallback if workspace rules are not enforced).
* @param workspaceList - The list of workspaces. * @param workspaceInstances - A list of Hyprland workspace objects.
* @param workspaceRules - The workspace rules map. * @param workspaceMonitorRules - The map of monitor names to assigned workspace numbers.
* @param monitor - The monitor ID. * @param monitorId - The numeric identifier of the monitor.
* @param isMonitorSpecific - Whether the workspaces are monitor-specific. * @param isMonitorSpecific - If `true`, only include the workspaces that match this monitor.
* @param monitorList - The list of monitors. * @param hyprlandMonitorInstances - A list of Hyprland monitor objects.
* *
* @returns The list of workspace numbers to render. * @returns An array of workspace numbers that should be shown.
*/ */
export const getWorkspacesToRender = ( export function getWorkspacesToRender(
totalWorkspaces: number, totalWorkspaces: number,
workspaceList: AstalHyprland.Workspace[], workspaceInstances: AstalHyprland.Workspace[],
workspaceRules: WorkspaceMap, workspaceMonitorRules: WorkspaceMonitorMap,
monitor: number, monitorId: number,
isMonitorSpecific: boolean, isMonitorSpecific: boolean,
monitorList: AstalHyprland.Monitor[], hyprlandMonitorInstances: AstalHyprland.Monitor[],
): number[] => { ): number[] {
let allWorkspaces = range(totalWorkspaces || 8); let allPotentialWorkspaces = range(totalWorkspaces || 8);
const activeWorkspaces = workspaceList.map((ws) => ws.id); const allWorkspaceInstances = workspaceInstances ?? [];
const wsList = workspaceList ?? []; const activeWorkspaceIds = allWorkspaceInstances.map((workspaceInstance) => workspaceInstance.id);
const workspaceMonitorList = wsList.map((ws) => {
const monitorReferencesForActiveWorkspaces = allWorkspaceInstances.map((workspaceInstance) => {
return { return {
id: ws.monitor?.id || -1, id: workspaceInstance.monitor?.id ?? -1,
name: ws.monitor?.name || '', name: workspaceInstance.monitor?.name ?? '',
}; };
}); });
const curMonitor = const currentMonitorInstance =
monitorList.find((mon) => mon.id === monitor) || workspaceMonitorList.find((mon) => mon.id === monitor); hyprlandMonitorInstances.find((monitorObj) => monitorObj.id === monitorId) ||
monitorReferencesForActiveWorkspaces.find((monitorObj) => monitorObj.id === monitorId);
const workspacesWithRules = Object.keys(workspaceRules).reduce((acc: number[], k: string) => { const allWorkspacesWithRules = Object.keys(workspaceMonitorRules).reduce(
return [...acc, ...workspaceRules[k]]; (accumulator: number[], monitorName: string) => {
}, []); return [...accumulator, ...workspaceMonitorRules[monitorName]];
},
[],
);
const activesForMonitor = activeWorkspaces.filter((w) => { const activeWorkspacesForCurrentMonitor = activeWorkspaceIds.filter((workspaceId) => {
if ( if (
curMonitor && currentMonitorInstance &&
Object.hasOwnProperty.call(workspaceRules, curMonitor.name) && Object.hasOwnProperty.call(workspaceMonitorRules, currentMonitorInstance.name) &&
workspacesWithRules.includes(w) allWorkspacesWithRules.includes(workspaceId)
) { ) {
return workspaceRules[curMonitor.name].includes(w); return workspaceMonitorRules[currentMonitorInstance.name].includes(workspaceId);
} }
return true; const metadataForWorkspace = allWorkspaceInstances.find((workspaceObj) => workspaceObj.id === workspaceId);
return metadataForWorkspace?.monitor?.id === monitorId;
}); });
if (isMonitorSpecific) { if (isMonitorSpecific) {
const workspacesInRange = range(totalWorkspaces).filter((ws) => { const validWorkspaceNumbers = range(totalWorkspaces).filter((workspaceNumber) => {
return getWorkspacesForMonitor(ws, workspaceRules, monitor, wsList, monitorList); return isWorkspaceValidForMonitor(
workspaceNumber,
workspaceMonitorRules,
monitorId,
allWorkspaceInstances,
hyprlandMonitorInstances,
);
}); });
allWorkspaces = [...new Set([...activesForMonitor, ...workspacesInRange])]; allPotentialWorkspaces = [...new Set([...activeWorkspacesForCurrentMonitor, ...validWorkspaceNumbers])];
} else { } else {
allWorkspaces = [...new Set([...allWorkspaces, ...activeWorkspaces])]; allPotentialWorkspaces = [...new Set([...allPotentialWorkspaces, ...activeWorkspaceIds])];
} }
return allWorkspaces.sort((a, b) => a - b); return allPotentialWorkspaces.filter((workspace) => !isWorkspaceIgnored(ignored, workspace)).sort((a, b) => a - b);
}; }
/** /**
* The workspace rules variable. * Subscribes to Hyprland service events related to workspaces to keep the local state updated.
* This variable holds the current workspace rules. *
* When certain events occur (like a configuration reload or a client being moved/added/removed),
* this function updates the workspace rules or toggles the `forceUpdater` variable to ensure
* that any dependent UI or logic is re-rendered or re-run.
*/ */
export const workspaceRules = Variable(getWorkspaceRules()); export function initWorkspaceEvents(): void {
/**
* The force updater variable.
* This variable is used to force updates when workspace events occur.
*/
export const forceUpdater = Variable(true);
/**
* Sets up connections for workspace events.
* This function sets up event listeners for various workspace-related events to update the workspace rules and force updates.
*/
export const setupConnections = (): void => {
hyprlandService.connect('config-reloaded', () => { hyprlandService.connect('config-reloaded', () => {
workspaceRules.set(getWorkspaceRules()); workspaceRules.set(getWorkspaceMonitorMap());
}); });
hyprlandService.connect('client-moved', () => { hyprlandService.connect('client-moved', () => {
@@ -358,9 +392,19 @@ export const setupConnections = (): void => {
hyprlandService.connect('client-removed', () => { hyprlandService.connect('client-removed', () => {
forceUpdater.set(!forceUpdater.get()); forceUpdater.set(!forceUpdater.get());
}); });
}; }
/**
* Throttled scroll handler functions for navigating workspaces.
*/
type ThrottledScrollHandlers = { type ThrottledScrollHandlers = {
/**
* Scroll up throttled handler.
*/
throttledScrollUp: () => void; throttledScrollUp: () => void;
/**
* Scroll down throttled handler.
*/
throttledScrollDown: () => void; throttledScrollDown: () => void;
}; };

View File

@@ -1,5 +1,5 @@
import options from 'src/options'; import options from 'src/options';
import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers'; import { initThrottledScrollHandlers, getWorkspacesForMonitor } from './helpers';
import { BarBoxChild } from 'src/lib/types/bar'; import { BarBoxChild } from 'src/lib/types/bar';
import { WorkspaceModule } from './workspaces'; import { WorkspaceModule } from './workspaces';
import { bind, Variable } from 'astal'; import { bind, Variable } from 'astal';
@@ -10,10 +10,10 @@ import { isScrollDown, isScrollUp } from 'src/lib/utils';
const { workspaces, scroll_speed } = options.bar.workspaces; const { workspaces, scroll_speed } = options.bar.workspaces;
const Workspaces = (monitor = -1): BarBoxChild => { const Workspaces = (monitor = -1): BarBoxChild => {
const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor)); const currentMonitorWorkspaces = Variable(getWorkspacesForMonitor(monitor));
workspaces.subscribe(() => { workspaces.subscribe(() => {
currentMonitorWorkspaces.set(getCurrentMonitorWorkspaces(monitor)); currentMonitorWorkspaces.set(getWorkspacesForMonitor(monitor));
}); });
const component = ( const component = (
@@ -35,7 +35,7 @@ const Workspaces = (monitor = -1): BarBoxChild => {
self.disconnect(scrollHandlers); self.disconnect(scrollHandlers);
} }
const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers( const { throttledScrollUp, throttledScrollDown } = initThrottledScrollHandlers(
scroll_speed, scroll_speed,
currentMonitorWorkspaces, currentMonitorWorkspaces,
); );

View File

@@ -1,6 +1,6 @@
import { hyprlandService } from 'src/lib/constants/services'; import { hyprlandService } from 'src/lib/constants/services';
import options from 'src/options'; import options from 'src/options';
import { forceUpdater, getWorkspacesToRender, isWorkspaceIgnored, setupConnections, workspaceRules } from './helpers'; import { forceUpdater, getWorkspacesToRender, initWorkspaceEvents, workspaceRules } from './helpers';
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from './helpers/utils'; import { getAppIcon, getWsColor, renderClassnames, renderLabel } from './helpers/utils';
import { ApplicationIcons, WorkspaceIconMap } from 'src/lib/types/workspace'; import { ApplicationIcons, WorkspaceIconMap } from 'src/lib/types/workspace';
import { bind, Variable } from 'astal'; import { bind, Variable } from 'astal';
@@ -30,7 +30,7 @@ const { available, active, occupied } = options.bar.workspaces.icons;
const { matugen } = options.theme; const { matugen } = options.theme;
const { smartHighlight } = options.theme.bar.buttons.workspaces; const { smartHighlight } = options.theme.bar.buttons.workspaces;
setupConnections(); initWorkspaceEvents();
export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element => { export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element => {
const boxChildren = Variable.derive( const boxChildren = Variable.derive(
@@ -98,10 +98,6 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element
); );
return workspacesToRender.map((wsId, index) => { return workspacesToRender.map((wsId, index) => {
if (isWorkspaceIgnored(ignored, wsId)) {
return <box />;
}
const appIcons = displayApplicationIcons const appIcons = displayApplicationIcons
? getAppIcon(wsId, appIconOncePerWorkspace, { ? getAppIcon(wsId, appIconOncePerWorkspace, {
iconMap: applicationIconMapping, iconMap: applicationIconMapping,

View File

@@ -1,4 +1,4 @@
import { bind, exec } from 'astal'; import { bind, GLib } from 'astal';
import { Gtk } from 'astal/gtk3'; import { Gtk } from 'astal/gtk3';
import options from 'src/options.js'; import options from 'src/options.js';
import { normalizePath, isAnImage } from 'src/lib/utils.js'; import { normalizePath, isAnImage } from 'src/lib/utils.js';
@@ -28,7 +28,8 @@ const ProfileName = (): JSX.Element => {
halign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER}
label={bind(name).as((profileName) => { label={bind(name).as((profileName) => {
if (profileName === 'system') { if (profileName === 'system') {
return exec('bash -c whoami'); const username = GLib.get_user_name();
return username;
} }
return profileName; return profileName;
})} })}

View File

@@ -1,97 +1,215 @@
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
import options from 'src/options'; import options from 'src/options';
import { bash } from 'src/lib/utils';
import { globalEventBoxes } from 'src/globals/dropdown'; import { globalEventBoxes } from 'src/globals/dropdown';
import { GLib } from 'astal'; import { GLib } from 'astal';
import { EventBox } from 'astal/gtk3/widget'; import { EventBox } from 'astal/gtk3/widget';
import { hyprlandService } from 'src/lib/constants/services';
const hyprland = AstalHyprland.get_default(); import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const { location } = options.theme.bar; const { location } = options.theme.bar;
const { scalingPriority } = options; const { scalingPriority } = options;
export const calculateMenuPosition = async (pos: number[], windowName: string): Promise<void> => { /**
* Retrieves the dropdown EventBox widget from the global event boxes map using the provided window name.
*
* @param windowName - A string identifier for the window whose EventBox you want to retrieve.
* @returns The EventBox object if found; otherwise, `undefined`.
*/
function getDropdownEventBox(windowName: string): EventBox | undefined {
return globalEventBoxes.get()[windowName];
}
/**
* Finds and returns the currently focused Hyprland monitor object.
*
* @returns The focused Hyprland monitor, or `undefined` if no match is found.
*/
function getFocusedHyprlandMonitor(): AstalHyprland.Monitor | undefined {
const allMonitors = hyprlandService.get_monitors();
return allMonitors.find((monitor) => monitor.id === hyprlandService.focusedMonitor.id);
}
/**
* Computes the scaled monitor dimensions based on user configuration and environment variables.
*
* This function applies:
* 1. GDK scaling (from the `GDK_SCALE` environment variable).
* 2. Hyprland scaling (from the monitor's scale).
*
* The order in which scaling is applied depends on `scalingPriority`:
* - 'both': Apply GDK scale first, then Hyprland scale.
* - 'gdk': Apply GDK scale only.
* - Otherwise: Apply Hyprland scale only.
*
* @param width - The original width of the focused Hyprland monitor.
* @param height - The original height of the focused Hyprland monitor.
* @param monitorScaling - The scale factor reported by Hyprland for this monitor.
* @returns An object containing `adjustedWidth` and `adjustedHeight` after scaling is applied.
*/
function applyMonitorScaling(width: number, height: number, monitorScaling: number): MonitorScaling {
const gdkEnvScale = GLib.getenv('GDK_SCALE') || '1';
const userScalingPriority = scalingPriority.get();
let adjustedWidth = width;
let adjustedHeight = height;
if (userScalingPriority === 'both') {
const gdkScaleValue = parseFloat(gdkEnvScale);
adjustedWidth /= gdkScaleValue;
adjustedHeight /= gdkScaleValue;
adjustedWidth /= monitorScaling;
adjustedHeight /= monitorScaling;
} else if (/^\d+(\.\d+)?$/.test(gdkEnvScale) && userScalingPriority === 'gdk') {
const gdkScaleValue = parseFloat(gdkEnvScale);
adjustedWidth /= gdkScaleValue;
adjustedHeight /= gdkScaleValue;
} else {
adjustedWidth /= monitorScaling;
adjustedHeight /= monitorScaling;
}
return { adjustedWidth, adjustedHeight };
}
/**
* Corrects monitor dimensions if the monitor is rotated (vertical orientation),
* which requires swapping the width and height.
*
* @param monitorWidth - The monitor width after scaling.
* @param monitorHeight - The monitor height after scaling.
* @param isVertical - Whether the monitor transform indicates a vertical rotation.
* @returns The appropriately adjusted width and height.
*/
function adjustForVerticalTransform(
monitorWidth: number,
monitorHeight: number,
isVertical: boolean,
): TransformedDimensions {
if (!isVertical) {
return { finalWidth: monitorWidth, finalHeight: monitorHeight };
}
return { finalWidth: monitorHeight, finalHeight: monitorWidth };
}
/**
* Calculates the left and right margins required to place the dropdown in the correct position
* relative to the monitor width and the desired anchor X coordinate.
*
* @param monitorWidth - The width of the monitor (already scaled).
* @param dropdownWidth - The width of the dropdown widget.
* @param anchorX - The X coordinate (in scaled pixels) around which the dropdown should be placed.
* @returns An object containing `leftMargin` and `rightMargin`, ensuring they do not go below 0.
*/
function calculateHorizontalMargins(monitorWidth: number, dropdownWidth: number, anchorX: number): HorizontalMargins {
const minimumSpacing = 0;
let rightMarginSpacing = monitorWidth - dropdownWidth / 2;
rightMarginSpacing -= anchorX;
let leftMarginSpacing = monitorWidth - dropdownWidth - rightMarginSpacing;
if (rightMarginSpacing < minimumSpacing) {
rightMarginSpacing = minimumSpacing;
leftMarginSpacing = monitorWidth - dropdownWidth - minimumSpacing;
}
if (leftMarginSpacing < minimumSpacing) {
leftMarginSpacing = minimumSpacing;
rightMarginSpacing = monitorWidth - dropdownWidth - minimumSpacing;
}
return { leftMargin: leftMarginSpacing, rightMargin: rightMarginSpacing };
}
/**
* Positions the dropdown vertically based on the global bar location setting.
* If the bar is positioned at the top, the dropdown is placed at the top (margin_top = 0).
* Otherwise, it's placed at the bottom.
*
* @param dropdownEventBox - The EventBox representing the dropdown.
* @param monitorHeight - The scaled (and possibly swapped) monitor height.
* @param dropdownHeight - The height of the dropdown widget.
*/
function setVerticalPosition(dropdownEventBox: EventBox, monitorHeight: number, dropdownHeight: number): void {
if (location.get() === 'top') {
dropdownEventBox.set_margin_top(0);
dropdownEventBox.set_margin_bottom(monitorHeight);
} else {
dropdownEventBox.set_margin_bottom(0);
dropdownEventBox.set_margin_top(monitorHeight - dropdownHeight);
}
}
/**
* Adjusts the position of a dropdown menu (event box) based on the focused monitor, scaling preferences,
* and the bar location setting. It ensures the dropdown is accurately placed either at the top or bottom
* of the screen within monitor boundaries, respecting both GDK scaling and Hyprland scaling.
*
* @param positionCoordinates - An array of `[x, y]` values representing the anchor position at which
* the dropdown should ideally appear (only the X coordinate is used here).
* @param windowName - A string that identifies the window in the globalEventBoxes map.
*
* @returns A Promise that resolves once the dropdown position is fully calculated and set.
*/
export const calculateMenuPosition = async (positionCoordinates: number[], windowName: string): Promise<void> => {
try { try {
const self = globalEventBoxes.get()[windowName] as EventBox; const dropdownEventBox = getDropdownEventBox(windowName);
const curHyprlandMonitor = hyprland.get_monitors().find((m) => m.id === hyprland.focusedMonitor.id); if (!dropdownEventBox) {
const dropdownWidth = self.get_child()?.get_allocation().width ?? 0;
const dropdownHeight = self.get_child()?.get_allocation().height ?? 0;
let hyprScaling = 1;
const monitorInfo = await bash('hyprctl monitors -j');
const parsedMonitorInfo = JSON.parse(monitorInfo);
const foundMonitor = parsedMonitorInfo.find(
(monitor: AstalHyprland.Monitor) => monitor.id === hyprland.focusedMonitor.id,
);
hyprScaling = foundMonitor?.scale || 1;
let monWidth = curHyprlandMonitor?.width;
let monHeight = curHyprlandMonitor?.height;
if (monWidth === undefined || monHeight === undefined || hyprScaling === undefined) {
return; return;
} }
// If GDK Scaling is applied, then get divide width by scaling const focusedHyprlandMonitor = getFocusedHyprlandMonitor();
// to get the proper coordinates.
// Ex: On a 2860px wide monitor... if scaling is set to 2, then the right
// end of the monitor is the 1430th pixel.
const gdkScale = GLib.getenv('GDK_SCALE') || '1';
if (scalingPriority.get() === 'both') { if (!focusedHyprlandMonitor) {
const scale = parseFloat(gdkScale); return;
monWidth = monWidth / scale;
monHeight = monHeight / scale;
monWidth = monWidth / hyprScaling;
monHeight = monHeight / hyprScaling;
} else if (/^\d+(.\d+)?$/.test(gdkScale) && scalingPriority.get() === 'gdk') {
const scale = parseFloat(gdkScale);
monWidth = monWidth / scale;
monHeight = monHeight / scale;
} else {
monWidth = monWidth / hyprScaling;
monHeight = monHeight / hyprScaling;
} }
// If monitor is vertical (transform = 1 || 3) swap height and width const dropdownWidth = dropdownEventBox.get_child()?.get_allocation().width ?? 0;
const isVertical = curHyprlandMonitor?.transform !== undefined ? curHyprlandMonitor.transform % 2 !== 0 : false; const dropdownHeight = dropdownEventBox.get_child()?.get_allocation().height ?? 0;
if (isVertical) { const monitorScaling = focusedHyprlandMonitor.scale || 1;
[monWidth, monHeight] = [monHeight, monWidth]; const { width: rawMonitorWidth, height: rawMonitorHeight, transform } = focusedHyprlandMonitor;
if (!rawMonitorWidth || !rawMonitorHeight) {
return;
} }
let marginRight = monWidth - dropdownWidth / 2; const { adjustedWidth, adjustedHeight } = applyMonitorScaling(
marginRight = marginRight - pos[0]; rawMonitorWidth,
let marginLeft = monWidth - dropdownWidth - marginRight; rawMonitorHeight,
monitorScaling,
);
const minimumMargin = 0; const isVertical = transform !== undefined ? transform % 2 !== 0 : false;
const { finalWidth, finalHeight } = adjustForVerticalTransform(adjustedWidth, adjustedHeight, isVertical);
if (marginRight < minimumMargin) { const { leftMargin, rightMargin } = calculateHorizontalMargins(
marginRight = minimumMargin; finalWidth,
marginLeft = monWidth - dropdownWidth - minimumMargin; dropdownWidth,
} positionCoordinates[0],
);
if (marginLeft < minimumMargin) { dropdownEventBox.set_margin_left(leftMargin);
marginLeft = minimumMargin; dropdownEventBox.set_margin_right(rightMargin);
marginRight = monWidth - dropdownWidth - minimumMargin;
}
self.set_margin_left(marginLeft); setVerticalPosition(dropdownEventBox, finalHeight, dropdownHeight);
self.set_margin_right(marginRight); } catch (caughtError) {
console.error(`Error getting menu position: ${caughtError}`);
if (location.get() === 'top') {
self.set_margin_top(0);
self.set_margin_bottom(monHeight);
} else {
self.set_margin_bottom(0);
self.set_margin_top(monHeight - dropdownHeight);
}
} catch (error) {
console.error(`Error getting menu position: ${error}`);
} }
}; };
type HorizontalMargins = {
leftMargin: number;
rightMargin: number;
};
type MonitorScaling = {
adjustedWidth: number;
adjustedHeight: number;
};
type TransformedDimensions = {
finalWidth: number;
finalHeight: number;
};

View File

@@ -140,12 +140,71 @@ export const BarSettings = (): JSX.Element => {
{/* Workspaces Section */} {/* Workspaces Section */}
<Header title="Workspaces" /> <Header title="Workspaces" />
<Option opt={options.theme.bar.buttons.workspaces.enableBorder} title="Button Border" type="boolean" /> <Option opt={options.theme.bar.buttons.workspaces.enableBorder} title="Button Border" type="boolean" />
<Option
opt={options.bar.workspaces.monitorSpecific}
title="Monitor Specific"
subtitle="Only workspaces of the monitor are shown."
type="boolean"
/>
<Option opt={options.bar.workspaces.show_icons} title="Show Workspace Icons" type="boolean" />
<Option opt={options.bar.workspaces.show_numbered} title="Show Workspace Numbers" type="boolean" />
<Option
opt={options.bar.workspaces.workspaceMask}
title="Zero-Based Workspace Numbers"
subtitle={
'Start all workspace numbers from 0 on each monitor.\n' +
"Requires 'Show Workspace Numbers' to be enabled."
}
type="boolean"
/>
<Option
opt={options.bar.workspaces.showWsIcons}
title="Map Workspaces to Icons"
subtitle="https://hyprpanel.com/configuration/panel.html#show-workspace-icons"
type="boolean"
/>
<Option
opt={options.bar.workspaces.showApplicationIcons}
title="Map Workspaces to Application Icons"
subtitle="Requires 'Map Workspace to Icons' enabled. See docs."
type="boolean"
/>
<Option
opt={options.bar.workspaces.applicationIconOncePerWorkspace}
title="Hide Duplicate App Icons"
type="boolean"
/>
<Option <Option
opt={options.bar.workspaces.showAllActive} opt={options.bar.workspaces.showAllActive}
title="Mark Active Workspace On All Monitors" title="Mark Active Workspace On All Monitors"
subtitle="Marks the currently active workspace on each monitor." subtitle="Marks the currently active workspace on each monitor."
type="boolean" type="boolean"
/> />
<Option
opt={options.bar.workspaces.numbered_active_indicator}
title="Numbered Workspace Identifier"
subtitle="Only applicable if Workspace Numbers are enabled"
type="enum"
enums={['underline', 'highlight', 'color']}
/>
<Option
opt={options.theme.bar.buttons.workspaces.smartHighlight}
title="Smart Highlight"
subtitle="Automatically determines highlight color for mapped icons."
type="boolean"
/>
<Option
opt={options.theme.bar.buttons.workspaces.numbered_active_highlight_border}
title="Highlight Radius"
subtitle="Only applicable if Workspace Numbers are enabled"
type="string"
/>
<Option
opt={options.theme.bar.buttons.workspaces.numbered_active_highlight_padding}
title="Highlight Padding"
subtitle="Only applicable if Workspace Numbers are enabled"
type="string"
/>
<Option <Option
opt={options.theme.bar.buttons.workspaces.pill.radius} opt={options.theme.bar.buttons.workspaces.pill.radius}
title="Pill Radius" title="Pill Radius"
@@ -176,53 +235,9 @@ export const BarSettings = (): JSX.Element => {
subtitle="Only applicable to numbered workspaces and mapped icons. Adjust carefully." subtitle="Only applicable to numbered workspaces and mapped icons. Adjust carefully."
type="string" type="string"
/> />
<Option opt={options.bar.workspaces.show_icons} title="Show Workspace Icons" type="boolean" />
<Option opt={options.bar.workspaces.icons.available} title="Workspace Available" type="string" /> <Option opt={options.bar.workspaces.icons.available} title="Workspace Available" type="string" />
<Option opt={options.bar.workspaces.icons.active} title="Workspace Active" type="string" /> <Option opt={options.bar.workspaces.icons.active} title="Workspace Active" type="string" />
<Option opt={options.bar.workspaces.icons.occupied} title="Workspace Occupied" type="string" /> <Option opt={options.bar.workspaces.icons.occupied} title="Workspace Occupied" type="string" />
<Option opt={options.bar.workspaces.show_numbered} title="Show Workspace Numbers" type="boolean" />
<Option
opt={options.bar.workspaces.numbered_active_indicator}
title="Numbered Workspace Identifier"
subtitle="Only applicable if Workspace Numbers are enabled"
type="enum"
enums={['underline', 'highlight', 'color']}
/>
<Option
opt={options.theme.bar.buttons.workspaces.smartHighlight}
title="Smart Highlight"
subtitle="Automatically determines highlight color for mapped icons."
type="boolean"
/>
<Option
opt={options.theme.bar.buttons.workspaces.numbered_active_highlight_border}
title="Highlight Radius"
subtitle="Only applicable if Workspace Numbers are enabled"
type="string"
/>
<Option
opt={options.theme.bar.buttons.workspaces.numbered_active_highlight_padding}
title="Highlight Padding"
subtitle="Only applicable if Workspace Numbers are enabled"
type="string"
/>
<Option
opt={options.bar.workspaces.showWsIcons}
title="Map Workspaces to Icons"
subtitle="https://hyprpanel.com/configuration/panel.html#show-workspace-icons"
type="boolean"
/>
<Option
opt={options.bar.workspaces.showApplicationIcons}
title="Map Workspaces to Application Icons"
subtitle="Requires 'Map Workspace to Icons' enabled. See docs."
type="boolean"
/>
<Option
opt={options.bar.workspaces.applicationIconOncePerWorkspace}
title="Hide Duplicate App Icons"
type="boolean"
/>
<Option <Option
opt={options.bar.workspaces.applicationIconMap} opt={options.bar.workspaces.applicationIconMap}
title="App Icon Mappings" title="App Icon Mappings"
@@ -254,28 +269,10 @@ export const BarSettings = (): JSX.Element => {
/> />
<Option <Option
opt={options.bar.workspaces.workspaces} opt={options.bar.workspaces.workspaces}
title="Total Workspaces" title="Persistent Workspaces"
subtitle="Minimum number of workspaces to always show." subtitle="Requires workspace rules to be defined if 'Monitor Specific' is selected."
type="number" type="number"
/> />
<Option
opt={options.bar.workspaces.monitorSpecific}
title="Monitor Specific"
subtitle="Only workspaces of the monitor are shown."
type="boolean"
/>
<Option
opt={options.bar.workspaces.hideUnoccupied}
title="Hide Unoccupied"
subtitle="Only show occupied or active workspaces"
type="boolean"
/>
<Option
opt={options.bar.workspaces.workspaceMask}
title="Mask Workspace Numbers On Monitors"
subtitle="For monitor-specific numbering"
type="boolean"
/>
<Option <Option
opt={options.bar.workspaces.reverse_scroll} opt={options.bar.workspaces.reverse_scroll}
title="Invert Scroll" title="Invert Scroll"

View File

@@ -1,5 +1,5 @@
import { Gtk } from 'astal/gtk3'; import { Gtk } from 'astal/gtk3';
import FileChooserButton from 'src/components/shared/FileChooseButton'; import FileChooserButton from 'src/components/shared/FileChooserButton';
import { Opt } from 'src/lib/option'; import { Opt } from 'src/lib/option';
const handleFileSet = const handleFileSet =

View File

@@ -1,4 +1,4 @@
import FileChooserButton from 'src/components/shared/FileChooseButton'; import FileChooserButton from 'src/components/shared/FileChooserButton';
import { Opt } from 'src/lib/option'; import { Opt } from 'src/lib/option';
import Wallpaper from 'src/services/Wallpaper'; import Wallpaper from 'src/services/Wallpaper';

View File

@@ -1,6 +1,7 @@
import { EventBox } from 'astal/gtk3/widget';
import Variable from 'astal/variable'; import Variable from 'astal/variable';
type GlobalEventBoxes = { type GlobalEventBoxes = {
[key: string]: unknown; [key: string]: EventBox;
}; };
export const globalEventBoxes: Variable<GlobalEventBoxes> = Variable({}); export const globalEventBoxes: Variable<GlobalEventBoxes> = Variable({});

View File

@@ -1,19 +1,18 @@
import { execAsync } from 'astal';
import { hyprlandService } from '../constants/services'; import { hyprlandService } from '../constants/services';
const floatSettingsDialog = (): void => { const floatSettingsDialog = (): void => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^(hyprpanel-settings)$"']); hyprlandService.message(`hyprctl keyword windowrulev2 "float, title:^(hyprpanel-settings)$"`);
hyprlandService.connect('config-reloaded', () => { hyprlandService.connect('config-reloaded', () => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^(hyprpanel-settings)$"']); hyprlandService.message(`hyprctl keyword windowrulev2 "float, title:^(hyprpanel-settings)$"`);
}); });
}; };
const floatFilePicker = (): void => { const floatFilePicker = (): void => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^((Save|Import) Hyprpanel.*)$"']); hyprlandService.message(`hyprctl keyword windowrulev2 "float, title:^((Save|Import) Hyprpanel.*)$"`);
hyprlandService.connect('config-reloaded', () => { hyprlandService.connect('config-reloaded', () => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^((Save|Import) Hyprpanel.*)$"']); hyprlandService.message(`hyprctl keyword windowrulev2 "float, title:^((Save|Import) Hyprpanel.*)$"`);
}); });
}; };

View File

@@ -3,7 +3,7 @@ export type WorkspaceRule = {
monitor: string; monitor: string;
}; };
export type WorkspaceMap = { export type WorkspaceMonitorMap = {
[key: string]: number[]; [key: string]: number[];
}; };

View File

@@ -959,7 +959,6 @@ const options = mkOptions(CONFIG, {
workspaces: opt(5), workspaces: opt(5),
spacing: opt(1), spacing: opt(1),
monitorSpecific: opt(true), monitorSpecific: opt(true),
hideUnoccupied: opt(true),
workspaceMask: opt(false), workspaceMask: opt(false),
reverse_scroll: opt(false), reverse_scroll: opt(false),
scroll_speed: opt(5), scroll_speed: opt(5),

View File

@@ -3,6 +3,7 @@ import { dependencies, sh } from '../lib/utils';
import options from '../options'; import options from '../options';
import { execAsync } from 'astal/process'; import { execAsync } from 'astal/process';
import { monitorFile } from 'astal/file'; import { monitorFile } from 'astal/file';
import { hyprlandService } from 'src/lib/constants/services';
const WP = `${GLib.get_home_dir()}/.config/background`; const WP = `${GLib.get_home_dir()}/.config/background`;
@@ -14,35 +15,34 @@ class Wallpaper extends GObject.Object {
#wallpaper(): void { #wallpaper(): void {
if (!dependencies('swww')) return; if (!dependencies('swww')) return;
sh('hyprctl cursorpos') try {
.then((pos) => { const cursorPosition = hyprlandService.message('cursorpos');
const transitionCmd = [ const transitionCmd = [
'swww', 'swww',
'img', 'img',
'--invert-y', '--invert-y',
'--transition-type', '--transition-type',
'grow', 'grow',
'--transition-duration', '--transition-duration',
'1.5', '1.5',
'--transition-fps', '--transition-fps',
'30', '30',
'--transition-pos', '--transition-pos',
pos.replace(' ', ''), cursorPosition.replace(' ', ''),
WP, WP,
].join(' '); ].join(' ');
sh(transitionCmd) sh(transitionCmd)
.then(() => { .then(() => {
this.notify('wallpaper'); this.notify('wallpaper');
this.emit('changed', true); this.emit('changed', true);
}) })
.catch((err) => { .catch((err) => {
console.error('Error setting wallpaper:', err); console.error('Error setting wallpaper:', err);
}); });
}) } catch (err) {
.catch((err) => { console.error('Error getting cursor position:', err);
console.error('Error getting cursor position:', err); }
});
} }
async #setWallpaper(path: string): Promise<void> { async #setWallpaper(path: string): Promise<void> {