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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ customrp-cli = customrp_cli.cli:main
@@ -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