Skip to content

Commit 3d342be

Browse files
authored
Merge pull request #8 from eeriemyxi/impl-click
Migrate from `argparse` to `click` I have tried to keep things backwards compatible or at least very similar to the previous interface. I have not properly tested everything yet and I have definitely not checked if it is actually backwards compatible.
2 parents 006af08 + 1043c3b commit 3d342be

File tree

6 files changed

+266
-239
lines changed

6 files changed

+266
-239
lines changed

pyproject.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@ requires-python = ">=3.10"
1010
dependencies = [
1111
"evdev>=1.7.1 ; sys_platform == 'linux'",
1212
"keyboard>=0.13.5 ; sys_platform == 'win32'",
13+
"click>=8.1.8",
1314
"kisesi>=0.3.0",
1415
"pyglet>=2.0.20",
1516
"websockets>=14.1",
1617
]
1718

1819
[project.scripts]
19-
mvibes = "mechvibes_lite.__main__:main"
20+
mvibes = "mechvibes_lite:cli"
2021

21-
[build-system]
22-
requires = ["hatchling"]
23-
build-backend = "hatchling.build"
24-
25-
[dependency-groups]
22+
[project.optional-dependencies]
2623
dev = [
2724
"mkdocs>=1.6.1",
2825
"mkdocs-material>=9.5.49",
2926
]
27+
28+
[build-system]
29+
requires = ["hatchling"]
30+
build-backend = "hatchling.build"

src/mechvibes_lite/__init__.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import asyncio
2+
import importlib.metadata
3+
import pathlib
4+
import sys
5+
import threading
6+
7+
import click
8+
import kisesi
9+
import websockets
10+
11+
from mechvibes_lite import const, struct, util, wskey
12+
13+
kisesi.basic_config(level="INFO")
14+
log = kisesi.get_logger(__name__)
15+
16+
17+
async def start_wskey_listener(theme_path, wskey_host, wskey_port) -> None:
18+
from mechvibes_lite import audio
19+
20+
log.debug(f"Started wskey listener {wskey_host=} {wskey_port=}")
21+
theme = struct.Theme.from_config(theme_path / "config.json", theme_path)
22+
keyplayer = audio.KeyPlayer(theme)
23+
24+
async for websocket in websockets.connect(
25+
f"ws://{wskey_host}:{wskey_port}", ping_timeout=None
26+
):
27+
try:
28+
log.debug("Got a connection...")
29+
async for message in websocket:
30+
code = int(message)
31+
log.debug("Received scan code: %s", code)
32+
keyplayer.play_for(int(code))
33+
except websockets.exceptions.ConnectionClosed:
34+
log.warning("Connection to wskey was lost.")
35+
continue
36+
37+
38+
def ensure_required_flags(args) -> bool:
39+
required_flags = ["theme_dir", "theme_folder_name", "wskey_host", "wskey_port"]
40+
if sys.platform == "linux":
41+
required_flags.append("event_id")
42+
43+
for flag in required_flags:
44+
if not args[flag]:
45+
log.error(f"'--{util.to_kebab(flag)}' flag was expected but not provided.")
46+
return False
47+
48+
return True
49+
50+
51+
@click.group(help=const.APP_DESCRIPTION, epilog=const.APP_EPILOG)
52+
@click.option(
53+
"--log-level",
54+
"-L",
55+
type=click.Choice(["DEBUG", "INFO", "WARNING", "CRITICAL", "ERROR"]),
56+
default="INFO",
57+
)
58+
@click.option(
59+
"--no-config",
60+
help="Do not read config file from standard locations. "
61+
"Will error if you don't provide required configuration as flags instead.",
62+
is_flag=True,
63+
default=False,
64+
)
65+
@click.option(
66+
"--no-wskey", help="Do not run the Wskey daemon.", is_flag=True, default=False
67+
)
68+
@click.option(
69+
"--with-config",
70+
help="Load this configuration instead of the one at the standard location. Can be - for stdin.",
71+
type=click.File("r"),
72+
default=None,
73+
)
74+
@click.option(
75+
"--theme-dir",
76+
type=click.Path(exists=True, path_type=pathlib.Path),
77+
help="Path to the theme directory.",
78+
default=None,
79+
)
80+
@click.option(
81+
"--theme-folder-name",
82+
help="Name of the theme folder. This folder must exist under --theme-dir.",
83+
default=None,
84+
)
85+
@click.option(
86+
"--wskey-host",
87+
help="The hostname to use to connect to the Wskey daemon.",
88+
default=None,
89+
)
90+
@click.option(
91+
"--wskey-port",
92+
type=int,
93+
help="The port to use to connect to the Wskey daemon.",
94+
default=None,
95+
)
96+
@click.option(
97+
"--event-id",
98+
help="The port to use for the Wskey server started when --no-wskey is *not* provided.",
99+
default=None,
100+
)
101+
@click.version_option(
102+
version=importlib.metadata.version(const.APP_NAME),
103+
prog_name=const.APP_NAME.replace("-", " ").title(),
104+
)
105+
@click.pass_context
106+
def cli(
107+
ctx,
108+
log_level: str,
109+
no_config: bool,
110+
no_wskey: bool,
111+
with_config: str,
112+
theme_dir: pathlib.Path,
113+
theme_folder_name: str,
114+
wskey_host: str,
115+
wskey_port: int,
116+
event_id: str,
117+
):
118+
log.set_level(log_level)
119+
120+
ctx.config = None
121+
122+
if no_config and not with_config:
123+
ret = ensure_required_flags(ctx.params)
124+
if not ret:
125+
sys.exit(1)
126+
ctx.config = struct.Configuration(
127+
theme_dir, theme_folder_name, wskey_host, wskey_port
128+
)
129+
log.debug(
130+
f"[NO_CONFIG, NO_WITH_CONFIG] Constructed {ctx.config} since {no_config=} and {with_config=}"
131+
)
132+
elif with_config:
133+
ctx.config = struct.Configuration.from_config(with_config.read())
134+
log.debug(
135+
f"[WITH_CONFIG] Constructed {ctx.config} since {no_config=} and {with_config=}"
136+
)
137+
else:
138+
ctx.config = struct.Configuration.from_config(const.CONFIG_PATH.read_text())
139+
log.debug(
140+
f"Constructed {ctx.config} from {const.CONFIG_PATH=} since {no_config=} and {with_config=}"
141+
)
142+
143+
if theme_dir:
144+
log.debug(f"Setting {ctx.config.theme_dir=} to {theme_dir=}")
145+
ctx.config.theme_dir = theme_dir
146+
if theme_folder_name:
147+
log.debug(f"Setting {ctx.config.theme_folder_name=} to {theme_folder_name=}")
148+
ctx.config.theme_folder_name = theme_folder_name
149+
if wskey_host:
150+
log.debug(f"Setting {ctx.config.wskey_host=} to {wskey_host=}")
151+
ctx.config.wskey_host = wskey_host
152+
if wskey_port:
153+
log.debug(f"Setting {ctx.config.wskey_port=} to {wskey_port=}")
154+
ctx.config.wskey_port = wskey_port
155+
if event_id:
156+
if sys.platform != "linux":
157+
log.error("--event-id flag is only for Linux users.")
158+
sys.exit(1)
159+
log.debug(f"Setting {ctx.config.event_id=} to {event_id=}")
160+
ctx.config.event_id = util.parse_event_id(event_id)
161+
162+
log.debug(f"Finalised configuration: {ctx.config=}")
163+
164+
165+
@cli.command(name="daemon", help="Run the keyboard input player as a daemon.")
166+
@click.pass_context
167+
def mvibes_daemon(ctx):
168+
root_ctx = ctx.find_root()
169+
config = root_ctx.config
170+
171+
if not root_ctx.params["no_wskey"]:
172+
# [INFO] I don't know why I didn't use asyncio tasks instead.
173+
# And I am too scared to find out why.
174+
thread = threading.Thread(
175+
target=asyncio.run,
176+
args=[wskey.start(config.wskey_host, config.wskey_port, config.event_path)],
177+
daemon=True,
178+
)
179+
thread.start()
180+
181+
import pyglet.app
182+
import pyglet.media
183+
184+
log.debug("Starting daemon")
185+
pyglet.options["headless"] = True
186+
thread = threading.Thread(
187+
target=asyncio.run,
188+
args=[start_wskey_listener(config.theme_path, config.wskey_host, config.wskey_port)],
189+
daemon=True,
190+
)
191+
thread.start()
192+
193+
try:
194+
pyglet.app.run()
195+
except KeyboardInterrupt:
196+
sys.stdout.write("\n")
197+
log.info(f"Exiting {const.APP_NAME}...")
198+
sys.exit()
199+
200+
201+
@cli.group(help="WebSocket server for sending keyboard input.", name="wskey")
202+
@click.option("--host", help="The hostname for the Wskey daemon.", default=None)
203+
@click.option("--port", type=int, help="The port for the Wskey daemon.", default=None)
204+
@click.option(
205+
"--event-id", help="The event id to use for the Wskey daemon.", default=None
206+
)
207+
def cmd_wskey(host: str, port: int, event_id: str):
208+
if event_id and sys.platform != "linux":
209+
log.error("--event-id flag is only for Linux users.")
210+
sys.exit(1)
211+
212+
213+
@cmd_wskey.command(name="daemon", help="Run a Wskey daemon.")
214+
@click.pass_context
215+
def wskey_daemon(ctx):
216+
params = ctx.parent.params
217+
config = ctx.find_root().config
218+
event_id = params["event_id"]
219+
220+
host = params["host"] or config.wskey_host
221+
port = params["port"] or config.wskey_port
222+
event_path = util.parse_event_id(event_id) if event_id else config.event_path
223+
224+
try:
225+
asyncio.run(wskey.start(host, port, event_path))
226+
except KeyboardInterrupt:
227+
sys.stdout.write("\n")
228+
log.info("Closing wskey...")
229+
sys.exit()
230+
231+
232+
if __name__ == "__main__":
233+
main()

0 commit comments

Comments
 (0)