import { bind, execAsync, interval, Variable } from 'astal'; import { Gtk, Widget } from 'astal/gtk3'; import Brightness from '../../services/brightness'; import Separator from '../misc/separator'; /* Types */ import AstalIO from 'gi://AstalIO'; interface Key { keytype: string label: string labelShift?: string labelAltGr?: string shape: string keycode: number } const brightness = Brightness.get_default(); const SPACING = 4; const LSHIFT_CODE = 42; const LALT_CODE = 56; const LCTRL_CODE = 29; export const Keys = Variable<Variable<boolean>[]>([]); // Keep track of modifier statuses const Super = Variable(false); const LAlt = Variable(false); const LCtrl = Variable(false); const AltGr = Variable(false); const RCtrl = Variable(false); const Caps = Variable(false); brightness.connect('notify::caps-level', (_, state) => { Caps.set(state); }); // Assume both shifts are the same for key.labelShift const LShift = Variable(false); const RShift = Variable(false); const Shift = Variable(false); LShift.subscribe(() => { Shift.set(LShift.get() || RShift.get()); }); RShift.subscribe(() => { Shift.set(LShift.get() || RShift.get()); }); const ModKey = (key: Key) => { let Mod: Variable<boolean>; if (key.label === 'Super') { Mod = Super; } // Differentiate left and right mods else if (key.label === 'Shift' && key.keycode === LSHIFT_CODE) { Mod = LShift; } else if (key.label === 'Alt' && key.keycode === LALT_CODE) { Mod = LAlt; } else if (key.label === 'Ctrl' && key.keycode === LCTRL_CODE) { Mod = LCtrl; } else if (key.label === 'Shift') { Mod = RShift; } else if (key.label === 'AltGr') { Mod = AltGr; } else if (key.label === 'Ctrl') { Mod = RCtrl; } Keys.set([...Keys.get(), Mod!]); const label = ( <label className={`mod ${key.label}`} label={key.label} /> ) as Widget.Label; const button = ( <eventbox className="key" cursor="pointer" onButtonPressEvent={() => { execAsync(`ydotool key ${key.keycode}:${Mod.get() ? 0 : 1}`); label.toggleClassName('active', !Mod.get()); Mod.set(!Mod.get()); }} > {label} </eventbox> ); return ( <box> {button} <Separator size={SPACING} /> </box> ) as Widget.Box; }; const RegularKey = (key: Key) => { const IsActive = Variable(false); const IsLongPressing = Variable(false); Keys.set([...Keys.get(), IsActive]); const widget = ( <eventbox className="key" cursor="pointer" onButtonReleaseEvent={() => { IsLongPressing.set(false); IsActive.set(false); }} > <label className={bind(IsActive).as((v) => [ 'normal', key.label, (v ? 'active' : ''), ].join(' '))} label={key.label} setup={(self) => { self .hook(Shift, () => { if (key.labelShift) { self.set_label(Shift.get() ? key.labelShift : key.label); } }) .hook(Caps, () => { if (key.label === 'Caps') { IsActive.set(Caps.get()); return; } if (key.labelShift && key.label.match(/[A-Za-z]/)) { self.set_label(Caps.get() ? key.labelShift : key.label); } }) .hook(AltGr, () => { if (key.labelAltGr) { self.toggleClassName('altgr', AltGr.get()); self.set_label(AltGr.get() ? key.labelAltGr : key.label); } }); }} /> </eventbox> ) as Widget.EventBox; const gesture = Gtk.GestureLongPress.new(widget); gesture.delay_factor = 1.0; const onClick = (callback: () => void) => { const pointer = gesture.get_point(null); const x = pointer[1]; const y = pointer[2]; if ((!x || !y) || (x === 0 && y === 0)) { return; } callback(); }; widget.hook(gesture, 'begin', () => { IsActive.set(true); }); widget.hook(gesture, 'cancelled', () => { onClick(() => { execAsync(`ydotool key ${key.keycode}:1`); execAsync(`ydotool key ${key.keycode}:0`); IsActive.set(false); }); }); // Long Press widget.hook(gesture, 'pressed', () => { onClick(() => { IsLongPressing.set(true); }); }); let spamClick: AstalIO.Time | undefined; IsLongPressing.subscribe((v) => { if (v) { spamClick = interval(100, () => { execAsync(`ydotool key ${key.keycode}:1`); execAsync(`ydotool key ${key.keycode}:0`); }); } else { spamClick?.cancel(); } }); return ( <box> {widget} <Separator size={SPACING} /> </box> ) as Widget.Box; }; export default (key: Key): Widget.Box => key.keytype === 'normal' ? RegularKey(key) : ModKey(key);