diff --git a/astal b/astal deleted file mode 120000 index ded1b32..0000000 --- a/astal +++ /dev/null @@ -1 +0,0 @@ -/usr/share/astal/gjs \ No newline at end of file diff --git a/nix/module.nix b/nix/module.nix index 8e2c825..8e440de 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/index.tsx b/src/components/bar/index.tsx index 9439727..aed8c48 100644 --- a/src/components/bar/index.tsx +++ b/src/components/bar/index.tsx @@ -28,7 +28,7 @@ import { import { WidgetContainer } from './shared/WidgetContainer'; import options from 'src/options'; -import { App, Gtk } from 'astal/gtk3/index'; +import { App, Gtk } from 'astal/gtk3'; import Astal from 'gi://Astal?version=3.0'; import { bind, Variable } from 'astal'; diff --git a/src/components/bar/modules/battery/index.tsx b/src/components/bar/modules/battery/index.tsx index 202d2fb..6ceabc1 100644 --- a/src/components/bar/modules/battery/index.tsx +++ b/src/components/bar/modules/battery/index.tsx @@ -5,7 +5,7 @@ import options from 'src/options'; import { BarBoxChild } from 'src/lib/types/bar.js'; import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js'; import Variable from 'astal/variable'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import AstalBattery from 'gi://AstalBattery?version=0.1'; import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers'; import { getBatteryIcon } from './helpers'; diff --git a/src/components/bar/modules/bluetooth/index.tsx b/src/components/bar/modules/bluetooth/index.tsx index a1287a6..473bddc 100644 --- a/src/components/bar/modules/bluetooth/index.tsx +++ b/src/components/bar/modules/bluetooth/index.tsx @@ -3,8 +3,7 @@ import options from 'src/options.js'; import { openMenu } from '../../utils/menu.js'; import { BarBoxChild } from 'src/lib/types/bar.js'; import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js'; -import { bind } from 'astal/binding.js'; -import Variable from 'astal/variable.js'; +import { Variable, bind } from 'astal'; import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js'; import AstalBluetooth from 'gi://AstalBluetooth?version=0.1'; import { Astal } from 'astal/gtk3'; 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/media/index.tsx b/src/components/bar/modules/media/index.tsx index 09fdfc9..7744943 100644 --- a/src/components/bar/modules/media/index.tsx +++ b/src/components/bar/modules/media/index.tsx @@ -3,9 +3,8 @@ import options from 'src/options.js'; import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js'; import { generateMediaLabel } from './helpers/index.js'; import { mprisService } from 'src/lib/constants/services.js'; -import Variable from 'astal/variable.js'; import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { BarBoxChild } from 'src/lib/types/bar.js'; import { Astal } from 'astal/gtk3'; import { activePlayer, mediaAlbum, mediaArtist, mediaTitle } from 'src/globals/media.js'; diff --git a/src/components/bar/modules/menu/index.tsx b/src/components/bar/modules/menu/index.tsx index 1278d51..5870579 100644 --- a/src/components/bar/modules/menu/index.tsx +++ b/src/components/bar/modules/menu/index.tsx @@ -2,8 +2,7 @@ import { runAsyncCommand, throttledScrollHandler } from '../../utils/helpers.js' import options from '../../../../options.js'; import { openMenu } from '../../utils/menu.js'; import { getDistroIcon } from '../../../../lib/utils.js'; -import { bind } from 'astal/binding.js'; -import Variable from 'astal/variable.js'; +import { Variable, bind } from 'astal'; import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js'; import { BarBoxChild } from 'src/lib/types/bar.js'; import { Astal } from 'astal/gtk3'; diff --git a/src/components/bar/modules/systray/index.tsx b/src/components/bar/modules/systray/index.tsx index 2343f6d..f25e141 100644 --- a/src/components/bar/modules/systray/index.tsx +++ b/src/components/bar/modules/systray/index.tsx @@ -4,7 +4,6 @@ import AstalTray from 'gi://AstalTray?version=0.1'; import { bind, Gio, Variable } from 'astal'; import { BarBoxChild } from 'src/lib/types/bar'; import { Gdk, Gtk } from 'astal/gtk3'; -import { BindableChild } from 'astal/gtk3/astalify'; const systemtray = AstalTray.get_default(); const { ignore, customIcons } = options.bar.systray; @@ -137,7 +136,7 @@ interface MenuCustomIconProps { interface MenuEntryProps { item: AstalTray.TrayItem; - child?: BindableChild; + child?: JSX.Element; } export { SysTray }; diff --git a/src/components/bar/modules/volume/index.tsx b/src/components/bar/modules/volume/index.tsx index 96a8dad..db4a034 100644 --- a/src/components/bar/modules/volume/index.tsx +++ b/src/components/bar/modules/volume/index.tsx @@ -2,8 +2,7 @@ import { audioService } from 'src/lib/constants/services.js'; import { openMenu } from '../../utils/menu.js'; import options from 'src/options'; import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js'; -import Variable from 'astal/variable.js'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js'; import { getIcon } from './helpers/index.js'; import { BarBoxChild } from 'src/lib/types/bar.js'; diff --git a/src/components/bar/modules/workspaces/helpers/index.ts b/src/components/bar/modules/workspaces/helpers/index.ts index fd20d2c..1e746ca 100644 --- a/src/components/bar/modules/workspaces/helpers/index.ts +++ b/src/components/bar/modules/workspaces/helpers/index.ts @@ -1,350 +1,364 @@ -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 = ( - 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); +function navigateWorkspace(direction: 'next' | 'prev', 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 = activeWorkspaceIds.sort((a, b) => a - b); - 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(ignoredWorkspacesVariable: Variable): void { + navigateWorkspace('next', 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(ignoredWorkspacesVariable: Variable): void { + navigateWorkspace('prev', 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 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 = ( - scrollSpeed: number, - currentMonitorWorkspaces: Variable, - activeWorkspaces: boolean = true, -): ThrottledScrollHandlers => { +export function initThrottledScrollHandlers(scrollSpeed: number): ThrottledScrollHandlers { const throttledScrollUp = throttle(() => { if (reverse_scroll.get()) { - goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToPreviousWorkspace(ignored); } else { - goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToNextWorkspace(ignored); } }, 200 / scrollSpeed); const throttledScrollDown = throttle(() => { if (reverse_scroll.get()) { - goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToNextWorkspace(ignored); } else { - goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored); + goToPreviousWorkspace(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 +372,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..0520097 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 } from './helpers'; import { BarBoxChild } from 'src/lib/types/bar'; import { WorkspaceModule } from './workspaces'; import { bind, Variable } from 'astal'; @@ -7,15 +7,9 @@ import { GtkWidget } from 'src/lib/types/widget'; import { Astal, Gdk } from 'astal/gtk3'; import { isScrollDown, isScrollUp } from 'src/lib/utils'; -const { workspaces, scroll_speed } = options.bar.workspaces; +const { scroll_speed } = options.bar.workspaces; const Workspaces = (monitor = -1): BarBoxChild => { - const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor)); - - workspaces.subscribe(() => { - currentMonitorWorkspaces.set(getCurrentMonitorWorkspaces(monitor)); - }); - const component = ( @@ -35,18 +29,15 @@ const Workspaces = (monitor = -1): BarBoxChild => { self.disconnect(scrollHandlers); } - const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers( - scroll_speed, - currentMonitorWorkspaces, - ); + const { throttledScrollUp, throttledScrollDown } = initThrottledScrollHandlers(scroll_speed); scrollHandlers = self.connect('scroll-event', (_: GtkWidget, event: Gdk.Event) => { if (isScrollUp(event)) { - throttledScrollDown(); + throttledScrollUp(); } if (isScrollDown(event)) { - throttledScrollUp(); + throttledScrollDown(); } }); }); 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/audio/active/devices/index.tsx b/src/components/menus/audio/active/devices/index.tsx index 68c7689..8b5f981 100644 --- a/src/components/menus/audio/active/devices/index.tsx +++ b/src/components/menus/audio/active/devices/index.tsx @@ -1,4 +1,3 @@ -import { BindableChild } from 'astal/gtk3/astalify'; import { audioService } from 'src/lib/constants/services'; import { SliderItem } from '../sliderItem/SliderItem'; import { ActiveDeviceMenu } from '..'; @@ -21,5 +20,5 @@ export const ActiveDevices = (): JSX.Element => { }; interface ActiveDeviceContainerProps { - children?: BindableChild | BindableChild[]; + children?: JSX.Element[]; } diff --git a/src/components/menus/audio/active/index.tsx b/src/components/menus/audio/active/index.tsx index 470aba1..708429d 100644 --- a/src/components/menus/audio/active/index.tsx +++ b/src/components/menus/audio/active/index.tsx @@ -1,8 +1,7 @@ import { Gtk } from 'astal/gtk3'; import { ActiveDevices } from './devices/index.js'; -import Variable from 'astal/variable.js'; import { ActivePlaybacks } from './playbacks/index.js'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { isPrimaryClick } from 'src/lib/utils.js'; export enum ActiveDeviceMenu { diff --git a/src/components/menus/audio/available/InputDevices.tsx b/src/components/menus/audio/available/InputDevices.tsx index 4c38323..7385cb7 100644 --- a/src/components/menus/audio/available/InputDevices.tsx +++ b/src/components/menus/audio/available/InputDevices.tsx @@ -1,5 +1,5 @@ import { audioService } from 'src/lib/constants/services.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { AudioDevice } from './Device'; import { NotFoundButton } from './NotFoundButton'; diff --git a/src/components/menus/audio/available/PlaybackDevices.tsx b/src/components/menus/audio/available/PlaybackDevices.tsx index 361613b..48c3fce 100644 --- a/src/components/menus/audio/available/PlaybackDevices.tsx +++ b/src/components/menus/audio/available/PlaybackDevices.tsx @@ -1,5 +1,5 @@ import { audioService } from 'src/lib/constants/services.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { AudioDevice } from './Device'; import { NotFoundButton } from './NotFoundButton'; diff --git a/src/components/menus/audio/index.tsx b/src/components/menus/audio/index.tsx index 688bf44..50fa948 100644 --- a/src/components/menus/audio/index.tsx +++ b/src/components/menus/audio/index.tsx @@ -1,7 +1,7 @@ import DropdownMenu from '../shared/dropdown/index.js'; import { VolumeSliders } from './active/index.js'; import options from 'src/options.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { Gtk } from 'astal/gtk3'; import { AvailableDevices } from './available/index.js'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; diff --git a/src/components/menus/bluetooth/devices/index.tsx b/src/components/menus/bluetooth/devices/index.tsx index fbfb1e5..e84bc48 100644 --- a/src/components/menus/bluetooth/devices/index.tsx +++ b/src/components/menus/bluetooth/devices/index.tsx @@ -1,5 +1,4 @@ -import Variable from 'astal/variable.js'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { bluetoothService } from 'src/lib/constants/services.js'; import { getAvailableBluetoothDevices, getConnectedBluetoothDevices } from './helpers.js'; import { NoBluetoothDevices } from './NoBluetoothDevices.js'; diff --git a/src/components/menus/bluetooth/index.tsx b/src/components/menus/bluetooth/index.tsx index 5b48044..362d98c 100644 --- a/src/components/menus/bluetooth/index.tsx +++ b/src/components/menus/bluetooth/index.tsx @@ -2,7 +2,7 @@ import DropdownMenu from '../shared/dropdown/index.js'; import { BluetoothDevices } from './devices/index.js'; import { Header } from './header/index.js'; import options from 'src/options.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { Gtk } from 'astal/gtk3'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; diff --git a/src/components/menus/dashboard/directories/Sections.tsx b/src/components/menus/dashboard/directories/Sections.tsx index 83e1c7a..da7eb83 100644 --- a/src/components/menus/dashboard/directories/Sections.tsx +++ b/src/components/menus/dashboard/directories/Sections.tsx @@ -1,5 +1,3 @@ -import { BindableChild } from 'astal/gtk3/astalify'; - export const LeftSection = ({ children }: SectionProps): JSX.Element => { return ( @@ -17,5 +15,5 @@ export const RightSection = ({ children }: SectionProps): JSX.Element => { }; interface SectionProps { - children?: BindableChild | BindableChild[]; + children?: JSX.Element | JSX.Element[]; } diff --git a/src/components/menus/dashboard/index.tsx b/src/components/menus/dashboard/index.tsx index 1bbea0a..b03c576 100644 --- a/src/components/menus/dashboard/index.tsx +++ b/src/components/menus/dashboard/index.tsx @@ -5,8 +5,7 @@ import { Controls } from './controls/index.js'; import { Stats } from './stats/index.js'; import { Directories } from './directories/index.js'; import options from 'src/options.js'; -import { bind } from 'astal/binding.js'; -import Variable from 'astal/variable.js'; +import { bind, Variable } from 'astal'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; const { controls, shortcuts, stats, directories } = options.menus.dashboard; 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/dashboard/profile/helpers.ts b/src/components/menus/dashboard/profile/helpers.ts index 7260b60..d6d2c97 100644 --- a/src/components/menus/dashboard/profile/helpers.ts +++ b/src/components/menus/dashboard/profile/helpers.ts @@ -1,7 +1,7 @@ import { App } from 'astal/gtk3'; import powermenu from '../../power/helpers/actions.js'; import { PowerOptions } from 'src/lib/types/options.js'; -import { execAsync } from 'astal/process.js'; +import { execAsync } from 'astal'; const { confirmation, shutdown, logout, sleep, reboot } = options.menus.dashboard.powermenu; /** diff --git a/src/components/menus/dashboard/shortcuts/sections/Column.tsx b/src/components/menus/dashboard/shortcuts/sections/Column.tsx index b969eb5..71864a6 100644 --- a/src/components/menus/dashboard/shortcuts/sections/Column.tsx +++ b/src/components/menus/dashboard/shortcuts/sections/Column.tsx @@ -1,5 +1,3 @@ -import { BindableChild } from 'astal/gtk3/astalify'; - export const LeftColumn = ({ isVisible, children }: LeftColumnProps): JSX.Element => { return ( @@ -26,9 +24,9 @@ export const RightColumn = ({ children }: RightColumnProps): JSX.Element => { interface LeftColumnProps { isVisible?: boolean; - children?: BindableChild | BindableChild[]; + children?: JSX.Element | JSX.Element[]; } interface RightColumnProps { - children?: BindableChild | BindableChild[]; + children?: JSX.Element | JSX.Element[]; } diff --git a/src/components/menus/energy/index.tsx b/src/components/menus/energy/index.tsx index bd1381e..42bd098 100644 --- a/src/components/menus/energy/index.tsx +++ b/src/components/menus/energy/index.tsx @@ -2,7 +2,7 @@ import DropdownMenu from '../shared/dropdown/index.js'; import { EnergyProfiles } from './profiles/index.js'; import { Brightness } from './brightness/index.js'; import options from 'src/options.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { Gtk } from 'astal/gtk3'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; diff --git a/src/components/menus/media/components/MediaContainer.tsx b/src/components/menus/media/components/MediaContainer.tsx index e82ee99..336ce73 100644 --- a/src/components/menus/media/components/MediaContainer.tsx +++ b/src/components/menus/media/components/MediaContainer.tsx @@ -1,6 +1,5 @@ import { getBackground } from './helpers.js'; import { Gtk } from 'astal/gtk3'; -import { BindableChild } from 'astal/gtk3/astalify.js'; export const MediaContainer = ({ children }: MediaContainerProps): JSX.Element => { return ( @@ -19,5 +18,5 @@ export const MediaContainer = ({ children }: MediaContainerProps): JSX.Element = }; interface MediaContainerProps { - children?: BindableChild | BindableChild[]; + children?: JSX.Element | JSX.Element[]; } diff --git a/src/components/menus/media/index.tsx b/src/components/menus/media/index.tsx index ea03ed4..8745a04 100644 --- a/src/components/menus/media/index.tsx +++ b/src/components/menus/media/index.tsx @@ -1,4 +1,4 @@ -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import DropdownMenu from '../shared/dropdown/index.js'; import options from 'src/options.js'; import { MediaContainer } from './components/MediaContainer.js'; diff --git a/src/components/menus/network/index.tsx b/src/components/menus/network/index.tsx index 1da4300..f70544c 100644 --- a/src/components/menus/network/index.tsx +++ b/src/components/menus/network/index.tsx @@ -2,7 +2,7 @@ import DropdownMenu from '../shared/dropdown/index.js'; import { Ethernet } from './ethernet/index.js'; import { Wifi } from './wifi/index.js'; import options from 'src/options.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { networkService } from 'src/lib/constants/services.js'; import { NoWifi } from './wifi/WirelessAPs/NoWifi.js'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; diff --git a/src/components/menus/notifications/index.tsx b/src/components/menus/notifications/index.tsx index 62ef4cb..bee12d2 100644 --- a/src/components/menus/notifications/index.tsx +++ b/src/components/menus/notifications/index.tsx @@ -3,9 +3,8 @@ import { Controls } from './controls/index.js'; import { NotificationsContainer } from './notification/index.js'; import { NotificationPager } from './pager/index.js'; import options from 'src/options.js'; -import Variable from 'astal/variable.js'; import { handlePageBoundaries } from './helpers.js'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; const { transition } = options.menus; diff --git a/src/components/menus/notifications/notification/index.tsx b/src/components/menus/notifications/notification/index.tsx index f737b91..b8ce1bf 100644 --- a/src/components/menus/notifications/notification/index.tsx +++ b/src/components/menus/notifications/notification/index.tsx @@ -1,9 +1,8 @@ import options from 'src/options.js'; import { filterNotifications } from 'src/lib/shared/notifications.js'; import AstalNotifd from 'gi://AstalNotifd?version=0.1'; -import Variable from 'astal/variable.js'; import { Gtk } from 'astal/gtk3'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { notifdService } from 'src/lib/constants/services.js'; import { NotificationCard } from 'src/components/notifications/Notification.js'; import { Placeholder } from './Placeholder'; diff --git a/src/components/menus/power/index.tsx b/src/components/menus/power/index.tsx index 420044c..a3d6814 100644 --- a/src/components/menus/power/index.tsx +++ b/src/components/menus/power/index.tsx @@ -4,7 +4,7 @@ import powermenu from './helpers/actions.js'; import options from 'src/options.js'; import { isPrimaryClick } from 'src/lib/utils.js'; import icons from 'src/lib/icons/icons.js'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import { Gtk } from 'astal/gtk3'; import { RevealerTransitionMap } from 'src/lib/constants/options.js'; diff --git a/src/components/menus/power/verification.tsx b/src/components/menus/power/verification.tsx index b6553be..30d717b 100644 --- a/src/components/menus/power/verification.tsx +++ b/src/components/menus/power/verification.tsx @@ -1,7 +1,7 @@ import PopupWindow from '../shared/popup/index.js'; import powermenu from './helpers/actions.js'; import { App, Gtk } from 'astal/gtk3'; -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; export default (): JSX.Element => ( diff --git a/src/components/menus/powerDropdown/index.tsx b/src/components/menus/powerDropdown/index.tsx index fb2b845..3b0e31a 100644 --- a/src/components/menus/powerDropdown/index.tsx +++ b/src/components/menus/powerDropdown/index.tsx @@ -1,4 +1,4 @@ -import { bind } from 'astal/binding.js'; +import { bind } from 'astal'; import DropdownMenu from '../shared/dropdown/index.js'; import { PowerButton } from './button.js'; import options from 'src/options.js'; 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/notifications/Header.tsx b/src/components/notifications/Header.tsx index 11ce0dd..d915e18 100644 --- a/src/components/notifications/Header.tsx +++ b/src/components/notifications/Header.tsx @@ -1,6 +1,6 @@ import AstalNotifd from 'gi://AstalNotifd?version=0.1'; import options from 'src/options.js'; -import { GLib } from 'astal/gobject.js'; +import { GLib } from 'astal'; import { Gtk } from 'astal/gtk3'; import { getNotificationIcon } from 'src/globals/notification.js'; import { notifHasImg } from './helpers'; diff --git a/src/components/notifications/index.tsx b/src/components/notifications/index.tsx index 5e5f373..1f52839 100644 --- a/src/components/notifications/index.tsx +++ b/src/components/notifications/index.tsx @@ -1,8 +1,7 @@ import { hyprlandService } from 'src/lib/constants/services.js'; import options from 'src/options.js'; import { getPosition } from 'src/lib/utils.js'; -import Variable from 'astal/variable.js'; -import { bind } from 'astal/binding.js'; +import { bind, Variable } from 'astal'; import { trackActiveMonitor, trackAutoTimeout, trackPopupNotifications } from './helpers.js'; import { Astal } from 'astal/gtk3'; import { NotificationCard } from './Notification.js'; 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 */}