update
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user