customrp-cli 0.1.1__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.
- customrp_cli/__init__.py +3 -0
- customrp_cli/__main__.py +8 -0
- customrp_cli/cli.py +335 -0
- customrp_cli/config.py +474 -0
- customrp_cli/instance.py +139 -0
- customrp_cli/logs.py +48 -0
- customrp_cli/messages.py +66 -0
- customrp_cli/paths.py +44 -0
- customrp_cli/runner.py +175 -0
- customrp_cli-0.1.1.dist-info/METADATA +287 -0
- customrp_cli-0.1.1.dist-info/RECORD +15 -0
- customrp_cli-0.1.1.dist-info/WHEEL +5 -0
- customrp_cli-0.1.1.dist-info/entry_points.txt +2 -0
- customrp_cli-0.1.1.dist-info/licenses/LICENSE +22 -0
- customrp_cli-0.1.1.dist-info/top_level.txt +1 -0
customrp_cli/__init__.py
ADDED
customrp_cli/__main__.py
ADDED
customrp_cli/cli.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Command-line parsing and the commands other than ``run``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from customrp_core import AssetsError, PresetError, build_activity, load_preset, save_preset
|
|
14
|
+
from customrp_core import __version__ as core_version
|
|
15
|
+
from customrp_core.assets import assets_page_url, fetch_assets
|
|
16
|
+
|
|
17
|
+
from . import __version__, logs, messages as msg, paths, runner
|
|
18
|
+
from .config import Config, ConfigError, load_config, write_config
|
|
19
|
+
from .instance import Instance
|
|
20
|
+
|
|
21
|
+
EXIT_OK = 0
|
|
22
|
+
EXIT_FAILED = 1
|
|
23
|
+
EXIT_CONFIG = 2 # Also what argparse uses for usage errors
|
|
24
|
+
EXIT_NOT_RUNNING = 3 # status only, like LSB init scripts
|
|
25
|
+
|
|
26
|
+
DESCRIPTION = """\
|
|
27
|
+
Show a custom Discord Rich Presence, configured with a TOML file.
|
|
28
|
+
An independent command-line alternative to CustomRP for Linux, macOS and Windows."""
|
|
29
|
+
|
|
30
|
+
EPILOG = """\
|
|
31
|
+
Start with "customrp-cli init", edit the config file it creates, then "customrp-cli run".
|
|
32
|
+
Documentation: https://github.com/KeiraOMG0/customrp-cli"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _out(text: str = ""):
|
|
36
|
+
print(text)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _err(text: str):
|
|
40
|
+
print(text, file=sys.stderr)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _common_options(parser: argparse.ArgumentParser, suppress: bool):
|
|
44
|
+
"""-c/-v/-q work before and after the command; the copy on subcommands mustn't reset them."""
|
|
45
|
+
default = argparse.SUPPRESS if suppress else None
|
|
46
|
+
parser.add_argument("-c", "--config", type=Path, metavar="FILE", default=default,
|
|
47
|
+
help=f"config file (default: {paths.default_config_path()}; or set ${paths.CONFIG_ENV})")
|
|
48
|
+
parser.add_argument("-v", "--verbose", action="count", default=argparse.SUPPRESS if suppress else 0,
|
|
49
|
+
help="show debug output")
|
|
50
|
+
parser.add_argument("-q", "--quiet", action="store_true", default=argparse.SUPPRESS if suppress else False,
|
|
51
|
+
help="only show errors")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
55
|
+
parser = argparse.ArgumentParser(prog="customrp-cli", description=DESCRIPTION, epilog=EPILOG,
|
|
56
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
57
|
+
parser.add_argument("-V", "--version", action="version",
|
|
58
|
+
version=f"customrp-cli {__version__} (customrp-core {core_version})")
|
|
59
|
+
_common_options(parser, suppress=False)
|
|
60
|
+
|
|
61
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
62
|
+
_common_options(common, suppress=True)
|
|
63
|
+
|
|
64
|
+
commands = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
65
|
+
|
|
66
|
+
def command(name, help_text, **kwargs):
|
|
67
|
+
return commands.add_parser(name, help=help_text, description=help_text, parents=[common], **kwargs)
|
|
68
|
+
|
|
69
|
+
p = command("init", "create a config file with every option and its default")
|
|
70
|
+
p.add_argument("--force", action="store_true", help="overwrite an existing file")
|
|
71
|
+
|
|
72
|
+
command("check", "check the config file for mistakes", aliases=["validate"])
|
|
73
|
+
|
|
74
|
+
command("preview", "print the activity that would be sent to Discord, as JSON")
|
|
75
|
+
|
|
76
|
+
p = command("run", "show the presence until stopped (Ctrl+C, SIGTERM or 'customrp-cli stop')")
|
|
77
|
+
p.add_argument("--no-watch", action="store_true", help="don't reload the config file when it changes")
|
|
78
|
+
|
|
79
|
+
p = command("stop", "stop the running instance")
|
|
80
|
+
p.add_argument("--timeout", type=float, default=10, metavar="SECONDS",
|
|
81
|
+
help="how long to wait for it to stop (default: 10)")
|
|
82
|
+
|
|
83
|
+
command("status", "say whether an instance is running, and how it's doing")
|
|
84
|
+
|
|
85
|
+
p = command("import", "replace the presence in the config file with a CustomRP preset (.crp)")
|
|
86
|
+
p.add_argument("preset", type=Path, help="the .crp file")
|
|
87
|
+
p.add_argument("--force", action="store_true", help="overwrite the existing config file's presence")
|
|
88
|
+
|
|
89
|
+
p = command("export", "save the configured presence as a CustomRP preset (.crp)")
|
|
90
|
+
p.add_argument("preset", type=Path, help="the .crp file to write")
|
|
91
|
+
p.add_argument("--force", action="store_true", help="overwrite an existing file")
|
|
92
|
+
|
|
93
|
+
p = command("assets", "list the art assets uploaded to the Discord application")
|
|
94
|
+
p.add_argument("--application-id", metavar="ID", help="application to look up (default: from the config file)")
|
|
95
|
+
|
|
96
|
+
command("paths", "show where the config and runtime files are")
|
|
97
|
+
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- Helpers -------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _config_path(args) -> Path:
|
|
104
|
+
path = args.config or paths.default_config_path()
|
|
105
|
+
return path.expanduser().absolute()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _load(args) -> Config | None:
|
|
109
|
+
"""Loads the config, printing problems. Returns None if it can't be used."""
|
|
110
|
+
try:
|
|
111
|
+
return load_config(_config_path(args))
|
|
112
|
+
except ConfigError as e:
|
|
113
|
+
if e.about_values:
|
|
114
|
+
_err(msg.CONFIG_INVALID)
|
|
115
|
+
for problem in e.problems:
|
|
116
|
+
_err(f" {problem}")
|
|
117
|
+
else:
|
|
118
|
+
_err(e.problems[0])
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _print_problems(problems):
|
|
123
|
+
for problem in problems:
|
|
124
|
+
prefix = msg.ERROR_PREFIX if problem.is_error else msg.WARNING_PREFIX
|
|
125
|
+
_err(f"{prefix}: {problem}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _instance() -> Instance:
|
|
129
|
+
return Instance(paths.state_dir())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --- Commands ------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def cmd_init(args) -> int:
|
|
135
|
+
path = _config_path(args)
|
|
136
|
+
try:
|
|
137
|
+
write_config(Config(), path, force=args.force)
|
|
138
|
+
except ConfigError as e:
|
|
139
|
+
_err(e.problems[0])
|
|
140
|
+
return EXIT_CONFIG
|
|
141
|
+
_out(msg.CONFIG_WRITTEN.format(path=path))
|
|
142
|
+
return EXIT_OK
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_check(args) -> int:
|
|
146
|
+
config = _load(args)
|
|
147
|
+
if config is None:
|
|
148
|
+
return EXIT_CONFIG
|
|
149
|
+
problems = config.problems()
|
|
150
|
+
_print_problems(problems)
|
|
151
|
+
errors = sum(p.is_error for p in problems)
|
|
152
|
+
if problems:
|
|
153
|
+
_out(msg.CHECK_SUMMARY.format(errors=errors, warnings=len(problems) - errors, path=config.path))
|
|
154
|
+
else:
|
|
155
|
+
_out(msg.CHECK_OK.format(path=config.path))
|
|
156
|
+
return EXIT_CONFIG if errors else EXIT_OK
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_preview(args) -> int:
|
|
160
|
+
config = _load(args)
|
|
161
|
+
if config is None:
|
|
162
|
+
return EXIT_CONFIG
|
|
163
|
+
problems = config.problems()
|
|
164
|
+
_print_problems(problems)
|
|
165
|
+
if any(p.is_error for p in problems):
|
|
166
|
+
return EXIT_CONFIG
|
|
167
|
+
|
|
168
|
+
now = datetime.now().astimezone()
|
|
169
|
+
activity, refresh_in = build_activity(config.presence, connected_at=now, started_at=now,
|
|
170
|
+
proxy_discord_cdn=config.proxy_discord_cdn)
|
|
171
|
+
_out(json.dumps({"application_id": config.presence.effective_application_id, "activity": activity},
|
|
172
|
+
ensure_ascii=False, indent=2))
|
|
173
|
+
return EXIT_OK
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_run(args) -> int:
|
|
177
|
+
config = _load(args)
|
|
178
|
+
if config is None:
|
|
179
|
+
return EXIT_CONFIG
|
|
180
|
+
|
|
181
|
+
logs.setup(logs.level_for(config.log_level, args.verbose, args.quiet), config.log_file)
|
|
182
|
+
problems = config.problems()
|
|
183
|
+
for problem in problems:
|
|
184
|
+
(logging.error if problem.is_error else logging.warning)("%s", problem)
|
|
185
|
+
if any(p.is_error for p in problems):
|
|
186
|
+
return EXIT_CONFIG
|
|
187
|
+
|
|
188
|
+
return runner.run(config, _instance(), watch=not args.no_watch)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def cmd_stop(args) -> int:
|
|
192
|
+
instance = _instance()
|
|
193
|
+
pid = instance.running_pid()
|
|
194
|
+
if pid is None:
|
|
195
|
+
_out(msg.NOT_RUNNING)
|
|
196
|
+
return EXIT_OK
|
|
197
|
+
|
|
198
|
+
instance.request_stop()
|
|
199
|
+
_out(msg.STOP_SENT.format(pid=pid or "unknown"))
|
|
200
|
+
deadline = time.monotonic() + args.timeout
|
|
201
|
+
while time.monotonic() < deadline:
|
|
202
|
+
if instance.running_pid() is None:
|
|
203
|
+
_out(msg.STOPPED)
|
|
204
|
+
return EXIT_OK
|
|
205
|
+
time.sleep(0.1)
|
|
206
|
+
_err(msg.STOP_TIMEOUT.format(pid=pid or "unknown", timeout=args.timeout))
|
|
207
|
+
return EXIT_FAILED
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_status(args) -> int:
|
|
211
|
+
instance = _instance()
|
|
212
|
+
pid = instance.running_pid()
|
|
213
|
+
if pid is None:
|
|
214
|
+
_out(msg.NOT_RUNNING)
|
|
215
|
+
return EXIT_NOT_RUNNING
|
|
216
|
+
|
|
217
|
+
_out(msg.RUNNING.format(pid=pid or "unknown"))
|
|
218
|
+
status = instance.read_status()
|
|
219
|
+
if status.get("state"):
|
|
220
|
+
detail = f" ({status['message']})" if status.get("message") else ""
|
|
221
|
+
_out(msg.STATUS_STATE.format(state=status["state"], detail=detail))
|
|
222
|
+
if status.get("user"):
|
|
223
|
+
_out(msg.STATUS_USER.format(user=status["user"]))
|
|
224
|
+
if status.get("config"):
|
|
225
|
+
_out(msg.STATUS_CONFIG.format(path=status["config"]))
|
|
226
|
+
if status.get("started"):
|
|
227
|
+
_out(msg.STATUS_SINCE.format(since=status["started"]))
|
|
228
|
+
return EXIT_OK
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_import(args) -> int:
|
|
232
|
+
path = _config_path(args)
|
|
233
|
+
try:
|
|
234
|
+
presence = load_preset(args.preset)
|
|
235
|
+
except PresetError as e:
|
|
236
|
+
_err(str(e))
|
|
237
|
+
return EXIT_FAILED
|
|
238
|
+
|
|
239
|
+
config = Config()
|
|
240
|
+
if path.exists():
|
|
241
|
+
if not args.force:
|
|
242
|
+
_err(msg.CONFIG_EXISTS.format(path=path))
|
|
243
|
+
return EXIT_CONFIG
|
|
244
|
+
try:
|
|
245
|
+
config = load_config(path)
|
|
246
|
+
_out(msg.PRESET_KEPT_SETTINGS)
|
|
247
|
+
except ConfigError:
|
|
248
|
+
pass # Broken anyway; replace it whole
|
|
249
|
+
|
|
250
|
+
config.presence = presence
|
|
251
|
+
try:
|
|
252
|
+
write_config(config, path, force=True)
|
|
253
|
+
except ConfigError as e:
|
|
254
|
+
_err(e.problems[0])
|
|
255
|
+
return EXIT_CONFIG
|
|
256
|
+
|
|
257
|
+
_print_problems(config.problems())
|
|
258
|
+
_out(msg.PRESET_IMPORTED.format(preset=args.preset, path=path))
|
|
259
|
+
return EXIT_OK
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cmd_export(args) -> int:
|
|
263
|
+
config = _load(args)
|
|
264
|
+
if config is None:
|
|
265
|
+
return EXIT_CONFIG
|
|
266
|
+
if args.preset.exists() and not args.force:
|
|
267
|
+
_err(msg.FILE_EXISTS.format(path=args.preset))
|
|
268
|
+
return EXIT_FAILED
|
|
269
|
+
try:
|
|
270
|
+
save_preset(config.presence, args.preset)
|
|
271
|
+
except PresetError as e:
|
|
272
|
+
_err(str(e))
|
|
273
|
+
return EXIT_FAILED
|
|
274
|
+
_out(msg.PRESET_EXPORTED.format(path=config.path, preset=args.preset))
|
|
275
|
+
return EXIT_OK
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def cmd_assets(args) -> int:
|
|
279
|
+
app_id = args.application_id
|
|
280
|
+
if not app_id:
|
|
281
|
+
config = _load(args)
|
|
282
|
+
if config is None:
|
|
283
|
+
return EXIT_CONFIG
|
|
284
|
+
app_id = config.presence.effective_application_id
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
assets = fetch_assets(app_id)
|
|
288
|
+
page = assets_page_url(app_id)
|
|
289
|
+
except AssetsError as e:
|
|
290
|
+
_err(str(e))
|
|
291
|
+
return EXIT_FAILED
|
|
292
|
+
|
|
293
|
+
if not assets:
|
|
294
|
+
_out(msg.ASSETS_NONE.format(app_id=app_id))
|
|
295
|
+
else:
|
|
296
|
+
_out(msg.ASSETS_HEADER.format(app_id=app_id))
|
|
297
|
+
for asset in sorted(assets, key=lambda a: a.name.lower()):
|
|
298
|
+
_out(f" {asset.name}")
|
|
299
|
+
_out(msg.ASSETS_PAGE.format(url=page))
|
|
300
|
+
return EXIT_OK
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def cmd_paths(args) -> int:
|
|
304
|
+
state = paths.state_dir()
|
|
305
|
+
_out(f"Config file: {_config_path(args)}")
|
|
306
|
+
_out(f"Config directory: {paths.config_dir()}")
|
|
307
|
+
_out(f"State directory: {state} (lock, PID and status files)")
|
|
308
|
+
return EXIT_OK
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
COMMANDS = {
|
|
312
|
+
"init": cmd_init,
|
|
313
|
+
"check": cmd_check,
|
|
314
|
+
"validate": cmd_check,
|
|
315
|
+
"preview": cmd_preview,
|
|
316
|
+
"run": cmd_run,
|
|
317
|
+
"stop": cmd_stop,
|
|
318
|
+
"status": cmd_status,
|
|
319
|
+
"import": cmd_import,
|
|
320
|
+
"export": cmd_export,
|
|
321
|
+
"assets": cmd_assets,
|
|
322
|
+
"paths": cmd_paths,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def main(argv: list[str] | None = None) -> int:
|
|
327
|
+
parser = build_parser()
|
|
328
|
+
args = parser.parse_args(argv)
|
|
329
|
+
if not args.command:
|
|
330
|
+
parser.print_help()
|
|
331
|
+
return EXIT_CONFIG
|
|
332
|
+
|
|
333
|
+
# Commands other than run log only library warnings (run sets up logging from the config)
|
|
334
|
+
logs.setup(logs.level_for("warning", args.verbose, args.quiet))
|
|
335
|
+
return COMMANDS[args.command](args)
|