Replaced the underlying structure of Hypridle inhibitor to the Astal Idle Inhibitor module. (#776)

* Update hypridle module to be an all-purpose idle inhibitor module using Astal inhibiting.

* Fix types and module code.

* Update note

* Removed lingering comment.

* Remove hypridle from dependencies.
This commit is contained in:
Jas Singh
2025-02-16 12:20:29 -08:00
committed by GitHub
parent 867b2a7d68
commit 3698bfe2b3
10 changed files with 98 additions and 103 deletions

View File

@@ -67,9 +67,6 @@ hyprpicker
## To enable hyprland's very own blue light filter ## To enable hyprland's very own blue light filter
hyprsunset hyprsunset
## To enable hyprland's very own idle inhibitor
hypridle
## To click resource/stat bars in the dashboard and open btop ## To click resource/stat bars in the dashboard and open btop
btop btop
@@ -91,7 +88,7 @@ pacman:
AUR: AUR:
```bash ```bash
yay -S --needed aylurs-gtk-shell-git grimblast-git gpu-screen-recorder-git hyprpicker matugen-bin python-gpustat hyprsunset-git hypridle-git yay -S --needed aylurs-gtk-shell-git grimblast-git gpu-screen-recorder-git hyprpicker matugen-bin python-gpustat hyprsunset-git
``` ```
### Fedora ### Fedora

View File

@@ -71,6 +71,44 @@ export const utilityCommands: Command[] = [
} }
}, },
}, },
{
name: 'isInhibiting',
aliases: ['isi'],
description: 'Returns the status of the Idle Inhibitor.',
category: 'Utility',
args: [],
handler: (): boolean => {
try {
return idleInhibit.get();
} catch (error) {
errorHandler(error);
}
},
},
{
name: 'idleInhibit',
aliases: ['idi'],
description: 'Enables/Disables the Idle Inhibitor. Toggles the Inhibitor if no parameter is provided.',
category: 'Utility',
args: [
{
name: 'shouldInhibit',
description: 'The boolean value that enables/disables the inhibitor.',
type: 'boolean',
required: false,
},
],
handler: (args: Record<string, unknown>): boolean => {
try {
const shouldInhibit = args['shouldInhibit'] ?? !idleInhibit.get();
idleInhibit.set(Boolean(shouldInhibit));
return idleInhibit.get();
} catch (error) {
errorHandler(error);
}
},
},
{ {
name: 'migrateConfig', name: 'migrateConfig',
aliases: ['mcfg'], aliases: ['mcfg'],

View File

@@ -127,6 +127,7 @@ export const Bar = (() => {
return ( return (
<window <window
inhibit={bind(idleInhibit)}
name={`bar-${hyprlandMonitor}`} name={`bar-${hyprlandMonitor}`}
namespace={`bar-${hyprlandMonitor}`} namespace={`bar-${hyprlandMonitor}`}
className={'bar'} className={'bar'}

View File

@@ -1,55 +0,0 @@
import { execAsync, Variable } from 'astal';
/**
* Checks if the hypridle process is active.
*
* This command checks if the hypridle process is currently running by using the `pgrep` command.
* It returns 'yes' if the process is found and 'no' otherwise.
*/
export const isActiveCommand = `bash -c "pgrep -x 'hypridle' &>/dev/null && echo 'yes' || echo 'no'"`;
/**
* A variable to track the active state of the hypridle process.
*/
export const isActive = Variable(false);
/**
* Updates the active state of the hypridle process.
*
* This function checks if the hypridle process is currently running and updates the `isActive` variable accordingly.
*
* @param isActive A Variable<boolean> that tracks the active state of the hypridle process.
*/
const updateIsActive = (isActive: Variable<boolean>): void => {
execAsync(isActiveCommand).then((res) => {
isActive.set(res === 'yes');
});
};
/**
* Toggles the hypridle process on or off based on its current state.
*
* This function checks if the hypridle process is currently running. If it is not running, it starts the process.
* If it is running, it stops the process. The active state is updated accordingly.
*
* @param isActive A Variable<boolean> that tracks the active state of the hypridle process.
*/
export const toggleIdle = (isActive: Variable<boolean>): void => {
execAsync(isActiveCommand).then((res) => {
const toggleIdleCommand =
res === 'no' ? `bash -c "nohup hypridle > /dev/null 2>&1 &"` : `bash -c "pkill hypridle"`;
execAsync(toggleIdleCommand).then(() => updateIsActive(isActive));
});
};
/**
* Checks the current status of the hypridle process and updates the active state.
*
* This function checks if the hypridle process is currently running and updates the `isActive` variable accordingly.
*/
export const checkIdleStatus = (): undefined => {
execAsync(isActiveCommand).then((res) => {
isActive.set(res === 'yes');
});
};

View File

@@ -1,37 +1,33 @@
import options from 'src/options'; import options from 'src/options';
import { Module } from '../../shared/Module'; import { Module } from '../../shared/Module';
import { inputHandler, throttleInput } from '../../utils/helpers'; import { inputHandler } from '../../utils/helpers';
import { checkIdleStatus, isActive, toggleIdle } from './helpers';
import { FunctionPoller } from '../../../../lib/poller/FunctionPoller';
import Variable from 'astal/variable'; import Variable from 'astal/variable';
import { bind } from 'astal'; import { bind } from 'astal';
import { BarBoxChild } from 'src/lib/types/bar'; import { BarBoxChild } from 'src/lib/types/bar';
import { Astal } from 'astal/gtk3'; import { Astal } from 'astal/gtk3';
const { label, pollingInterval, onIcon, offIcon, onLabel, offLabel, rightClick, middleClick, scrollUp, scrollDown } = const { label, onIcon, offIcon, onLabel, offLabel, rightClick, middleClick, scrollUp, scrollDown } =
options.bar.customModules.hypridle; options.bar.customModules.hypridle;
const dummyVar = Variable(undefined); function toggleInhibit(): void {
idleInhibit.set(!idleInhibit.get());
checkIdleStatus(); }
const idleStatusPoller = new FunctionPoller<undefined, []>(dummyVar, [], bind(pollingInterval), checkIdleStatus);
idleStatusPoller.initialize('hypridle');
const throttledToggleIdle = throttleInput(() => toggleIdle(isActive), 1000);
export const Hypridle = (): BarBoxChild => { export const Hypridle = (): BarBoxChild => {
const iconBinding = Variable.derive([bind(isActive), bind(onIcon), bind(offIcon)], (active, onIcn, offIcn) => { const iconBinding = Variable.derive([bind(idleInhibit), bind(onIcon), bind(offIcon)], (active, onIcn, offIcn) => {
return active ? onIcn : offIcn; return active ? onIcn : offIcn;
}); });
const labelBinding = Variable.derive([bind(isActive), bind(onLabel), bind(offLabel)], (active, onLbl, offLbl) => {
const labelBinding = Variable.derive(
[bind(idleInhibit), bind(onLabel), bind(offLabel)],
(active, onLbl, offLbl) => {
return active ? onLbl : offLbl; return active ? onLbl : offLbl;
}); },
);
const hypridleModule = Module({ const hypridleModule = Module({
textIcon: iconBinding(), textIcon: iconBinding(),
tooltipText: bind(isActive).as((active) => `Hypridle ${active ? 'enabled' : 'disabled'}`), tooltipText: bind(idleInhibit).as((active) => `Idle Inhibitor: ${active ? 'Enabled' : 'Disabled'}`),
boxClass: 'hypridle', boxClass: 'hypridle',
label: labelBinding(), label: labelBinding(),
showLabelBinding: bind(label), showLabelBinding: bind(label),
@@ -40,7 +36,7 @@ export const Hypridle = (): BarBoxChild => {
inputHandler(self, { inputHandler(self, {
onPrimaryClick: { onPrimaryClick: {
fn: () => { fn: () => {
throttledToggleIdle(); toggleInhibit();
}, },
}, },
onSecondaryClick: { onSecondaryClick: {

View File

@@ -1,6 +1,6 @@
import { ResourceLabelType } from 'src/lib/types/bar'; import { ResourceLabelType } from 'src/lib/types/bar';
import { GenericResourceData, Postfix, UpdateHandlers } from 'src/lib/types/customModules/generic'; import { GenericResourceData, Postfix, UpdateHandlers } from 'src/lib/types/customModules/generic';
import { InputHandlerEvents, RunAsyncCommand } from 'src/lib/types/customModules/utils'; import { InputHandlerEventArgs, InputHandlerEvents, RunAsyncCommand } from 'src/lib/types/customModules/utils';
import { ThrottleFn } from 'src/lib/types/utils'; import { ThrottleFn } from 'src/lib/types/utils';
import { bind, Binding, execAsync, Variable } from 'astal'; import { bind, Binding, execAsync, Variable } from 'astal';
import { openMenu } from 'src/components/bar/utils/menu'; import { openMenu } from 'src/components/bar/utils/menu';
@@ -56,6 +56,15 @@ export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdat
.catch((err) => console.error(`Error running command "${cmd}": ${err})`)); .catch((err) => console.error(`Error running command "${cmd}": ${err})`));
}; };
/*
* NOTE: Added a throttle since spamming a button yields duplicate events
* which undo the toggle.
*/
const throttledAsyncCommand = throttleInput(
(cmd, events, fn, postInputUpdater?: Variable<boolean>) => runAsyncCommand(cmd, events, fn, postInputUpdater),
50,
);
/** /**
* Generic throttle function to limit the rate at which a function can be called. * Generic throttle function to limit the rate at which a function can be called.
* *
@@ -90,7 +99,7 @@ export function throttleInput<T extends ThrottleFn>(func: T, limit: number): T {
*/ */
export const throttledScrollHandler = (interval: number): ThrottleFn => export const throttledScrollHandler = (interval: number): ThrottleFn =>
throttleInput((cmd: string, args, fn, postInputUpdater) => { throttleInput((cmd: string, args, fn, postInputUpdater) => {
runAsyncCommand(cmd, args, fn, postInputUpdater); throttledAsyncCommand(cmd, args, fn, postInputUpdater);
}, 200 / interval); }, 200 / interval);
/** /**
@@ -114,7 +123,7 @@ export const inputHandler = (
}: InputHandlerEvents, }: InputHandlerEvents,
postInputUpdater?: Variable<boolean>, postInputUpdater?: Variable<boolean>,
): void => { ): void => {
const sanitizeInput = (input: Variable<string>): string => { const sanitizeInput = (input?: Variable<string> | Variable<string>): string => {
if (input === undefined) { if (input === undefined) {
return ''; return '';
} }
@@ -126,34 +135,34 @@ export const inputHandler = (
const throttledHandler = throttledScrollHandler(interval); const throttledHandler = throttledScrollHandler(interval);
const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => { const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
runAsyncCommand( throttledAsyncCommand(
sanitizeInput(onPrimaryClickInput?.cmd || dummyVar), sanitizeInput(onPrimaryClickInput?.cmd || dummyVar),
{ clicked, event }, { clicked, event },
onPrimaryClickInput.fn, onPrimaryClickInput?.fn,
postInputUpdater, postInputUpdater,
); );
}); });
const disconnectSecondaryClick = onSecondaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => { const disconnectSecondaryClick = onSecondaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
runAsyncCommand( throttledAsyncCommand(
sanitizeInput(onSecondaryClickInput?.cmd || dummyVar), sanitizeInput(onSecondaryClickInput?.cmd || dummyVar),
{ clicked, event }, { clicked, event },
onSecondaryClickInput.fn, onSecondaryClickInput?.fn,
postInputUpdater, postInputUpdater,
); );
}); });
const disconnectMiddleClick = onMiddleClick(self, (clicked: GtkWidget, event: Gdk.Event) => { const disconnectMiddleClick = onMiddleClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
runAsyncCommand( throttledAsyncCommand(
sanitizeInput(onMiddleClickInput?.cmd || dummyVar), sanitizeInput(onMiddleClickInput?.cmd || dummyVar),
{ clicked, event }, { clicked, event },
onMiddleClickInput.fn, onMiddleClickInput?.fn,
postInputUpdater, postInputUpdater,
); );
}); });
const id = self.connect('scroll-event', (self: GtkWidget, event: Gdk.Event) => { const id = self.connect('scroll-event', (self: GtkWidget, event: Gdk.Event) => {
const handleScroll = (input?: { cmd: Variable<string>; fn: (output: string) => void }): void => { const handleScroll = (input?: InputHandlerEventArgs): void => {
if (input) { if (input) {
throttledHandler(sanitizeInput(input.cmd), { clicked: self, event }, input.fn, postInputUpdater); throttledHandler(sanitizeInput(input.cmd), { clicked: self, event }, input.fn, postInputUpdater);
} }
@@ -178,8 +187,8 @@ export const inputHandler = (
updateHandlers(); updateHandlers();
const sanitizeVariable = (someVar: Variable<string> | undefined): Binding<string> => { const sanitizeVariable = (someVar?: Variable<string>): Binding<string> => {
if (someVar === undefined || typeof someVar.bind !== 'function') { if (someVar === undefined) {
return bind(dummyVar); return bind(dummyVar);
} }
return bind(someVar); return bind(someVar);
@@ -188,11 +197,11 @@ export const inputHandler = (
Variable.derive( Variable.derive(
[ [
bind(scrollSpeed), bind(scrollSpeed),
sanitizeVariable(onPrimaryClickInput), sanitizeVariable(onPrimaryClickInput?.cmd),
sanitizeVariable(onSecondaryClickInput), sanitizeVariable(onSecondaryClickInput?.cmd),
sanitizeVariable(onMiddleClickInput), sanitizeVariable(onMiddleClickInput?.cmd),
sanitizeVariable(onScrollUpInput), sanitizeVariable(onScrollUpInput?.cmd),
sanitizeVariable(onScrollDownInput), sanitizeVariable(onScrollDownInput?.cmd),
], ],
() => { () => {
const handlers = updateHandlers(); const handlers = updateHandlers();

1
src/globals.d.ts vendored
View File

@@ -15,6 +15,7 @@ declare global {
var globalWeatherVar: Variable<Weather>; var globalWeatherVar: Variable<Weather>;
var options: Options; var options: Options;
var removingNotifications: Variable<boolean>; var removingNotifications: Variable<boolean>;
var idleInhibit: Variable<boolean>;
} }
export {}; export {};

View File

@@ -1,6 +1,7 @@
import { App } from 'astal/gtk3'; import { App } from 'astal/gtk3';
import options from '../options'; import options from '../options';
import { BarLayouts } from 'src/lib/types/options'; import { BarLayouts } from 'src/lib/types/options';
import { Variable } from 'astal';
globalThis.isWindowVisible = (windowName: string): boolean => { globalThis.isWindowVisible = (windowName: string): boolean => {
const appWindow = App.get_window(windowName); const appWindow = App.get_window(windowName);
@@ -22,3 +23,5 @@ globalThis.setLayout = (layout: BarLayouts): string => {
return `Failed to set layout: ${error}`; return `Failed to set layout: ${error}`;
} }
}; };
globalThis.idleInhibit = Variable(false);

View File

@@ -1,12 +1,17 @@
import { Variable } from 'astal';
import { Opt } from 'src/lib/option';
import { Binding } from 'src/lib/utils'; import { Binding } from 'src/lib/utils';
import { Variable } from 'types/variable';
export type InputHandlerEventArgs = {
cmd?: Opt<string> | Variable<string>;
fn?: (output: string) => void;
};
export type InputHandlerEvents = { export type InputHandlerEvents = {
onPrimaryClick?: Binding; onPrimaryClick?: InputHandlerEventArgs;
onSecondaryClick?: Binding; onSecondaryClick?: InputHandlerEventArgs;
onMiddleClick?: Binding; onMiddleClick?: InputHandlerEventArgs;
onScrollUp?: Binding; onScrollUp?: InputHandlerEventArgs;
onScrollDown?: Binding; onScrollDown?: InputHandlerEventArgs;
}; };
export type RunAsyncCommand = ( export type RunAsyncCommand = (

View File

@@ -1172,8 +1172,8 @@ const options = mkOptions(CONFIG, {
}, },
hypridle: { hypridle: {
label: opt(true), label: opt(true),
onIcon: opt(''), onIcon: opt('󰒳'),
offIcon: opt(''), offIcon: opt('󰒲'),
onLabel: opt('On'), onLabel: opt('On'),
offLabel: opt('Off'), offLabel: opt('Off'),
pollingInterval: opt(1000 * 2), pollingInterval: opt(1000 * 2),