diff --git a/nix/module.nix b/nix/module.nix index ca84fa2..c539259 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -315,7 +315,6 @@ in bar.workspaces.applicationIconEmptyWorkspace = mkStrOption ""; bar.workspaces.applicationIconFallback = mkStrOption "󰣆"; bar.workspaces.applicationIconOncePerWorkspace = mkBoolOption true; - bar.workspaces.hideUnoccupied = mkBoolOption true; bar.workspaces.icons.active = mkStrOption ""; bar.workspaces.icons.available = mkStrOption ""; bar.workspaces.icons.occupied = mkStrOption ""; diff --git a/src/components/bar/modules/kblayout/helpers/index.ts b/src/components/bar/modules/kblayout/helpers/index.ts index ae5d2aa..c7bc5de 100644 --- a/src/components/bar/modules/kblayout/helpers/index.ts +++ b/src/components/bar/modules/kblayout/helpers/index.ts @@ -13,13 +13,13 @@ import { layoutMap } from './layouts'; * 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. * - * @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'. * * @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 => { - const hyprctlDevices: HyprctlDeviceLayout = JSON.parse(obj); +export const getKeyboardLayout = (layoutData: string, format: KbLabelType): LayoutKeys | LayoutValues => { + const hyprctlDevices: HyprctlDeviceLayout = JSON.parse(layoutData); const keyboards = hyprctlDevices['keyboards']; if (keyboards.length === 0) { diff --git a/src/components/bar/modules/kblayout/index.tsx b/src/components/bar/modules/kblayout/index.tsx index 3e1e753..b9b4ddb 100644 --- a/src/components/bar/modules/kblayout/index.tsx +++ b/src/components/bar/modules/kblayout/index.tsx @@ -4,13 +4,22 @@ import { Module } from '../../shared/Module'; import { inputHandler } from 'src/components/bar/utils/helpers'; import { getKeyboardLayout } from './helpers'; import { BarBoxChild } from 'src/lib/types/bar'; -import { bind, execAsync } from 'astal'; +import { bind } from 'astal'; import { useHook } from 'src/lib/shared/hookHandler'; import { Astal } from 'astal/gtk3'; const { label, labelType, icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } = 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 => { const keyboardModule = Module({ textIcon: bind(icon), @@ -20,25 +29,13 @@ export const KbInput = (): BarBoxChild => { self, hyprlandService, () => { - execAsync('hyprctl devices -j') - .then((obj) => { - self.label = getKeyboardLayout(obj, labelType.get()); - }) - .catch((err) => { - console.error(err); - }); + setLabel(self); }, 'keyboard-layout', ); useHook(self, labelType, () => { - execAsync('hyprctl devices -j') - .then((obj) => { - self.label = getKeyboardLayout(obj, labelType.get()); - }) - .catch((err) => { - console.error(err); - }); + setLabel(self); }); }, boxClass: 'kblayout', diff --git a/src/components/bar/modules/workspaces/helpers/index.ts b/src/components/bar/modules/workspaces/helpers/index.ts index fd20d2c..5a3bc05 100644 --- a/src/components/bar/modules/workspaces/helpers/index.ts +++ b/src/components/bar/modules/workspaces/helpers/index.ts @@ -1,350 +1,384 @@ -import { exec, Variable } from 'astal'; +import { Variable } from 'astal'; import AstalHyprland from 'gi://AstalHyprland?version=0.1'; 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 options from 'src/options'; const { workspaces, reverse_scroll, ignored } = options.bar.workspaces; /** - * Retrieves the workspaces for a specific monitor. - * - * 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. + * A Variable that holds the current map of monitors to the workspace numbers assigned to them. */ -export const getWorkspacesForMonitor = ( - curWs: number, - wsRules: WorkspaceMap, - monitor: number, +export const workspaceRules = Variable(getWorkspaceMonitorMap()); + +/** + * 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[], monitorList: AstalHyprland.Monitor[], -): boolean => { - if (!wsRules || !Object.keys(wsRules).length) { - return true; - } +): boolean { + const monitorNameMap: MonitorMap = {}; + const allWorkspaceInstances = workspaceList ?? []; - const monitorMap: MonitorMap = {}; - - const wsList = workspaceList ?? []; - - const workspaceMonitorList = wsList - .filter((m) => m !== null) - .map((m) => { - return { id: m.monitor?.id, name: m.monitor?.name }; + const workspaceMonitorReferences = allWorkspaceInstances + .filter((workspaceInstance) => workspaceInstance !== null) + .map((workspaceInstance) => { + return { + id: workspaceInstance.monitor?.id, + name: workspaceInstance.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 monitorWSRules = wsRules[currentMonitorName]; + const currentMonitorName = monitorNameMap[monitorId]; + const currentMonitorWorkspaceRules = workspaceMonitorRules[currentMonitorName]; - if (monitorWSRules === undefined) { - return true; + if (currentMonitorWorkspaceRules === undefined) { + 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 { - 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) => { - const workspaceNum = parseInt(rule.workspaceString, 10); - if (isNaN(workspaceNum)) { + if (rule.monitor === undefined || isNaN(workspaceNumber)) { 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 { - workspaceRules[rule.monitor] = [workspaceNum]; + workspaceMonitorRules[rule.monitor] = [workspaceNumber]; } }); - return workspaceRules; - } catch (err) { - console.error(err); + return workspaceMonitorRules; + } catch (error) { + console.error(error); 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 The list of workspace numbers. + * @returns `true` if the workspace should be ignored, otherwise `false`. */ -export const getCurrentMonitorWorkspaces = (monitor: number): number[] => { - if (hyprlandService.get_monitors().length === 1) { - return Array.from({ length: workspaces.get() }, (_, i) => i + 1); +function isWorkspaceIgnored(ignoredWorkspacesVariable: Variable, workspaceNumber: number): boolean { + if (ignoredWorkspacesVariable.get() === '') { + return false; } - const monitorWorkspaces = getWorkspaceRules(); - const monitorMap: MonitorMap = {}; - hyprlandService.get_monitors().forEach((m) => (monitorMap[m.id] = m.name)); - - const currentMonitorName = monitorMap[monitor]; - - return monitorWorkspaces[currentMonitorName]; -}; + const ignoredWorkspacesRegex = new RegExp(ignoredWorkspacesVariable.get()); + return ignoredWorkspacesRegex.test(workspaceNumber.toString()); +} /** - * 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. - * - * @param ignoredWorkspaces - The ignored workspaces variable. - * @param workspaceNumber - The workspace number. - * - * @returns Whether the workspace is ignored. - */ -export const isWorkspaceIgnored = (ignoredWorkspaces: Variable, 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. + * 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 + * workspaces, skipping any that match the ignored pattern. * * @param direction - The direction to navigate ('next' or 'prev'). - * @param currentMonitorWorkspaces - The current monitor's workspaces variable. - * @param activeWorkspaces - Whether to consider only active workspaces. - * @param ignoredWorkspaces - The ignored workspaces variable. + * @param currentMonitorWorkspacesVariable - A Variable containing an array of workspace numbers for the current monitor. + * @param onlyActiveWorkspaces - Whether to only include active (occupied) workspaces when navigating. + * @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern. */ -const navigateWorkspace = ( +function navigateWorkspace( direction: 'next' | 'prev', - currentMonitorWorkspaces: Variable, - activeWorkspaces: boolean, - ignoredWorkspaces: Variable, -): void => { - const hyprlandWorkspaces = hyprlandService.get_workspaces() || []; - const occupiedWorkspaces = hyprlandWorkspaces - .filter((ws) => hyprlandService.focusedMonitor.id === ws.monitor?.id) - .map((ws) => ws.id); + currentMonitorWorkspacesVariable: Variable, + onlyActiveWorkspaces: boolean, + ignoredWorkspacesVariable: Variable, +): void { + const allHyprlandWorkspaces = hyprlandService.get_workspaces() || []; - const workspacesList = activeWorkspaces - ? occupiedWorkspaces - : currentMonitorWorkspaces.get() || Array.from({ length: workspaces.get() }, (_, i) => i + 1); + const activeWorkspaceIds = allHyprlandWorkspaces + .filter((workspaceInstance) => hyprlandService.focusedMonitor.id === workspaceInstance.monitor?.id) + .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; - let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length; + + let newIndex = (workspaceIndex + step + assignedOrOccupiedWorkspaces.length) % assignedOrOccupiedWorkspaces.length; let attempts = 0; - while (attempts < workspacesList.length) { - const targetWS = workspacesList[newIndex]; - if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) { - hyprlandService.dispatch('workspace', targetWS.toString()); + while (attempts < assignedOrOccupiedWorkspaces.length) { + const targetWorkspaceNumber = assignedOrOccupiedWorkspaces[newIndex]; + if (!isWorkspaceIgnored(ignoredWorkspacesVariable, targetWorkspaceNumber)) { + hyprlandService.dispatch('workspace', targetWorkspaceNumber.toString()); return; } - newIndex = (newIndex + step + workspacesList.length) % workspacesList.length; + newIndex = (newIndex + step + assignedOrOccupiedWorkspaces.length) % assignedOrOccupiedWorkspaces.length; 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 currentMonitorWorkspaces - The current monitor's workspaces variable. - * @param activeWorkspaces - Whether to consider only active workspaces. - * @param ignoredWorkspaces - The ignored workspaces variable. + * @param currentMonitorWorkspacesVariable - A Variable containing workspace numbers for the current monitor. + * @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces. + * @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern. */ -export const goToNextWS = ( - currentMonitorWorkspaces: Variable, - activeWorkspaces: boolean, - ignoredWorkspaces: Variable, -): void => { - navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces); -}; +export function goToNextWorkspace( + currentMonitorWorkspacesVariable: Variable, + onlyActiveWorkspaces: boolean, + ignoredWorkspacesVariable: Variable, +): void { + 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 currentMonitorWorkspaces - The current monitor's workspaces variable. - * @param activeWorkspaces - Whether to consider only active workspaces. - * @param ignoredWorkspaces - The ignored workspaces variable. + * @param currentMonitorWorkspacesVariable - A Variable containing workspace numbers for the current monitor. + * @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces. + * @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern. */ -export const goToPrevWS = ( - currentMonitorWorkspaces: Variable, - activeWorkspaces: boolean, - ignoredWorkspaces: Variable, -): void => { - navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces); -}; +export function goToPreviousWorkspace( + currentMonitorWorkspacesVariable: Variable, + onlyActiveWorkspaces: boolean, + ignoredWorkspacesVariable: Variable, +): void { + 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. - * @param limit - The time limit in milliseconds. - * - * @returns The throttled function. + * @returns The throttled version of the input function. */ export function throttle void>(func: T, limit: number): T { - let inThrottle: boolean; + let isThrottleActive: boolean; + return function (this: ThisParameterType, ...args: Parameters) { - if (!inThrottle) { + if (!isThrottleActive) { func.apply(this, args); - inThrottle = true; + isThrottleActive = true; + setTimeout(() => { - inThrottle = false; + isThrottleActive = false; }, limit); } } 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. - * @param currentMonitorWorkspaces - The current monitor's workspaces variable. - * @param activeWorkspaces - Whether to consider only active workspaces. - * - * @returns The throttled scroll handlers. + * @returns An object containing two functions (`throttledScrollUp` and `throttledScrollDown`), both throttled. */ -export const createThrottledScrollHandlers = ( +export function initThrottledScrollHandlers( scrollSpeed: number, - currentMonitorWorkspaces: Variable, - activeWorkspaces: boolean = true, -): ThrottledScrollHandlers => { + currentMonitorWorkspacesVariable: Variable, + onlyActiveWorkspaces: boolean = true, +): ThrottledScrollHandlers { const throttledScrollUp = throttle(() => { if (reverse_scroll.get()) { - goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToPreviousWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored); } else { - goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToNextWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored); } }, 200 / scrollSpeed); const throttledScrollDown = throttle(() => { if (reverse_scroll.get()) { - goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToNextWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored); } else { - goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToPreviousWorkspace(currentMonitorWorkspacesVariable, onlyActiveWorkspaces, ignored); } }, 200 / scrollSpeed); 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 workspaceList - The list of workspaces. - * @param workspaceRules - The workspace rules map. - * @param monitor - The monitor ID. - * @param isMonitorSpecific - Whether the workspaces are monitor-specific. - * @param monitorList - The list of monitors. + * @param totalWorkspaces - The total number of workspaces (a fallback if workspace rules are not enforced). + * @param workspaceInstances - A list of Hyprland workspace objects. + * @param workspaceMonitorRules - The map of monitor names to assigned workspace numbers. + * @param monitorId - The numeric identifier of the monitor. + * @param isMonitorSpecific - If `true`, only include the workspaces that match this monitor. + * @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, - workspaceList: AstalHyprland.Workspace[], - workspaceRules: WorkspaceMap, - monitor: number, + workspaceInstances: AstalHyprland.Workspace[], + workspaceMonitorRules: WorkspaceMonitorMap, + monitorId: number, isMonitorSpecific: boolean, - monitorList: AstalHyprland.Monitor[], -): number[] => { - let allWorkspaces = range(totalWorkspaces || 8); - const activeWorkspaces = workspaceList.map((ws) => ws.id); + hyprlandMonitorInstances: AstalHyprland.Monitor[], +): number[] { + let allPotentialWorkspaces = range(totalWorkspaces || 8); + const allWorkspaceInstances = workspaceInstances ?? []; - const wsList = workspaceList ?? []; - const workspaceMonitorList = wsList.map((ws) => { + const activeWorkspaceIds = allWorkspaceInstances.map((workspaceInstance) => workspaceInstance.id); + + const monitorReferencesForActiveWorkspaces = allWorkspaceInstances.map((workspaceInstance) => { return { - id: ws.monitor?.id || -1, - name: ws.monitor?.name || '', + id: workspaceInstance.monitor?.id ?? -1, + name: workspaceInstance.monitor?.name ?? '', }; }); - const curMonitor = - monitorList.find((mon) => mon.id === monitor) || workspaceMonitorList.find((mon) => mon.id === monitor); + const currentMonitorInstance = + hyprlandMonitorInstances.find((monitorObj) => monitorObj.id === monitorId) || + monitorReferencesForActiveWorkspaces.find((monitorObj) => monitorObj.id === monitorId); - const workspacesWithRules = Object.keys(workspaceRules).reduce((acc: number[], k: string) => { - return [...acc, ...workspaceRules[k]]; - }, []); + const allWorkspacesWithRules = Object.keys(workspaceMonitorRules).reduce( + (accumulator: number[], monitorName: string) => { + return [...accumulator, ...workspaceMonitorRules[monitorName]]; + }, + [], + ); - const activesForMonitor = activeWorkspaces.filter((w) => { + const activeWorkspacesForCurrentMonitor = activeWorkspaceIds.filter((workspaceId) => { if ( - curMonitor && - Object.hasOwnProperty.call(workspaceRules, curMonitor.name) && - workspacesWithRules.includes(w) + currentMonitorInstance && + Object.hasOwnProperty.call(workspaceMonitorRules, currentMonitorInstance.name) && + 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) { - const workspacesInRange = range(totalWorkspaces).filter((ws) => { - return getWorkspacesForMonitor(ws, workspaceRules, monitor, wsList, monitorList); + const validWorkspaceNumbers = range(totalWorkspaces).filter((workspaceNumber) => { + return isWorkspaceValidForMonitor( + workspaceNumber, + workspaceMonitorRules, + monitorId, + allWorkspaceInstances, + hyprlandMonitorInstances, + ); }); - allWorkspaces = [...new Set([...activesForMonitor, ...workspacesInRange])]; + allPotentialWorkspaces = [...new Set([...activeWorkspacesForCurrentMonitor, ...validWorkspaceNumbers])]; } 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. - * This variable holds the current workspace rules. + * Subscribes to Hyprland service events related to workspaces to keep the local state updated. + * + * 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()); - -/** - * 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 => { +export function initWorkspaceEvents(): void { hyprlandService.connect('config-reloaded', () => { - workspaceRules.set(getWorkspaceRules()); + workspaceRules.set(getWorkspaceMonitorMap()); }); hyprlandService.connect('client-moved', () => { @@ -358,9 +392,19 @@ export const setupConnections = (): void => { hyprlandService.connect('client-removed', () => { forceUpdater.set(!forceUpdater.get()); }); -}; +} +/** + * Throttled scroll handler functions for navigating workspaces. + */ type ThrottledScrollHandlers = { + /** + * Scroll up throttled handler. + */ throttledScrollUp: () => void; + + /** + * Scroll down throttled handler. + */ throttledScrollDown: () => void; }; diff --git a/src/components/bar/modules/workspaces/index.tsx b/src/components/bar/modules/workspaces/index.tsx index eeb7196..cf94135 100644 --- a/src/components/bar/modules/workspaces/index.tsx +++ b/src/components/bar/modules/workspaces/index.tsx @@ -1,5 +1,5 @@ import options from 'src/options'; -import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers'; +import { initThrottledScrollHandlers, getWorkspacesForMonitor } from './helpers'; import { BarBoxChild } from 'src/lib/types/bar'; import { WorkspaceModule } from './workspaces'; 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 = (monitor = -1): BarBoxChild => { - const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor)); + const currentMonitorWorkspaces = Variable(getWorkspacesForMonitor(monitor)); workspaces.subscribe(() => { - currentMonitorWorkspaces.set(getCurrentMonitorWorkspaces(monitor)); + currentMonitorWorkspaces.set(getWorkspacesForMonitor(monitor)); }); const component = ( @@ -35,7 +35,7 @@ const Workspaces = (monitor = -1): BarBoxChild => { self.disconnect(scrollHandlers); } - const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers( + const { throttledScrollUp, throttledScrollDown } = initThrottledScrollHandlers( scroll_speed, currentMonitorWorkspaces, ); diff --git a/src/components/bar/modules/workspaces/workspaces.tsx b/src/components/bar/modules/workspaces/workspaces.tsx index d630aab..8a1bca7 100644 --- a/src/components/bar/modules/workspaces/workspaces.tsx +++ b/src/components/bar/modules/workspaces/workspaces.tsx @@ -1,6 +1,6 @@ import { hyprlandService } from 'src/lib/constants/services'; 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 { ApplicationIcons, WorkspaceIconMap } from 'src/lib/types/workspace'; import { bind, Variable } from 'astal'; @@ -30,7 +30,7 @@ const { available, active, occupied } = options.bar.workspaces.icons; const { matugen } = options.theme; const { smartHighlight } = options.theme.bar.buttons.workspaces; -setupConnections(); +initWorkspaceEvents(); export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element => { const boxChildren = Variable.derive( @@ -98,10 +98,6 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element ); return workspacesToRender.map((wsId, index) => { - if (isWorkspaceIgnored(ignored, wsId)) { - return ; - } - const appIcons = displayApplicationIcons ? getAppIcon(wsId, appIconOncePerWorkspace, { iconMap: applicationIconMapping, diff --git a/src/components/menus/dashboard/profile/Profile.tsx b/src/components/menus/dashboard/profile/Profile.tsx index cb2d175..cd44a3e 100644 --- a/src/components/menus/dashboard/profile/Profile.tsx +++ b/src/components/menus/dashboard/profile/Profile.tsx @@ -1,4 +1,4 @@ -import { bind, exec } from 'astal'; +import { bind, GLib } from 'astal'; import { Gtk } from 'astal/gtk3'; import options from 'src/options.js'; import { normalizePath, isAnImage } from 'src/lib/utils.js'; @@ -28,7 +28,8 @@ const ProfileName = (): JSX.Element => { halign={Gtk.Align.CENTER} label={bind(name).as((profileName) => { if (profileName === 'system') { - return exec('bash -c whoami'); + const username = GLib.get_user_name(); + return username; } return profileName; })} diff --git a/src/components/menus/shared/dropdown/locationHandler/index.ts b/src/components/menus/shared/dropdown/locationHandler/index.ts index 9d4a02f..74b9603 100644 --- a/src/components/menus/shared/dropdown/locationHandler/index.ts +++ b/src/components/menus/shared/dropdown/locationHandler/index.ts @@ -1,97 +1,215 @@ -import AstalHyprland from 'gi://AstalHyprland?version=0.1'; - import options from 'src/options'; -import { bash } from 'src/lib/utils'; import { globalEventBoxes } from 'src/globals/dropdown'; import { GLib } from 'astal'; import { EventBox } from 'astal/gtk3/widget'; - -const hyprland = AstalHyprland.get_default(); +import { hyprlandService } from 'src/lib/constants/services'; +import AstalHyprland from 'gi://AstalHyprland?version=0.1'; const { location } = options.theme.bar; const { scalingPriority } = options; -export const calculateMenuPosition = async (pos: number[], windowName: string): Promise => { +/** + * 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 => { try { - const self = globalEventBoxes.get()[windowName] as EventBox; + const dropdownEventBox = getDropdownEventBox(windowName); - const curHyprlandMonitor = hyprland.get_monitors().find((m) => m.id === hyprland.focusedMonitor.id); - - 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) { + if (!dropdownEventBox) { return; } - // If GDK Scaling is applied, then get divide width by scaling - // 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'; + const focusedHyprlandMonitor = getFocusedHyprlandMonitor(); - if (scalingPriority.get() === 'both') { - const scale = parseFloat(gdkScale); - 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 (!focusedHyprlandMonitor) { + return; } - // If monitor is vertical (transform = 1 || 3) swap height and width - const isVertical = curHyprlandMonitor?.transform !== undefined ? curHyprlandMonitor.transform % 2 !== 0 : false; + const dropdownWidth = dropdownEventBox.get_child()?.get_allocation().width ?? 0; + const dropdownHeight = dropdownEventBox.get_child()?.get_allocation().height ?? 0; - if (isVertical) { - [monWidth, monHeight] = [monHeight, monWidth]; + const monitorScaling = focusedHyprlandMonitor.scale || 1; + const { width: rawMonitorWidth, height: rawMonitorHeight, transform } = focusedHyprlandMonitor; + + if (!rawMonitorWidth || !rawMonitorHeight) { + return; } - let marginRight = monWidth - dropdownWidth / 2; - marginRight = marginRight - pos[0]; - let marginLeft = monWidth - dropdownWidth - marginRight; + const { adjustedWidth, adjustedHeight } = applyMonitorScaling( + rawMonitorWidth, + rawMonitorHeight, + monitorScaling, + ); - const minimumMargin = 0; + const isVertical = transform !== undefined ? transform % 2 !== 0 : false; + const { finalWidth, finalHeight } = adjustForVerticalTransform(adjustedWidth, adjustedHeight, isVertical); - if (marginRight < minimumMargin) { - marginRight = minimumMargin; - marginLeft = monWidth - dropdownWidth - minimumMargin; - } + const { leftMargin, rightMargin } = calculateHorizontalMargins( + finalWidth, + dropdownWidth, + positionCoordinates[0], + ); - if (marginLeft < minimumMargin) { - marginLeft = minimumMargin; - marginRight = monWidth - dropdownWidth - minimumMargin; - } + dropdownEventBox.set_margin_left(leftMargin); + dropdownEventBox.set_margin_right(rightMargin); - self.set_margin_left(marginLeft); - self.set_margin_right(marginRight); - - 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}`); + setVerticalPosition(dropdownEventBox, finalHeight, dropdownHeight); + } catch (caughtError) { + console.error(`Error getting menu position: ${caughtError}`); } }; + +type HorizontalMargins = { + leftMargin: number; + rightMargin: number; +}; + +type MonitorScaling = { + adjustedWidth: number; + adjustedHeight: number; +}; + +type TransformedDimensions = { + finalWidth: number; + finalHeight: number; +}; diff --git a/src/components/settings/pages/config/bar/index.tsx b/src/components/settings/pages/config/bar/index.tsx index 772751c..e3c5c02 100644 --- a/src/components/settings/pages/config/bar/index.tsx +++ b/src/components/settings/pages/config/bar/index.tsx @@ -140,12 +140,71 @@ export const BarSettings = (): JSX.Element => { {/* Workspaces Section */}