difonlib 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
difonlib/input_devs.py ADDED
@@ -0,0 +1,209 @@
1
+ from pathlib import Path
2
+ from evdev import InputDevice, categorize
3
+ from evdev.events import KeyEvent
4
+ from typing import Dict, Any, List, Optional
5
+ from difonlib.utils import logdbg
6
+ from dataclasses import dataclass
7
+ import re
8
+ import asyncio
9
+
10
+ dbg = logdbg
11
+
12
+ # import asyncio
13
+
14
+ KEY_LONG_PRESSED = 1000
15
+ KEY_LONG_TIME_HOLD = 0.7
16
+
17
+
18
+ @dataclass
19
+ class IDevKbd:
20
+ """event: /dev/input/eventX"""
21
+
22
+ name: str = ""
23
+ uniq: str = ""
24
+ event = ""
25
+
26
+
27
+ @dataclass
28
+ class IDevKbdKey:
29
+ scancode: int = 0
30
+ hold_time: float = 0
31
+ keycode: str | tuple = ""
32
+
33
+
34
+ def get_connected_input_devices() -> List[Dict[str, Any]]:
35
+ path = Path("/proc/bus/input/devices")
36
+ devices: List[Dict[str, Any]] = []
37
+ dev: Dict[str, Any] = {}
38
+
39
+ with open(path, "r") as f:
40
+ for line in f:
41
+ line = line.strip()
42
+ if not line: # пустая строка -> новый девайс
43
+ if dev:
44
+ devices.append(dev)
45
+ dev = {}
46
+ continue
47
+
48
+ key = line[0]
49
+ value = line[3:] # пропускаем "X: "
50
+
51
+ if key == "I":
52
+ # I: Bus=0003 Vendor=05ac Product=024f Version=0111
53
+ dev["I"] = dict(item.split("=") for item in value.split())
54
+ elif key in ("N", "P", "S", "U", "H"):
55
+ # Строковые поля
56
+ k, v = value.strip().split("=", 1)
57
+ dev[k] = v.strip('"')
58
+ elif key == "B":
59
+ # B: PROP=0 или B: KEY=... B: EV=...
60
+ bkey, bval = value.split("=", 1)
61
+ if "B" not in dev:
62
+ dev["B"] = {}
63
+ dev["B"][bkey] = bval
64
+ else:
65
+ dev[key] = value
66
+
67
+ # не забыть последний блок, так, на всякий случай. Обычно его там нет.
68
+ if dev:
69
+ devices.append(dev)
70
+
71
+ return devices
72
+
73
+
74
+ def idev_get_by_field(field: str, field_value: str) -> Optional[List[IDevKbd]]:
75
+ """If device connected by bluetooth - field 'Uniq' is mac address"""
76
+ conn_devs = get_connected_input_devices()
77
+ # dbg(f" = conn_devs: {conn_devs}") # //Dima
78
+ try:
79
+ devs = [dev for dev in conn_devs if dev[field] == field_value]
80
+ except Exception:
81
+ return None
82
+ kdevs = None
83
+ if devs:
84
+ kdevs = []
85
+ for dev in devs:
86
+ kdev = IDevKbd()
87
+ kdev.name = dev["Name"]
88
+ kdev.uniq = dev["Uniq"]
89
+ kdev.event = re.findall(r"event\d+", dev["Handlers"])[0]
90
+ kdevs.append(kdev)
91
+ return kdevs
92
+
93
+
94
+ def idev_key_monitor(dev_event: str) -> Optional[IDevKbdKey]:
95
+ """
96
+ Wait for any pressed key on dev_event
97
+ Return: ( key_event.scancode, hold_time, key_event.keycode )"""
98
+
99
+ press_time: float | None = None # timer key down timestamp
100
+ key: IDevKbdKey | None = None
101
+
102
+ dev = InputDevice(f"/dev/input/{dev_event}")
103
+ dbg(f"Listening on: {dev.name}")
104
+
105
+ for event in dev.read_loop():
106
+ # if event.type == ecodes.EV_KEY:
107
+ key_event = categorize(event)
108
+
109
+ if not isinstance(key_event, KeyEvent):
110
+ continue
111
+
112
+ if key_event.keystate == KeyEvent.key_down:
113
+ press_time = key_event.event.timestamp()
114
+
115
+ elif key_event.keystate == KeyEvent.key_up and press_time is not None:
116
+ key = IDevKbdKey()
117
+ key.hold_time = round(key_event.event.timestamp() - press_time, 2)
118
+ key.scancode = key_event.scancode
119
+ key.keycode = (
120
+ key_event.keycode
121
+ if not isinstance(key_event.keycode, list)
122
+ else key_event.keycode[0]
123
+ )
124
+ break
125
+ dev.close()
126
+ return key
127
+
128
+
129
+ async def idev_get_pressed_key(dev_event: str, timeout: int = 5) -> Optional[IDevKbdKey]:
130
+ """
131
+ Wait timeout seconds for pressed key on dev_event
132
+ """
133
+ dev = InputDevice(f"/dev/input/{dev_event}")
134
+ dbg(f" - Listening on: {dev.name}")
135
+ # key = await _get_first_key_event(dev)
136
+ try:
137
+ # asyncio.wait_for ограничивает выполнение асинхронной задачи по времени
138
+ key = await asyncio.wait_for(
139
+ # Вложенная функция, которая читает loop и возвращает первое событие
140
+ _get_first_key_event(dev),
141
+ timeout=timeout,
142
+ )
143
+ return key
144
+ except asyncio.CancelledError:
145
+ print(" =!= idev_get_pressed_key cancelled")
146
+ raise
147
+ except asyncio.TimeoutError:
148
+ print(f" =!= Timeout ({timeout} sec)")
149
+ except Exception as e:
150
+ print(f" =!= Error: {e}")
151
+
152
+ finally:
153
+ dev.close()
154
+ return None
155
+
156
+
157
+ async def _get_first_key_event(dev: InputDevice) -> Optional[IDevKbdKey]:
158
+ press_time: float | None = None
159
+
160
+ async for event in dev.async_read_loop():
161
+ key_event = categorize(event)
162
+
163
+ if not isinstance(key_event, KeyEvent):
164
+ continue
165
+
166
+ if key_event.keystate == KeyEvent.key_down:
167
+ press_time = key_event.event.timestamp()
168
+
169
+ elif key_event.keystate == KeyEvent.key_up and press_time is not None:
170
+ key = IDevKbdKey()
171
+ key.hold_time = round(key_event.event.timestamp() - press_time, 2)
172
+ key.scancode = key_event.scancode
173
+ key.keycode = (
174
+ key_event.keycode
175
+ if not isinstance(key_event.keycode, list)
176
+ else key_event.keycode[0]
177
+ )
178
+ return key
179
+
180
+ return None
181
+
182
+
183
+ # U: Uniq=40:b4:cd:ce:31:d6
184
+ if __name__ == "__main__":
185
+ dbg(" == START ==") # //Dima
186
+ # devs = get_connected_input_devices()
187
+ # print_dicts_list(devs)
188
+ # dbg(f"---------------------------------------------") # //Dima
189
+ # exit()
190
+ devs = idev_get_by_field("Name", "Keychron Keychron K5")
191
+ if devs:
192
+ for dev in devs:
193
+ dbg(f"dev: {dev.__dict__}") # //Dima
194
+
195
+ devs = idev_get_by_field(field="Uniq", field_value="40:b4:cd:ce:31:d6")
196
+ # print(repr(f"dev: {dev.__dict__}"))
197
+ if devs:
198
+ dev = devs[0]
199
+ dbg(f"Input dev: {dev.__dict__}")
200
+
201
+ key = idev_key_monitor(dev.event)
202
+ dbg(f"Pressed key: {key.__dict__}")
203
+
204
+ key = asyncio.run(idev_get_pressed_key(dev.event))
205
+ if key:
206
+ dbg(f"Pressed key: {key}")
207
+ dbg(f"Pressed key: {key.__dict__}")
208
+
209
+ dbg(" == FINISH ==") # //Dima
difonlib/ng_lib.py ADDED
@@ -0,0 +1,151 @@
1
+ from nicegui import ui
2
+ import inspect
3
+ import asyncio
4
+ from typing import Any, Callable, Awaitable, Dict, List, Optional, Union, Literal
5
+
6
+ from difonlib.utils import logdbg
7
+
8
+ dbg = logdbg
9
+
10
+
11
+ class CardTable:
12
+ _current_yes_handler: Optional[Callable[[], Union[None, Awaitable[None]]]] = None
13
+
14
+ def __init__(
15
+ self,
16
+ title: str,
17
+ columns: list,
18
+ rows: list = [],
19
+ selection: Optional[Literal["single", "multiple"]] = None, # single, multiple or None
20
+ on_selection_change: List[Callable] = [], # Handlers on_selection_change event
21
+ ):
22
+ self.on_selection_change: list = on_selection_change
23
+ # A list of buttons that change state (active, inactive) when table rows are selected or deselected.
24
+ self.buttons_on_row_select_changed: list = []
25
+
26
+ """Карточка с таблицей и встроенным диалогом подтверждения."""
27
+ with ui.card().classes("p-4 shadow-lg") as self.card:
28
+ # --- Заголовок ---
29
+ with ui.row().classes("items-center justify-between w-full mb-2"):
30
+ ui.label(f"📋 {title}").classes("text-green-700 text-lg font-bold")
31
+ with ui.row().classes("gap-2") as self.top_table:
32
+ pass
33
+
34
+ # --- Таблица ---
35
+ self.table = ui.table(
36
+ columns=columns,
37
+ rows=self.enum_data(rows),
38
+ row_key="sn",
39
+ selection=selection,
40
+ on_select=self._on_selection_change,
41
+ column_defaults={
42
+ "align": "left",
43
+ "headerClasses": "uppercase",
44
+ },
45
+ ).classes("w-full shadow-lg bg-black-900 text-gray-200")
46
+
47
+ # --- Диалог подтверждения ---
48
+ with ui.dialog() as self.confirm_dialog, ui.card().classes("p-4"):
49
+ self.dialog_title = ui.label().classes("text-lg font-bold mb-4")
50
+ with ui.row().classes("justify-end w-full gap-2"):
51
+ ui.button("No", color="gray", on_click=self.confirm_dialog.close)
52
+ ui.button("Yes", color="red", on_click=self._on_yes_clicked)
53
+
54
+ # --- Диалог "Processing..." ---
55
+ with (
56
+ ui.dialog() as self.processing_dialog,
57
+ ui.card().classes("p-4 items-center justify-center"),
58
+ ):
59
+ with ui.row().classes("items-center gap-3"):
60
+ self.processing_spinner = ui.spinner(size="md")
61
+ self.processing_label = ui.label("Processing...").classes("text-base")
62
+
63
+ def visible(self, state: Literal[True, False]) -> None:
64
+ if state:
65
+ self.table.visible = True
66
+ self.card.visible = True
67
+ else:
68
+ self.table.visible = False
69
+ self.card.visible = False
70
+
71
+ async def _on_selection_change(self, e: Any) -> None:
72
+ for btn in self.buttons_on_row_select_changed:
73
+ if e.selection:
74
+ dbg("Da") # //Dima
75
+ btn.classes("!bg-blue-500", remove="!bg-gray-500").enable()
76
+ else:
77
+ dbg("Net") # //Dima
78
+ btn.classes("!bg-gray-500", remove="!bg-blue-500").disable()
79
+ dbg(f"** self.on_selection_change: {self.on_selection_change}") # //Dima
80
+ for handler in self.on_selection_change:
81
+ await handler(e)
82
+
83
+ async def _on_yes_clicked(self) -> None:
84
+ """Обработка подтверждения с показом диалога 'Processing...'."""
85
+ handler: Optional[Callable[[], Union[None, Awaitable[None]]]] = self._current_yes_handler
86
+ self._current_yes_handler = None
87
+ self.confirm_dialog.close()
88
+
89
+ if not handler:
90
+ return
91
+
92
+ # --- Показать диалог "Processing..." ---
93
+ self.processing_dialog.open()
94
+
95
+ try:
96
+ if inspect.iscoroutinefunction(handler):
97
+ await handler()
98
+ await asyncio.sleep(0.3) # плавность UX
99
+ else:
100
+ handler()
101
+ finally:
102
+ self.processing_dialog.close()
103
+
104
+ def enum_data(self, data: List[Dict]) -> List[Dict]:
105
+ return [{"sn": i + 1, **row} for i, row in enumerate(data)]
106
+
107
+ def set_rows(self, rows: List[Dict]) -> None:
108
+ self.table.rows = self.enum_data(rows)
109
+ self.table.update()
110
+
111
+ # def add_handler_on_selection_change(self, handler: Callable[[Any]]) -> None:
112
+ # if handler in self.on_selection_change:
113
+ # return
114
+ # self.on_selection_change.append(handler)
115
+
116
+ def add_button(
117
+ self,
118
+ btn_txt: str,
119
+ on_click: Callable,
120
+ default_enable: bool = True, # enable the added button by default
121
+ color: str = "blue",
122
+ active_on_rows_selected: bool = False, # activate button when row(s) is selected, if True - always actived
123
+ use_dialog_confirm: bool = False,
124
+ confirm_title: Optional[str] = None,
125
+ ) -> ui.button:
126
+ """Добавляет кнопку в заголовок таблицы, с опциональным подтверждением."""
127
+ with self.top_table:
128
+ btn = ui.button(btn_txt, color=color)
129
+
130
+ if active_on_rows_selected:
131
+ self.buttons_on_row_select_changed.append(btn)
132
+ if default_enable:
133
+ btn.classes("!bg-blue-500", remove="!bg-gray-500").enable()
134
+ else:
135
+ btn.classes("!bg-gray-500", remove="!bg-blue-500").disable()
136
+
137
+ dbg(f"btns: {self.buttons_on_row_select_changed}") # //Dima
138
+
139
+ # --- Без диалога ---
140
+ if not use_dialog_confirm:
141
+ btn.on_click(on_click)
142
+ return btn
143
+
144
+ # --- С подтверждением ---
145
+ async def handle_click() -> None:
146
+ self.dialog_title.text = confirm_title or f"Confirm {btn_txt}?"
147
+ self._current_yes_handler = on_click
148
+ self.confirm_dialog.open()
149
+
150
+ btn.on_click(handle_click)
151
+ return btn
difonlib/py.typed ADDED
File without changes
difonlib/remctrl.py ADDED
@@ -0,0 +1,157 @@
1
+ import asyncio
2
+ from difonlib.input_devs import idev_get_pressed_key, IDevKbdKey
3
+ from difonlib.utils import logdbg, YamlConfig
4
+ from typing import List, Optional # Dict, Any, List
5
+
6
+ dbg = logdbg
7
+
8
+ KEY_LONG_PRESSED_TIME = 0.7
9
+ KEY_LONG_PRESSED_CONST = 1000
10
+
11
+
12
+ class RemoteControls:
13
+ """
14
+ Remote Control.
15
+ 1. Learn buttons
16
+ Bind func_keys with remote control buttons
17
+ 2. Run monitor service of remote control pressed button (async mode)
18
+ """
19
+
20
+ def __init__(self, config_file_name: str) -> None:
21
+ # Load(create) config file
22
+ self.config = YamlConfig(config_file_name)
23
+ if self.config.config == {}:
24
+ self.config.config["infrared_devs"] = {} # {dev_id:xxx, dev_local_key:xxx}
25
+ self.config.config["remctrl_devs"] = {}
26
+ self.config.config["func_keys"] = []
27
+ self.config.save()
28
+
29
+ self.cfg_ir_devs = self.config.config["infrared_devs"]
30
+ self.cfg_rc_devs = self.config.config["remctrl_devs"]
31
+ self.cfg_func_keys = self.config.config["func_keys"]
32
+
33
+ # if self.func_keys != self.cfg_func_keys:
34
+ # self.cfg_func_keys = self.func_keys
35
+ # self.config.save()
36
+
37
+ def set_func_keys(self, func_keys: List[str]) -> None:
38
+ if func_keys != self.cfg_func_keys:
39
+ self.config.config.update({"func_keys": func_keys})
40
+ self.cfg_func_keys = self.config.config["func_keys"]
41
+ self.config.save()
42
+
43
+ def remove_func_btn(self, remctrl_name: str, func_key: Optional[str] = None) -> None:
44
+ if not func_key:
45
+ # remove remctrl_name from config
46
+ try:
47
+ self.config.config.pop(remctrl_name)
48
+ except Exception:
49
+ pass
50
+ else:
51
+ self.config.config[remctrl_name][func_key] = None
52
+ self.config.save()
53
+
54
+ def get_func_btn(self, rem_ctrl_name: Optional[str] = None) -> list:
55
+ rem_ctrl = self.cfg_rc_devs.get(rem_ctrl_name)
56
+ if rem_ctrl:
57
+ dbg(f" * rem_ctrl: {rem_ctrl}") # //Dima
58
+ fs = [{f: rem_ctrl.get(f)} for f in self.cfg_func_keys]
59
+ return fs
60
+ fs = [{f: None} for f in self.cfg_func_keys]
61
+ return fs
62
+
63
+ def get_func_btn_format(
64
+ self,
65
+ func_field_name: str,
66
+ btn_field_name: str,
67
+ rem_ctrl_name: Optional[str] = None,
68
+ ) -> list:
69
+ _rows = self.get_func_btn(rem_ctrl_name)
70
+ rows = [
71
+ {
72
+ func_field_name: list(row.keys())[0],
73
+ btn_field_name: list(row.values())[0],
74
+ }
75
+ for row in _rows
76
+ ]
77
+ return rows
78
+
79
+ async def devs_monitor(self) -> None:
80
+ """
81
+ Monitor input event devices for pressed key
82
+ Detect pressed key and call handler('KEY_XXXXXX')
83
+ """
84
+ pass
85
+
86
+ async def get_pressed_button(self, dev_event: str, timeout: int = 7) -> Optional[IDevKbdKey]:
87
+ key = await idev_get_pressed_key(dev_event, timeout=timeout)
88
+ dbg(f" * Pressed KEY: {key}") # //Dima
89
+ return key
90
+
91
+ async def learn_button(
92
+ self, idev_name: str, idev_event: str, func_key: str, timeout: int = 10
93
+ ) -> Optional[IDevKbdKey]:
94
+ # dbg(f"self.cfg_func_keys: {self.cfg_func_keys}") # //Dima
95
+ if func_key not in self.cfg_func_keys:
96
+ print(f" =!= func_key: '{func_key}' is not exist")
97
+ return None
98
+
99
+ # dbg(f"*** CONFIG: {self.config.config}") # //Dima
100
+ # idev_name = idev["name"]
101
+ # # dev_uniq = idev["mac"]
102
+ # idev_event = idev["event"]
103
+ key = await idev_get_pressed_key(idev_event, timeout=timeout)
104
+ dbg(f" * Pressed KEY: {key}") # //Dima
105
+ if key:
106
+ key_scan_code = key.scancode
107
+ if key.hold_time > KEY_LONG_PRESSED_TIME:
108
+ key_scan_code += KEY_LONG_PRESSED_CONST
109
+ if not self.cfg_rc_devs.get(idev_name):
110
+ self.cfg_rc_devs[idev_name] = {}
111
+ # self.config.config["remctrl_devs"].add(dev_uniq)
112
+ self.cfg_rc_devs[idev_name].update({func_key: key_scan_code})
113
+ self.config.save()
114
+
115
+ return key
116
+
117
+
118
+ if __name__ == "__main__":
119
+
120
+ REMOTE_CTRL_CONFIG = "./___remctrl_devs_config.yaml"
121
+ func_keys = [
122
+ "KEY_PREV",
123
+ "KEY_PLAY_ALBUM",
124
+ "KEY_PAUSE_PLAY",
125
+ "KEY_NEXT",
126
+ "KEY_SEEKCUR",
127
+ "KEY_RADIO",
128
+ "KEY_JUKEBOX_ON",
129
+ "KEY_PLAY_LIKE",
130
+ "KEY_SET_LIKE",
131
+ "KEY_VOL+",
132
+ "KEY_VOL-",
133
+ "KEY_OnOff",
134
+ "KEY_EXT_DISP",
135
+ ]
136
+
137
+ rc = RemoteControls(REMOTE_CTRL_CONFIG)
138
+ rc.set_func_keys(func_keys)
139
+
140
+ from difonlib.input_devs import idev_get_by_field
141
+
142
+ remctrl_name = "Amazon Fire TV Remote Keyboard"
143
+ idev_kbd = idev_get_by_field(field="Name", field_value=remctrl_name)
144
+
145
+ if idev_kbd:
146
+ key = asyncio.run(
147
+ rc.learn_button(
148
+ idev_name=remctrl_name,
149
+ idev_event=idev_kbd[0].event,
150
+ func_key="KEY_PAUSE_PLAY",
151
+ )
152
+ )
153
+ dbg(f"key: {key}") # //Dima
154
+ else:
155
+ dbg(f"The Input Device '{remctrl_name}' is not connected.") # //Dima
156
+ fb = rc.get_func_btn(remctrl_name)
157
+ dbg(f"Function-Button: {fb}") # //Dima