Skip to content

Commit 08ed41a

Browse files
authored
Merge pull request #7 from hmasdev/feature/add-sshkeyboard-monitor
add sshkeyboard key monitor
2 parents 3a9826d + 0fc4ba7 commit 08ed41a

13 files changed

+322
-3
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,14 @@ To see the default values, see [`./simple_typing_application/models/config_model
136136
You can specity the followings as `key_monitor_type`:
137137

138138
- `PYNPUT`: `pynput`-based local key monitor
139+
- `SSHKEYBOARD`: `sshkeyboard`-based key monitor
139140

140141
For each `key_monitor_type`, you can specify the detailed parameters as `key_monitor_config`:
141142

142143
- `PYNPUT`
143144
- No parameters
145+
- `SSHKEYBOARD`
146+
- No parameters
144147

145148
To see the default values, see [`./simple_typing_application/models/config_models/key_monitor_config_model.py`](./simple_typing_application/models/config_models/key_monitor_config_model.py).
146149

mypy.ini

+3
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ warn_return_any = True
33
warn_unused_configs = True
44
check_untyped_defs = True
55

6+
[mypy-sshkeyboard]
7+
ignore_missing_imports = True
8+
69
[mypy-transformers]
710
ignore_missing_imports = True

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ openai
44
pydantic
55
pynput
66
requests
7+
sshkeyboard
78
types-pynput
89
types-requests
910
--extra-index-url https://download.pytorch.org/whl/cu117

sample_config_with_sshkeyboard.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"sentence_generator_type": "OPENAI",
3+
"sentence_generator_config": {
4+
"model": "gpt-3.5-turbo-16k",
5+
"temperature": 0.7,
6+
"openai_api_key": "HERE_IS_YOUR_API_KEY",
7+
"memory_size": 0,
8+
"max_retry": 5
9+
},
10+
"user_interface_type": "CONSOLE",
11+
"user_interface_config": {},
12+
"key_monitor_type": "SSHKEYBOARD",
13+
"key_monitor_config": {},
14+
"record_direc": "./record"
15+
}

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ install_requires =
2828
pydantic
2929
pynput
3030
requests
31+
sshkeyboard
3132
types-pynput
3233
types-requests
3334

simple_typing_application/const/key_monitor.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33

44
class EKeyMonitorType(Enum):
5+
SSHKEYBOARD: str = 'SSHKEYBOARD'
56
PYNPUT: str = 'PYNPUT'

simple_typing_application/key_monitor/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from . import (
2+
sshkeyboard,
23
base,
34
factory,
45
pynput,
@@ -10,6 +11,7 @@
1011

1112
__all__ = [
1213
base.__name__,
14+
sshkeyboard.__name__,
1315
factory.__name__,
1416
pynput.__name__,
1517
BaseKeyMonitor.__name__,

simple_typing_application/key_monitor/factory.py

+4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
from .base import BaseKeyMonitor
55
from .pynput import PynputBasedKeyMonitor
6+
from .sshkeyboard import SSHKeyboardBasedKeyMonitor
67
from ..const.key_monitor import EKeyMonitorType
78
from ..models.config_models.key_monitor_config_model import (
9+
SSHKeyboardBasedKeyMonitorConfigModel,
810
BaseKeyMonitorConfigModel,
911
PynputBasedKeyMonitorConfigModel,
1012
)
@@ -14,6 +16,8 @@ def _select_class_and_config_model(key_monitor_type: EKeyMonitorType) -> tuple[t
1416

1517
if key_monitor_type == EKeyMonitorType.PYNPUT:
1618
return PynputBasedKeyMonitor, PynputBasedKeyMonitorConfigModel
19+
elif key_monitor_type == EKeyMonitorType.SSHKEYBOARD:
20+
return SSHKeyboardBasedKeyMonitor, SSHKeyboardBasedKeyMonitorConfigModel # noqa
1721
else:
1822
raise ValueError(f'Unsupported key monitor type: {key_monitor_type}')
1923

simple_typing_application/key_monitor/pynput.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def _on_press_callback_wrapper(
6666
self._logger.warning(f'{self.__class__.__name__}._on_press_callback is None.') # noqa
6767
return
6868

69-
self._on_press_callback(cleaned_key)
69+
return self._on_press_callback(cleaned_key)
7070

7171
def _on_release_callback_wrapper(
7272
self,
@@ -79,4 +79,4 @@ def _on_release_callback_wrapper(
7979
self._logger.warning(f'{self.__class__.__name__}._on_release_callback is None.') # noqa
8080
return
8181

82-
self._on_release_callback(cleaned_key)
82+
return self._on_release_callback(cleaned_key)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Path: sshkeyboard.py
2+
from collections import deque
3+
from logging import getLogger, Logger
4+
from threading import Thread
5+
from typing import Callable
6+
from sshkeyboard import listen_keyboard, stop_listening
7+
from .base import BaseKeyMonitor
8+
from ..const.keys import EMetaKey
9+
10+
11+
class SSHKeyboardBasedKeyMonitor(BaseKeyMonitor):
12+
13+
def __init__(
14+
self,
15+
logger: Logger = getLogger(__name__),
16+
):
17+
self._on_press_callback: Callable[[EMetaKey | str | None], bool | None] | None = None # noqa
18+
self._on_release_callback: Callable[[EMetaKey | str | None], bool | None] | None = None # noqa
19+
self._logger = logger
20+
21+
self.__thread: Thread | None = None
22+
self.__keys_queue: deque = deque()
23+
24+
def start(self):
25+
self.__thread = Thread(
26+
target=listen_keyboard,
27+
kwargs=dict(
28+
on_press=self._on_press_callback_wrapper,
29+
on_release=self._on_release_callback_wrapper,
30+
)
31+
)
32+
self.__thread.start()
33+
self.__thread.join()
34+
35+
def stop(self):
36+
37+
if self.__thread is None:
38+
self._logger.warning(f'{self.__class__.__name__}() has not been started.') # noqa
39+
return
40+
41+
# send stop command
42+
stop_listening()
43+
# reset keys queue
44+
self._reset_keys_queue()
45+
# reset thread
46+
self.__thread = None
47+
48+
def _reset_keys_queue(self):
49+
self.__keys_queue.clear()
50+
51+
def _clean_key(self, key: str) -> EMetaKey | str | None:
52+
if len(key) == 1:
53+
return key
54+
elif key.lower() == 'tab':
55+
return EMetaKey.TAB
56+
elif key.lower() == 'esc':
57+
return EMetaKey.ESC
58+
elif key.lower() == 'space':
59+
return ' '
60+
else:
61+
self._logger.warning(f'Invalid key: {key}')
62+
return None
63+
64+
def _on_press_callback_wrapper(self, key: str):
65+
self._logger.debug(f'pressed key: {key}')
66+
# record key
67+
if len(key) == 1:
68+
# NOTE: key can be two or more characters when it is a special key like "tab" # noqa
69+
self.__keys_queue.append(key)
70+
# preprocess
71+
key_ = self._clean_key(key)
72+
# callback
73+
if self._on_press_callback is None:
74+
self._logger.warning(f'{self.__class__.__name__}._on_press_callback is None.') # noqa
75+
return
76+
return self._on_press_callback(key_)
77+
78+
def _on_release_callback_wrapper(self, key: str):
79+
self._logger.debug(f'released key: {key}')
80+
if self._on_release_callback is None:
81+
self._logger.warning(f'{self.__class__.__name__}._on_release_callback is None.') # noqa
82+
return
83+
return self._on_release_callback(self._clean_key(key))

simple_typing_application/models/config_models/key_monitor_config_model.py

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ class BaseKeyMonitorConfigModel(BaseModel):
77

88
class PynputBasedKeyMonitorConfigModel(BaseKeyMonitorConfigModel):
99
pass
10+
11+
12+
class SSHKeyboardBasedKeyMonitorConfigModel(BaseKeyMonitorConfigModel):
13+
pass

tests/key_monitor/test_factory.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from simple_typing_application.const.key_monitor import EKeyMonitorType # noqa
44
from simple_typing_application.models.config_models.key_monitor_config_model import ( # noqa
5+
SSHKeyboardBasedKeyMonitorConfigModel,
56
PynputBasedKeyMonitorConfigModel,
67
)
8+
from simple_typing_application.key_monitor.sshkeyboard import SSHKeyboardBasedKeyMonitor # noqa
79
from simple_typing_application.key_monitor.factory import (
810
create_key_monitor,
911
_select_class_and_config_model
@@ -15,6 +17,7 @@
1517
"key_monitor_type, expected_class, expected_config_model",
1618
[
1719
(EKeyMonitorType.PYNPUT, PynputBasedKeyMonitor, PynputBasedKeyMonitorConfigModel), # noqa
20+
(EKeyMonitorType.SSHKEYBOARD, SSHKeyboardBasedKeyMonitor, SSHKeyboardBasedKeyMonitorConfigModel), # noqa
1821
]
1922
)
2023
def test_select_class_and_config_model(
@@ -49,7 +52,8 @@ def test_select_class_and_config_model_raise_value_error():
4952
EKeyMonitorType.PYNPUT,
5053
{},
5154
PynputBasedKeyMonitor,
52-
)
55+
),
56+
(EKeyMonitorType.SSHKEYBOARD, SSHKeyboardBasedKeyMonitorConfigModel().model_dump(), SSHKeyboardBasedKeyMonitor), # noqa
5357
]
5458
)
5559
def test_create_key_monitor(

0 commit comments

Comments
 (0)