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/messages.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""User-facing text, kept in one place so it's easy to review (and to translate, should that ever happen).
|
|
2
|
+
|
|
3
|
+
Messages from customrp-core (validation, presets, connection errors) are passed through as they are.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# --- Config file -----------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
CONFIG_MISSING = "No config file at {path}. Create one with: customrp-cli init"
|
|
9
|
+
CONFIG_UNREADABLE = "Can't read {path}: {reason}"
|
|
10
|
+
CONFIG_NOT_UTF8 = "{path} isn't a UTF-8 text file"
|
|
11
|
+
CONFIG_BAD_TOML = "{path} isn't valid TOML: {reason}"
|
|
12
|
+
CONFIG_EXISTS = "{path} already exists. Use --force to overwrite it."
|
|
13
|
+
CONFIG_WRITTEN = "Wrote {path}"
|
|
14
|
+
CONFIG_INVALID = "The config file has errors:"
|
|
15
|
+
|
|
16
|
+
UNKNOWN_OPTION = "unknown option"
|
|
17
|
+
DID_YOU_MEAN = " (did you mean '{suggestion}'?)"
|
|
18
|
+
EXPECTED_TYPE = "expected {expected}, got {actual}"
|
|
19
|
+
EXPECTED_CHOICE = "{value!r} isn't one of: {choices}"
|
|
20
|
+
AT_LEAST = "must be at least {minimum}"
|
|
21
|
+
TOO_MANY_BUTTONS = "Discord shows at most 2 buttons; {count} are set"
|
|
22
|
+
NEEDS_TIME = "needs a time as well as a date, like {example}"
|
|
23
|
+
BAD_DATETIME = "{value!r} isn't a date and time, like {example}"
|
|
24
|
+
PIPE_HINT = '"auto" or a number from 0 to 9'
|
|
25
|
+
|
|
26
|
+
TYPE_NAMES = {
|
|
27
|
+
str: "a string", bool: "true or false", int: "a whole number", float: "a number", dict: "a table",
|
|
28
|
+
list: "an array",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# --- Commands --------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
CHECK_OK = "No problems found in {path}"
|
|
34
|
+
CHECK_SUMMARY = "{errors} error(s), {warnings} warning(s) in {path}"
|
|
35
|
+
ERROR_PREFIX = "error"
|
|
36
|
+
WARNING_PREFIX = "warning"
|
|
37
|
+
|
|
38
|
+
ALREADY_RUNNING = "customrp-cli is already running (PID {pid}). Stop it with: customrp-cli stop"
|
|
39
|
+
NOT_RUNNING = "customrp-cli isn't running"
|
|
40
|
+
RUNNING = "customrp-cli is running (PID {pid})"
|
|
41
|
+
STATUS_STATE = " State: {state}{detail}"
|
|
42
|
+
STATUS_USER = " Discord user: {user}"
|
|
43
|
+
STATUS_CONFIG = " Config: {path}"
|
|
44
|
+
STATUS_SINCE = " Running since: {since}"
|
|
45
|
+
STOP_SENT = "Asked customrp-cli (PID {pid}) to stop"
|
|
46
|
+
STOPPED = "Stopped"
|
|
47
|
+
STOP_TIMEOUT = "customrp-cli (PID {pid}) didn't stop within {timeout} seconds"
|
|
48
|
+
|
|
49
|
+
PRESET_IMPORTED = "Imported {preset} into {path}"
|
|
50
|
+
PRESET_EXPORTED = "Exported the presence from {path} to {preset}"
|
|
51
|
+
PRESET_KEPT_SETTINGS = "Kept the [connection], [run] and [logging] settings from the existing file"
|
|
52
|
+
FILE_EXISTS = "{path} already exists. Use --force to overwrite it."
|
|
53
|
+
CANT_WRITE = "Can't write {path}: {reason}"
|
|
54
|
+
|
|
55
|
+
ASSETS_NONE = "Application {app_id} has no art assets"
|
|
56
|
+
ASSETS_HEADER = "Art assets of application {app_id}:"
|
|
57
|
+
ASSETS_PAGE = "Manage them at {url}"
|
|
58
|
+
|
|
59
|
+
# --- Running ---------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
RUN_STARTING = "Starting customrp-cli {version} with {path}"
|
|
62
|
+
RUN_STOPPED = "Stopped"
|
|
63
|
+
RUN_GAVE_UP = "Gave up connecting to Discord. Is the Discord desktop app running?"
|
|
64
|
+
RUN_RELOADED = "Reloaded {path}"
|
|
65
|
+
RUN_RELOAD_FAILED = "Not reloading {path}, it has errors:"
|
|
66
|
+
RUN_RESTART_NEEDED = "Changes to [connection] and [logging] take effect after a restart"
|
customrp_cli/paths.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Where the config file and runtime files live on each platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
APP_DIR_NAME = "customrp-cli"
|
|
10
|
+
CONFIG_FILE_NAME = "config.toml"
|
|
11
|
+
|
|
12
|
+
#: Overrides the config file location (the -c option still wins)
|
|
13
|
+
CONFIG_ENV = "CUSTOMRP_CLI_CONFIG"
|
|
14
|
+
#: Overrides the directory for the lock, PID and status files
|
|
15
|
+
STATE_ENV = "CUSTOMRP_CLI_STATE_DIR"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _env_dir(name: str, fallback: Path) -> Path:
|
|
19
|
+
value = os.environ.get(name)
|
|
20
|
+
return Path(value) if value else fallback
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def config_dir() -> Path:
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
return _env_dir("APPDATA", Path.home() / "AppData" / "Roaming") / APP_DIR_NAME
|
|
26
|
+
if sys.platform == "darwin":
|
|
27
|
+
return Path.home() / "Library" / "Application Support" / APP_DIR_NAME
|
|
28
|
+
return _env_dir("XDG_CONFIG_HOME", Path.home() / ".config") / APP_DIR_NAME
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def state_dir() -> Path:
|
|
32
|
+
override = os.environ.get(STATE_ENV)
|
|
33
|
+
if override:
|
|
34
|
+
return Path(override)
|
|
35
|
+
if sys.platform == "win32":
|
|
36
|
+
return _env_dir("LOCALAPPDATA", Path.home() / "AppData" / "Local") / APP_DIR_NAME
|
|
37
|
+
if sys.platform == "darwin":
|
|
38
|
+
return Path.home() / "Library" / "Application Support" / APP_DIR_NAME
|
|
39
|
+
return _env_dir("XDG_STATE_HOME", Path.home() / ".local" / "state") / APP_DIR_NAME
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_config_path() -> Path:
|
|
43
|
+
override = os.environ.get(CONFIG_ENV)
|
|
44
|
+
return Path(override) if override else config_dir() / CONFIG_FILE_NAME
|
customrp_cli/runner.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""The ``run`` command: show the presence until asked to stop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import threading
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from customrp_core import PresenceSession, SessionState, display_name
|
|
13
|
+
|
|
14
|
+
from . import __version__, messages as msg
|
|
15
|
+
from .config import Config, ConfigError, load_config
|
|
16
|
+
from .instance import Instance
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger("customrp_cli")
|
|
19
|
+
|
|
20
|
+
EXIT_OK = 0
|
|
21
|
+
EXIT_FAILED = 1
|
|
22
|
+
|
|
23
|
+
# How often to check for stop requests and config changes, in seconds
|
|
24
|
+
TICK = 0.5
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigWatcher:
|
|
28
|
+
"""Notices when the config file changes, once it has stopped changing (editors save in steps)."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, path: Path):
|
|
31
|
+
self.path = path
|
|
32
|
+
self._loaded = self._signature()
|
|
33
|
+
self._pending = None
|
|
34
|
+
|
|
35
|
+
def _signature(self):
|
|
36
|
+
try:
|
|
37
|
+
stat = self.path.stat()
|
|
38
|
+
return stat.st_mtime_ns, stat.st_size
|
|
39
|
+
except OSError:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def changed(self) -> bool:
|
|
43
|
+
current = self._signature()
|
|
44
|
+
if current == self._loaded or current is None:
|
|
45
|
+
self._pending = None
|
|
46
|
+
return False
|
|
47
|
+
if current != self._pending:
|
|
48
|
+
self._pending = current # Wait one more tick to see that it has settled
|
|
49
|
+
return False
|
|
50
|
+
self._loaded = current
|
|
51
|
+
self._pending = None
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def mark_loaded(self):
|
|
55
|
+
self._loaded = self._signature()
|
|
56
|
+
self._pending = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _log_problems(problems):
|
|
60
|
+
for problem in problems:
|
|
61
|
+
if problem.is_error:
|
|
62
|
+
log.error("%s", problem)
|
|
63
|
+
else:
|
|
64
|
+
log.warning("%s", problem)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Runner:
|
|
68
|
+
def __init__(self, config: Config, instance: Instance, *, watch: bool):
|
|
69
|
+
self.config = config
|
|
70
|
+
self.instance = instance
|
|
71
|
+
self.watch = watch and config.watch_config and config.path is not None
|
|
72
|
+
self.stop_event = threading.Event()
|
|
73
|
+
self.reload_event = threading.Event()
|
|
74
|
+
self.started = datetime.now().astimezone()
|
|
75
|
+
self.session = PresenceSession(
|
|
76
|
+
config.presence, pipe=config.pipe, retry_interval=config.retry_interval,
|
|
77
|
+
max_retries=config.max_retries, proxy_discord_cdn=config.proxy_discord_cdn,
|
|
78
|
+
on_state_change=self._state_changed)
|
|
79
|
+
|
|
80
|
+
def _state_changed(self, state: SessionState, message: str | None):
|
|
81
|
+
# The library logs connection problems itself; this only records the state for "status"
|
|
82
|
+
user = self.session.user
|
|
83
|
+
self.instance.write_status({
|
|
84
|
+
"pid": os.getpid(),
|
|
85
|
+
"state": state.value,
|
|
86
|
+
"message": message,
|
|
87
|
+
"user": display_name(user) if user else None,
|
|
88
|
+
"config": str(self.config.path) if self.config.path else None,
|
|
89
|
+
"started": self.started.isoformat(timespec="seconds"),
|
|
90
|
+
"updated": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
def _install_signal_handlers(self):
|
|
94
|
+
previous = {}
|
|
95
|
+
|
|
96
|
+
def stop(signum, frame):
|
|
97
|
+
self.stop_event.set()
|
|
98
|
+
|
|
99
|
+
def reload(signum, frame):
|
|
100
|
+
self.reload_event.set()
|
|
101
|
+
|
|
102
|
+
# SIGINT stays as KeyboardInterrupt. SIGTERM is what service managers send;
|
|
103
|
+
# SIGBREAK is Ctrl+Break on Windows; SIGHUP traditionally means "reload".
|
|
104
|
+
for name, handler in (("SIGTERM", stop), ("SIGBREAK", stop), ("SIGHUP", reload)):
|
|
105
|
+
signum = getattr(signal, name, None)
|
|
106
|
+
if signum is not None:
|
|
107
|
+
try:
|
|
108
|
+
previous[signum] = signal.signal(signum, handler)
|
|
109
|
+
except (ValueError, OSError): # Not on the main thread, or not supported
|
|
110
|
+
pass
|
|
111
|
+
return previous
|
|
112
|
+
|
|
113
|
+
def _reload(self, watcher: ConfigWatcher | None):
|
|
114
|
+
path = self.config.path
|
|
115
|
+
if watcher:
|
|
116
|
+
watcher.mark_loaded()
|
|
117
|
+
try:
|
|
118
|
+
new = load_config(path)
|
|
119
|
+
except ConfigError as e:
|
|
120
|
+
log.error(msg.RUN_RELOAD_FAILED.format(path=path))
|
|
121
|
+
for problem in e.problems:
|
|
122
|
+
log.error(" %s", problem)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
problems = new.problems()
|
|
126
|
+
if any(p.is_error for p in problems):
|
|
127
|
+
log.error(msg.RUN_RELOAD_FAILED.format(path=path))
|
|
128
|
+
_log_problems(problems)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
_log_problems(problems)
|
|
132
|
+
if not self.config.same_runtime_settings(new):
|
|
133
|
+
log.warning(msg.RUN_RESTART_NEEDED)
|
|
134
|
+
self.session.proxy_discord_cdn = new.proxy_discord_cdn
|
|
135
|
+
self.watch = self.watch and new.watch_config
|
|
136
|
+
self.config = new
|
|
137
|
+
log.info(msg.RUN_RELOADED.format(path=path))
|
|
138
|
+
self.session.update(new.presence)
|
|
139
|
+
|
|
140
|
+
def run(self) -> int:
|
|
141
|
+
previous_handlers = self._install_signal_handlers()
|
|
142
|
+
watcher = ConfigWatcher(self.config.path) if self.watch else None
|
|
143
|
+
self.session.start()
|
|
144
|
+
try:
|
|
145
|
+
while not self.session.wait(TICK):
|
|
146
|
+
if self.stop_event.is_set() or self.instance.stop_requested():
|
|
147
|
+
break
|
|
148
|
+
if self.reload_event.is_set() or (self.watch and watcher and watcher.changed()):
|
|
149
|
+
self.reload_event.clear()
|
|
150
|
+
self._reload(watcher)
|
|
151
|
+
except KeyboardInterrupt:
|
|
152
|
+
pass
|
|
153
|
+
finally:
|
|
154
|
+
self.session.stop()
|
|
155
|
+
self.session.wait(5)
|
|
156
|
+
for signum, handler in previous_handlers.items():
|
|
157
|
+
signal.signal(signum, handler)
|
|
158
|
+
|
|
159
|
+
if self.session.gave_up:
|
|
160
|
+
log.error(msg.RUN_GAVE_UP)
|
|
161
|
+
return EXIT_FAILED
|
|
162
|
+
log.info(msg.RUN_STOPPED)
|
|
163
|
+
return EXIT_OK
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def run(config: Config, instance: Instance, *, watch: bool = True) -> int:
|
|
167
|
+
"""Runs until stopped. The caller has already set up logging and checked the config for errors."""
|
|
168
|
+
if not instance.acquire():
|
|
169
|
+
log.error(msg.ALREADY_RUNNING.format(pid=instance.running_pid() or "unknown"))
|
|
170
|
+
return EXIT_FAILED
|
|
171
|
+
try:
|
|
172
|
+
log.info(msg.RUN_STARTING.format(version=__version__, path=config.path))
|
|
173
|
+
return Runner(config, instance, watch=watch).run()
|
|
174
|
+
finally:
|
|
175
|
+
instance.release()
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: customrp-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Custom Discord Rich Presence from the command line, configured with a TOML file. An independent alternative to CustomRP for Linux, macOS and Windows.
|
|
5
|
+
Author: KeiraOMG0
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/KeiraOMG0/customrp-cli
|
|
8
|
+
Project-URL: Documentation, https://github.com/KeiraOMG0/customrp-cli/tree/main/docs
|
|
9
|
+
Project-URL: Issues, https://github.com/KeiraOMG0/customrp-cli/issues
|
|
10
|
+
Keywords: discord,rich presence,rpc,customrp,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Communications :: Chat
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: customrp-core<0.2,>=0.1.2
|
|
22
|
+
Requires-Dist: tomli>=1.1; python_version < "3.11"
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# customrp-cli
|
|
28
|
+
|
|
29
|
+
Show a custom Discord Rich Presence ("Playing ...", with your own text, images, buttons and timer) from
|
|
30
|
+
the command line, configured with a TOML file. It runs natively on **Linux**, **macOS** and **Windows**.
|
|
31
|
+
|
|
32
|
+
customrp-cli is a command-line alternative to [CustomRP](https://www.customrp.xyz), the Windows app by
|
|
33
|
+
maximmax42. It does what CustomRP does, minus the window. It's built on
|
|
34
|
+
[customrp-core](https://github.com/KeiraOMG0/customrp-core), a library that does the heavy lifting.
|
|
35
|
+
|
|
36
|
+
> **Independent project.** customrp-cli isn't made or maintained by CustomRP's developer, and isn't an
|
|
37
|
+
> official replacement. Please report problems [here](https://github.com/KeiraOMG0/customrp-cli/issues),
|
|
38
|
+
> not on the CustomRP repository.
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
$ customrp-cli init
|
|
42
|
+
Wrote /home/you/.config/customrp-cli/config.toml
|
|
43
|
+
$ nano ~/.config/customrp-cli/config.toml
|
|
44
|
+
$ customrp-cli run
|
|
45
|
+
2026-01-01 12:00:00 INFO Starting customrp-cli 0.1.0 with /home/you/.config/customrp-cli/config.toml
|
|
46
|
+
2026-01-01 12:00:00 INFO Connecting to Discord with application ID 896771305108553788
|
|
47
|
+
2026-01-01 12:00:00 INFO Connected to /run/user/1000/discord-ipc-0
|
|
48
|
+
2026-01-01 12:00:00 INFO Connected to Discord as you
|
|
49
|
+
2026-01-01 12:00:00 INFO Presence updated
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Why
|
|
53
|
+
|
|
54
|
+
CustomRP is a Windows Forms application, so it only runs on Windows. Linux users have had to rely on Wine
|
|
55
|
+
or RPC bridges. The GUI is convenient, but the useful part, telling Discord what to show, doesn't need a
|
|
56
|
+
window. customrp-cli does that part natively, from a config file. That also makes it work on headless
|
|
57
|
+
setups, under a service manager, and in dotfiles.
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- Everything CustomRP can show: activity type (Playing, Listening, Watching, Competing), name, details
|
|
62
|
+
and state lines with optional links, party size, large and small images (asset names or URLs) with
|
|
63
|
+
tooltips and links, two buttons, and which field appears in the member list
|
|
64
|
+
- All five CustomRP timestamp modes: since last connection, since startup, since last update, local
|
|
65
|
+
time, and custom start/end (including countdowns)
|
|
66
|
+
- Live reload: save the config file and the presence updates within a second, without reconnecting
|
|
67
|
+
- Checks your config the way Discord will (length limits in bytes, one-character fields, broken URLs,
|
|
68
|
+
image URLs that are too long) and names the exact option at fault
|
|
69
|
+
- Waits for Discord: if Discord isn't running yet, it retries every 10 seconds, and reconnects if
|
|
70
|
+
Discord restarts
|
|
71
|
+
- Imports and exports CustomRP's `.crp` presets, so you can move presets between this and the Windows app
|
|
72
|
+
- Lists the art assets uploaded to your Discord application
|
|
73
|
+
- One instance per user, with `status` and `stop` commands that work on every platform
|
|
74
|
+
- Runs as a systemd user service, a launchd agent, or a Windows scheduled task; ready-made files are in
|
|
75
|
+
[`contrib/`](https://github.com/KeiraOMG0/customrp-cli/tree/main/contrib)
|
|
76
|
+
- No GUI, no Wine, and nothing beyond Python (plus `tomli` on Python 3.9 and 3.10)
|
|
77
|
+
|
|
78
|
+
## How it differs from CustomRP
|
|
79
|
+
|
|
80
|
+
| CustomRP (Windows app) | customrp-cli |
|
|
81
|
+
|---|---|
|
|
82
|
+
| Window with text boxes and dropdowns | A TOML config file ([reference](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/configuration.md)) |
|
|
83
|
+
| Click "Update presence" | Save the file; it reloads automatically |
|
|
84
|
+
| Minimizes to the tray | Runs in a terminal or as a background service |
|
|
85
|
+
| "Run on startup" checkbox | systemd, launchd or Task Scheduler ([guide](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/running-in-the-background.md)) |
|
|
86
|
+
| Gives up after 30 attempts to reach Discord | Retries forever by default (`max_retries`) |
|
|
87
|
+
| Translated into many languages | English only for now |
|
|
88
|
+
| Built-in update checker | Update with `pipx upgrade customrp-cli` |
|
|
89
|
+
|
|
90
|
+
More in [docs/migrating-from-customrp.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/migrating-from-customrp.md).
|
|
91
|
+
|
|
92
|
+
## Supported systems
|
|
93
|
+
|
|
94
|
+
| System | Status |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Linux | Tests run in CI on Ubuntu, and were run on Ubuntu 24.04 (WSL), against a fake Discord over a real Unix socket, including running as a separate process. **Not yet tested against the real Discord client on Linux.** |
|
|
97
|
+
| Windows 10/11 | Tests run in CI and on Windows 11 against a fake Discord over a named pipe, including background mode with `pythonw`. The underlying library has completed a handshake with the real Discord client. |
|
|
98
|
+
| macOS | Tests run in CI on macOS against a fake Discord over a real Unix socket, including running as a separate process. **Not yet tested against the real Discord client on macOS**, and the launchd setup is untested. |
|
|
99
|
+
|
|
100
|
+
You need:
|
|
101
|
+
|
|
102
|
+
- Python 3.9 or newer
|
|
103
|
+
- The Discord **desktop** app (or a client that provides Discord's IPC socket, like Vesktop) running on
|
|
104
|
+
the same computer, as the same user. The browser version can't show Rich Presence from other programs.
|
|
105
|
+
|
|
106
|
+
## Installation
|
|
107
|
+
|
|
108
|
+
The easiest way is [pipx](https://pipx.pypa.io), which puts customrp-cli in its own environment and
|
|
109
|
+
on your `PATH`:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
pipx install customrp-cli
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Don't have pipx? It needs Python 3.11 or newer. The short version:
|
|
116
|
+
|
|
117
|
+
| System | Command |
|
|
118
|
+
|---|---|
|
|
119
|
+
| macOS | `brew install pipx` |
|
|
120
|
+
| Ubuntu, Debian | `sudo apt install pipx` |
|
|
121
|
+
| Fedora | `sudo dnf install pipx` |
|
|
122
|
+
| Arch | `sudo pacman -S python-pipx` |
|
|
123
|
+
| Windows | `py -m pip install --user pipx` |
|
|
124
|
+
|
|
125
|
+
Then run `pipx ensurepath` (on Windows: `py -m pipx ensurepath`) and open a new terminal. Step-by-step
|
|
126
|
+
instructions for every system are in [docs/installation.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/installation.md#installing-pipx).
|
|
127
|
+
|
|
128
|
+
Or with a virtual environment and `requirements.txt`:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
git clone https://github.com/KeiraOMG0/customrp-cli.git
|
|
132
|
+
cd customrp-cli
|
|
133
|
+
python3 -m venv .venv
|
|
134
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
135
|
+
pip install -r requirements.txt # Installs customrp-core from GitHub
|
|
136
|
+
pip install .
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Plain `pip install customrp-cli` inside a virtual environment works too.
|
|
140
|
+
|
|
141
|
+
Details, including Windows notes and uninstalling, are in [docs/installation.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/installation.md).
|
|
142
|
+
|
|
143
|
+
## Quick start
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
customrp-cli init # Writes a config file with every option, then prints its path
|
|
147
|
+
# Edit the file: set details, state, images, buttons...
|
|
148
|
+
customrp-cli check # Points out mistakes
|
|
149
|
+
customrp-cli preview # Shows exactly what will be sent to Discord
|
|
150
|
+
customrp-cli run # Shows the presence until you press Ctrl+C
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
To use your own application name and images, create an application in the
|
|
154
|
+
[Discord Developer Portal](https://discord.com/developers/applications) and put its ID in
|
|
155
|
+
`application_id`. CustomRP's [setup guide](https://docs.customrp.xyz/setting-up) walks through it.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
A minimal config:
|
|
160
|
+
|
|
161
|
+
```toml
|
|
162
|
+
[presence]
|
|
163
|
+
application_id = "123456789012345678"
|
|
164
|
+
details = "Exploring the Linux desktop"
|
|
165
|
+
state = "Day 3"
|
|
166
|
+
timestamps = "since_startup"
|
|
167
|
+
|
|
168
|
+
[presence.large_image]
|
|
169
|
+
key = "penguin" # An asset name from your application, or an image URL
|
|
170
|
+
text = "Tux says hi"
|
|
171
|
+
|
|
172
|
+
[[presence.buttons]]
|
|
173
|
+
label = "My website"
|
|
174
|
+
url = "https://example.com"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Every option is optional. [`examples/config.toml`](https://github.com/KeiraOMG0/customrp-cli/blob/main/examples/config.toml) is a complete, commented
|
|
178
|
+
example, and [docs/configuration.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/configuration.md) explains each option.
|
|
179
|
+
|
|
180
|
+
The config file is at:
|
|
181
|
+
|
|
182
|
+
| System | Default path |
|
|
183
|
+
|---|---|
|
|
184
|
+
| Linux | `~/.config/customrp-cli/config.toml` (or `$XDG_CONFIG_HOME/customrp-cli/`) |
|
|
185
|
+
| macOS | `~/Library/Application Support/customrp-cli/config.toml` |
|
|
186
|
+
| Windows | `%APPDATA%\customrp-cli\config.toml` |
|
|
187
|
+
|
|
188
|
+
Use another file with `-c path/to/file.toml`, or set `CUSTOMRP_CLI_CONFIG`.
|
|
189
|
+
|
|
190
|
+
## Commands
|
|
191
|
+
|
|
192
|
+
| Command | Does |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `customrp-cli init [--force]` | Create a config file listing every option |
|
|
195
|
+
| `customrp-cli check` | Check the config for mistakes (alias: `validate`) |
|
|
196
|
+
| `customrp-cli preview` | Print the activity that would be sent, as JSON |
|
|
197
|
+
| `customrp-cli run [--no-watch]` | Show the presence until stopped |
|
|
198
|
+
| `customrp-cli status` | Show whether it's running, its connection state and Discord user |
|
|
199
|
+
| `customrp-cli stop` | Stop the running instance |
|
|
200
|
+
| `customrp-cli import FILE.crp [--force]` | Replace the presence in the config with a CustomRP preset |
|
|
201
|
+
| `customrp-cli export FILE.crp [--force]` | Save the configured presence as a CustomRP preset |
|
|
202
|
+
| `customrp-cli assets [--application-id ID]` | List the images uploaded to your Discord application |
|
|
203
|
+
| `customrp-cli paths` | Show where the config and runtime files are |
|
|
204
|
+
|
|
205
|
+
Options for every command: `-c/--config FILE`, `-v/--verbose` (debug output), `-q/--quiet` (errors
|
|
206
|
+
only), `-h/--help`, plus `-V/--version`. Exit codes and more examples are in
|
|
207
|
+
[docs/commands.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/commands.md).
|
|
208
|
+
|
|
209
|
+
customrp-cli never asks questions or waits for input, so it's safe to use from scripts, cron and service
|
|
210
|
+
managers.
|
|
211
|
+
|
|
212
|
+
## Running in the background
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
# Linux (systemd), after installing with pipx:
|
|
216
|
+
mkdir -p ~/.config/systemd/user
|
|
217
|
+
curl -o ~/.config/systemd/user/customrp-cli.service \
|
|
218
|
+
https://raw.githubusercontent.com/KeiraOMG0/customrp-cli/main/contrib/systemd/customrp-cli.service
|
|
219
|
+
systemctl --user daemon-reload
|
|
220
|
+
systemctl --user enable --now customrp-cli
|
|
221
|
+
journalctl --user -u customrp-cli -f # Watch the log
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
macOS (launchd) and Windows (Task Scheduler) are covered in
|
|
225
|
+
[docs/running-in-the-background.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/running-in-the-background.md).
|
|
226
|
+
|
|
227
|
+
## Logging and troubleshooting
|
|
228
|
+
|
|
229
|
+
`run` logs to the terminal (stderr). Add `-v` to see every message exchanged with Discord, or set
|
|
230
|
+
`[logging] file` to also log to a file (rotated at 1 MB).
|
|
231
|
+
|
|
232
|
+
| Problem | Try |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `Couldn't find a running Discord client` | Start the Discord desktop app. With Flatpak or Snap, see [troubleshooting](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/troubleshooting.md#discord-isnt-found). |
|
|
235
|
+
| `Discord returned an error (4000): Invalid Client ID` | `application_id` is wrong |
|
|
236
|
+
| Runs fine but nothing shows on your profile | In Discord: User Settings → Activity Privacy → turn on sharing your activity |
|
|
237
|
+
| `already running` | `customrp-cli status`, then `customrp-cli stop` |
|
|
238
|
+
|
|
239
|
+
More in [docs/troubleshooting.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/troubleshooting.md).
|
|
240
|
+
|
|
241
|
+
## Known limitations
|
|
242
|
+
|
|
243
|
+
- English only.
|
|
244
|
+
- Only the presence settings are shared with CustomRP, through `.crp` presets; CustomRP's own settings
|
|
245
|
+
file isn't read.
|
|
246
|
+
- One instance per user. Showing two presences at once isn't supported.
|
|
247
|
+
- Discord's socket locations for Flatpak, Snap and Vesktop come from other Rich Presence projects and
|
|
248
|
+
haven't been verified here.
|
|
249
|
+
- Changes to `[connection]` and `[logging]` need a restart; `[presence]` changes apply live.
|
|
250
|
+
|
|
251
|
+
## Documentation
|
|
252
|
+
|
|
253
|
+
[docs/](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/index.md): [installation](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/installation.md) · [configuration](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/configuration.md) ·
|
|
254
|
+
[commands](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/commands.md) · [background](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/running-in-the-background.md) ·
|
|
255
|
+
[troubleshooting](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/troubleshooting.md) · [migrating from CustomRP](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/migrating-from-customrp.md) ·
|
|
256
|
+
[design](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/design.md) · [development](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/development.md)
|
|
257
|
+
|
|
258
|
+
## Running the tests
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
pip install -e ".[test]"
|
|
262
|
+
pytest
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The tests don't need Discord; they use a fake Discord server from customrp-core. See
|
|
266
|
+
[docs/development.md](https://github.com/KeiraOMG0/customrp-cli/blob/main/docs/development.md).
|
|
267
|
+
|
|
268
|
+
## Contributing
|
|
269
|
+
|
|
270
|
+
Issues and pull requests are welcome. Please:
|
|
271
|
+
|
|
272
|
+
- open an issue first for anything big
|
|
273
|
+
- add tests for behaviour changes (`pytest` must pass)
|
|
274
|
+
- keep it small: no new dependencies or abstractions without a concrete need
|
|
275
|
+
- put changes to how presences work in [customrp-core](https://github.com/KeiraOMG0/customrp-core),
|
|
276
|
+
and changes to the command line, config and running in this repository
|
|
277
|
+
|
|
278
|
+
## Credits
|
|
279
|
+
|
|
280
|
+
- [CustomRP](https://github.com/maximmax42/Discord-CustomRP) by maximmax42 (MIT). The features, rules and
|
|
281
|
+
preset format come from it, and its developer was open to an independent alternative.
|
|
282
|
+
- CustomRP's default Discord application and its image proxy (`customrp.xyz/proxy`, used for Discord-hosted
|
|
283
|
+
images) belong to CustomRP's developer.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT. See [LICENSE](https://github.com/KeiraOMG0/customrp-cli/blob/main/LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
customrp_cli/__init__.py,sha256=9LemoxjFCBTWcEmSw74DNVqMREFmYSLK8a7_RdVYmuk,124
|
|
2
|
+
customrp_cli/__main__.py,sha256=t2N7Y3qgy6N_RgKOdQ1xrlSVO1EnFDrHsG2XTq0_hP4,146
|
|
3
|
+
customrp_cli/cli.py,sha256=tuGiRrjcYuI7TIA2IBQPmjW7TBqTqp2b3M84hegyJVM,11294
|
|
4
|
+
customrp_cli/config.py,sha256=vs6miMehx9p2ZeI7h9j8K8ucJ2NoMev3w29Bn6ZJgzk,19175
|
|
5
|
+
customrp_cli/instance.py,sha256=BzOQPg_yXalX9t-c9W6AmPGVlhb3HUknfpb2m5wmzHM,4382
|
|
6
|
+
customrp_cli/logs.py,sha256=bKgWfumedxWWABIwGH0w2c_XBPH7CUBUFyAJpRM8HsI,1656
|
|
7
|
+
customrp_cli/messages.py,sha256=dGxRVy-_9jpU9j1VV39q1-cPTA7un2GWwfDATiQBvVg,2954
|
|
8
|
+
customrp_cli/paths.py,sha256=0OIeVqzNL5s_vsxmU_I1GCS0PqDmd9-3DXo0OzUsRsY,1472
|
|
9
|
+
customrp_cli/runner.py,sha256=51Pmig8dCd_qwotQuddJSIg8fbzVQttnUVki4QOU6dc,6137
|
|
10
|
+
customrp_cli-0.1.1.dist-info/licenses/LICENSE,sha256=g9gZVj6t0ROL4NWd7zN6OVYbdopsJm6pDruVEkTKJyU,1159
|
|
11
|
+
customrp_cli-0.1.1.dist-info/METADATA,sha256=Iv2oAsxpAHTqG6lvzszXGUHLwTySti-bQo1ENzgRS1E,13923
|
|
12
|
+
customrp_cli-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
13
|
+
customrp_cli-0.1.1.dist-info/entry_points.txt,sha256=jZ263Ar7gNJJTPY8Zn6Vm9eMT0Cw5JQuEnO7gxXAoGY,55
|
|
14
|
+
customrp_cli-0.1.1.dist-info/top_level.txt,sha256=qCyamvoYKt4H-sM1gH9sdVyamg3K60HsHZLZfLRyYXA,13
|
|
15
|
+
customrp_cli-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KeiraOMG0
|
|
4
|
+
Copyright (c) 2020-2026 maximmax42 (CustomRP, whose features and preset format this follows)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
customrp_cli
|