diff --git a/config/quickshell/.config/quickshell/Services/CapslockService.qml b/config/quickshell/.config/quickshell/Services/CapslockService.qml index 7855ce7..a5b2c9c 100644 --- a/config/quickshell/.config/quickshell/Services/CapslockService.qml +++ b/config/quickshell/.config/quickshell/Services/CapslockService.qml @@ -11,7 +11,11 @@ Singleton { property bool isCapslockOn: false onIsCapslockOnChanged: { - TempNotificationService.showWithIcon("letter-case", root.isCapslockOn ? "CAPS LOCK ON" : "caps lock off"); + if (root.isCapslockOn) { + TempNotificationService.showWithIcon("letter-case-toggle", "CAPS LOCK ON"); + } else { + TempNotificationService.showWithIcon("letter-case", "caps lock off"); + } } Process { diff --git a/config/scripts/.local/scripts/led-monitor b/config/scripts/.local/scripts/led-monitor index cfb1040..146425e 100755 --- a/config/scripts/.local/scripts/led-monitor +++ b/config/scripts/.local/scripts/led-monitor @@ -6,15 +6,16 @@ import select import os import struct -# struct input_event: struct timeval (typically 2 longs), type (uint16), code (uint16), value (int32) -# '@' ensures native byte order, size, and alignment. -EVENT_FORMAT = "@llHHi" +EVENT_FORMAT = '@llHHi' EVENT_SIZE = struct.calcsize(EVENT_FORMAT) EV_LED = 0x11 -LED_CODES = {"numlock": 0x00, "capslock": 0x01, "scrolllock": 0x02} - +LED_CODES = { + 'numlock': 0x00, + 'capslock': 0x01, + 'scrolllock': 0x02 +} def get_led_state(led_type: str) -> int: pattern = f"/sys/class/leds/*::{led_type}/brightness" @@ -23,26 +24,24 @@ def get_led_state(led_type: str) -> int: return 0 for path in paths: try: - with open(path, "r") as f: + with open(path, 'r') as f: if int(f.read().strip()) > 0: return 1 except (IOError, ValueError, OSError): continue return 0 - def has_led_capability(event_path: str) -> bool: try: basename = os.path.basename(event_path) cap_path = f"/sys/class/input/{basename}/device/capabilities/led" if os.path.exists(cap_path): - with open(cap_path, "r") as f: + with open(cap_path, 'r') as f: return f.read().strip() != "0" except OSError: pass return False - class DeviceMonitor: def __init__(self, target_led: str): self.target_led = target_led @@ -55,12 +54,17 @@ class DeviceMonitor: self.emit_state(self.last_state) def emit_state(self, state: int) -> None: - sys.stdout.write(f"{state}\n") - sys.stdout.flush() + try: + sys.stdout.write(f"{state}\n") + sys.stdout.flush() + except BrokenPipeError: + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + sys.exit(0) def scan_devices(self) -> None: current_fds = set(self.active_fds.keys()) - paths = glob.glob("/dev/input/event*") + paths = glob.glob('/dev/input/event*') found_fds = set() for path in paths: @@ -76,7 +80,6 @@ class DeviceMonitor: if not already_open: try: - # Pure unbuffered OS file descriptor fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) self.poller.register(fd, select.POLLIN) self.active_fds[fd] = path @@ -99,6 +102,36 @@ class DeviceMonitor: except Exception: pass + def handle_device_events(self, events: list) -> None: + fds_to_remove = [] + + for fd, event in events: + if event & select.POLLIN: + try: + while True: + data = os.read(fd, 4096) + if not data: + fds_to_remove.append(fd) + break + + events_count = len(data) // EVENT_SIZE + for i in range(events_count): + chunk = data[i * EVENT_SIZE : (i + 1) * EVENT_SIZE] + _, _, ev_type, ev_code, ev_value = struct.unpack(EVENT_FORMAT, chunk) + + if ev_type == EV_LED and ev_code == self.target_led_code: + if ev_value != self.last_state: + self.emit_state(ev_value) + self.last_state = ev_value + + except BlockingIOError: + pass + except OSError: + fds_to_remove.append(fd) + + for fd in fds_to_remove: + self.remove_device(fd) + def run(self) -> None: self.scan_devices() @@ -112,53 +145,8 @@ class DeviceMonitor: if current_state != self.last_state: self.emit_state(current_state) self.last_state = current_state - continue - - fds_to_remove = [] - # Flag to check if we intercepted an EV_LED during this batch - led_event_triggered = False - - for fd, event in events: - if event & select.POLLIN: - try: - while True: - # Drain heavily with pure OS call - data = os.read(fd, 4096) - if not data: - fds_to_remove.append(fd) - break - - # Process the chunk if we want direct parsing - events_count = len(data) // EVENT_SIZE - for i in range(events_count): - chunk = data[i * EVENT_SIZE : (i + 1) * EVENT_SIZE] - _, _, ev_type, ev_code, _ = struct.unpack( - EVENT_FORMAT, chunk - ) - - if ( - ev_type == EV_LED - and ev_code == self.target_led_code - ): - # Instead of acting immediately on the value, we just flag it. - # This prevents state bouncing between multiple kernel devices. - led_event_triggered = True - - except BlockingIOError: - pass - except OSError: - fds_to_remove.append(fd) - - for fd in fds_to_remove: - self.remove_device(fd) - - # If ANY EV_LED was detected during the drain, fall back to sysfs ONCE - # at the end of the batch. This solves the bouncing/dropping issue perfectly. - if led_event_triggered: - current_state = get_led_state(self.target_led) - if current_state != self.last_state: - self.emit_state(current_state) - self.last_state = current_state + else: + self.handle_device_events(events) except KeyboardInterrupt: sys.exit(0) @@ -169,22 +157,19 @@ class DeviceMonitor: except Exception: pass - def main(): - parser = argparse.ArgumentParser(description="Keyboard LED monitor.") + parser = argparse.ArgumentParser(description="Zero-polling keyboard LED monitor.") parser.add_argument( - "-l", - "--led", + '-l', '--led', type=str, - default="capslock", - choices=["capslock", "numlock", "scrolllock"], - help="Target LED to monitor", + default='capslock', + choices=['capslock', 'numlock', 'scrolllock'], + help="Target LED to monitor" ) args = parser.parse_args() monitor = DeviceMonitor(args.led) monitor.run() - if __name__ == "__main__": main() diff --git a/config/yazi/.config/yazi/package.toml b/config/yazi/.config/yazi/package.toml index 277f141..6dce053 100644 --- a/config/yazi/.config/yazi/package.toml +++ b/config/yazi/.config/yazi/package.toml @@ -20,8 +20,8 @@ hash = "771af2becc575a3f43d0542de823969d" [[plugin.deps]] use = "llanosrocas/yaziline" -rev = "d9cc2cb" -hash = "b6073aadf2f9a1d5389a6d389f33f69c" +rev = "cc0314c" +hash = "b937c5c8e2d9fa314d4532489176814e" [[plugin.deps]] use = "Rolv-Apneseth/starship" diff --git a/config/yazi/.config/yazi/plugins/yaziline.yazi/main.lua b/config/yazi/.config/yazi/plugins/yaziline.yazi/main.lua index 2bbbd16..92c3da8 100644 --- a/config/yazi/.config/yazi/plugins/yaziline.yazi/main.lua +++ b/config/yazi/.config/yazi/plugins/yaziline.yazi/main.lua @@ -30,17 +30,15 @@ local function setup(_, options) color = options.color or nil, secondary_color = options.secondary_color or nil, default_files_color = options.default_files_color - or th.which.separator_style:fg() - or "darkgray", + or th.which.separator_style:fg() + or "darkgray", selected_files_color = options.selected_files_color - or th.mgr.count_selected:bg() - or "white", - yanked_files_color = options.selected_files_color - or th.mgr.count_copied:bg() - or "green", - cut_files_color = options.cut_files_color - or th.mgr.count_cut:bg() - or "red", + or th.mgr.count_selected:bg() + or "white", + yanked_files_color = options.yanked_files_color + or th.mgr.count_copied:bg() + or "green", + cut_files_color = options.cut_files_color or th.mgr.count_cut:bg() or "red", } local current_separator_style = config.separator_styles @@ -55,10 +53,10 @@ local function setup(_, options) local style = self:style() return ui.Line({ ui.Span(current_separator_style.separator_head) - :fg(config.color or style.main:bg()), + :fg(config.color or style.main:bg()), ui.Span(" " .. mode .. " ") - :fg(th.which.mask:bg()) - :bg(config.color or style.main:bg()), + :fg(th.which.mask:bg()) + :bg(config.color or style.main:bg()), }) end @@ -67,9 +65,14 @@ local function setup(_, options) local size = h and (h:size() or h.cha.len) or 0 local style = self:style() - return ui.Span(current_separator_style.separator_close .. " " .. ya.readable_size(size) .. " ") - :fg(config.color or style.main:bg()) - :bg(config.secondary_color or th.which.separator_style:fg()) + return ui.Span( + current_separator_style.separator_close + .. " " + .. ya.readable_size(size) + .. " " + ) + :fg(config.color or style.main:bg()) + :bg(config.secondary_color or th.which.separator_style:fg()) end function Status:utf8_sub(str, start_char, end_char) @@ -90,8 +93,8 @@ local function setup(_, options) if utf8.len(base_name) > max_length then base_name = self:utf8_sub(base_name, 1, config.filename_truncate_length) - .. config.filename_truncate_separator - .. self:utf8_sub(base_name, -config.filename_truncate_length) + .. config.filename_truncate_separator + .. self:utf8_sub(base_name, -config.filename_truncate_length) end return base_name .. extension @@ -103,19 +106,18 @@ local function setup(_, options) if not h then return ui.Line({ ui.Span(current_separator_style.separator_close .. " ") - :fg(config.secondary_color or th.which.separator_style:fg()), - ui.Span("Empty dir") - :fg(config.color or style.main:bg()), + :fg(config.secondary_color or th.which.separator_style:fg()), + ui.Span("Empty dir"):fg(config.color or style.main:bg()), }) end - local truncated_name = self:truncate_name(h.name, config.filename_max_length) + local truncated_name = + self:truncate_name(h.name, config.filename_max_length) return ui.Line({ ui.Span(current_separator_style.separator_close .. " ") - :fg(config.secondary_color or th.which.separator_style:fg()), - ui.Span(truncated_name) - :fg(config.color or style.main:bg()), + :fg(config.secondary_color or th.which.separator_style:fg()), + ui.Span(truncated_name):fg(config.color or style.main:bg()), }) end @@ -124,28 +126,22 @@ local function setup(_, options) local files_selected = #cx.active.selected local files_cut = cx.yanked.is_cut - local selected_fg = files_selected > 0 - and config.selected_files_color - or config.default_files_color + local selected_fg = files_selected > 0 and config.selected_files_color + or config.default_files_color local yanked_fg = files_yanked > 0 - and - (files_cut - and config.cut_files_color - or config.yanked_files_color - ) - or config.default_files_color + and (files_cut and config.cut_files_color or config.yanked_files_color) + or config.default_files_color local yanked_text = files_yanked > 0 and config.yank_symbol .. " " .. files_yanked - or config.yank_symbol .. " 0" + or config.yank_symbol .. " 0" return ui.Line({ ui.Span(" " .. current_separator_style.separator_close_thin .. " ") - :fg(th.which.separator_style:fg()), + :fg(th.which.separator_style:fg()), ui.Span(config.select_symbol .. " " .. files_selected .. " ") - :fg(selected_fg), - ui.Span(yanked_text .. " ") - :fg(yanked_fg), + :fg(selected_fg), + ui.Span(yanked_text .. " "):fg(yanked_fg), }) end @@ -159,8 +155,12 @@ local function setup(_, options) local cha = hovered.cha local time = (cha.mtime or 0) // 1 - return ui.Span(os.date("%Y-%m-%d %H:%M", time) .. " " .. current_separator_style.separator_open_thin .. " ") - :fg(th.which.separator_style:fg()) + return ui.Span( + os.date("%Y-%m-%d %H:%M", time) + .. " " + .. current_separator_style.separator_open_thin + .. " " + ):fg(th.which.separator_style:fg()) end function Status:percent() @@ -182,13 +182,13 @@ local function setup(_, options) local style = self:style() return ui.Line({ ui.Span(" " .. current_separator_style.separator_open) - :fg(config.secondary_color or th.which.separator_style:fg()), + :fg(config.secondary_color or th.which.separator_style:fg()), ui.Span(percent) - :fg(config.color or style.main:bg()) - :bg(config.secondary_color or th.which.separator_style:fg()), + :fg(config.color or style.main:bg()) + :bg(config.secondary_color or th.which.separator_style:fg()), ui.Span(current_separator_style.separator_open) - :fg(config.color or style.main:bg()) - :bg(config.secondary_color or th.which.separator_style:fg()), + :fg(config.color or style.main:bg()) + :bg(config.secondary_color or th.which.separator_style:fg()), }) end @@ -198,10 +198,13 @@ local function setup(_, options) local style = self:style() return ui.Line({ - ui.Span(string.format(" %2d/%-2d ", math.min(cursor + 1, length), length)) - :fg(th.which.mask:bg()) - :bg(config.color or style.main:bg()), - ui.Span(current_separator_style.separator_tail):fg(config.color or style.main:bg()), + ui.Span( + string.format(" %2d/%-2d ", math.min(cursor + 1, length), length) + ) + :fg(th.which.mask:bg()) + :bg(config.color or style.main:bg()), + ui.Span(current_separator_style.separator_tail) + :fg(config.color or style.main:bg()), }) end