clashctl
Python 3 rewrite of clashctl — a Textual-based TUI for the Clash (and mihomo) external-controller REST API.
A live dashboard with five tabs: Status / Proxies / Rules / Conns / Logs. Server management lives entirely inside the TUI — no add/use/del subcommands.
┌─ Status ───────────────────────────────────────────────────┐
│ ⇉ Connections 12 │ ▲ ▁▁▃▂▄▇▆▆▅▇▆▆▆▆▇▆▅▆▇▆▆▇▇▆▇▆▆▇ │
│ ▲ Upload 12.4 KiB/s │ ▼ ▁▂▁▁▂▁▁▁▁▂▂▂▂▁▂▂▁▂▁▂▁▂▁▁▂▁▂▁ │
│ ▼ Download 1.2 MiB/s │ │
│ ▲ Avg. 9.1 KiB/s │ max ↑ 487 KiB/s ↓ 4.3 MiB/s │
│ ▼ Avg. 812 KiB/s │ │
│ Clash Ver. v1.18.0 │ │
└────────────────────────────────────────────────────────────┘
Status
MVP complete — all five tabs functional, in-TUI server management, 213 unit tests, headless E2E verified.
Notable differences from the Rust original:
- Real instantaneous connection speeds (the Rust version reports
total_bytes / seconds_since_connection_start, which mis-represents short bursts; here speeds are computed by diffing successive/connectionspolls). ConnSortis actually implemented (the Rust counterpart wastodo!()for all 11 keys).- Config in TOML instead of RON.
- Server management lives in the TUI, not in CLI subcommands.
Install
git clone https://github.com/frezcirno/clashctl.git
cd clashctl
pip install -e ".[dev]" # editable + test deps
# or, with uv:
uv pip install -e ".[dev]"
Python 3.8 or newer is required.
Run
clashctl # default config path
clashctl -c /path/to/config.toml # custom path
clashctl --debug # verbose log to /tmp/clashctl.log
On first launch with no configured server, a modal pops up and walks you through adding one. The form probes /version against the supplied URL before saving so typos and bad secrets are caught early.
Keybindings
Global (any tab)
| Key | Action |
|---|---|
q, Ctrl+C |
Quit |
1–5 |
Switch tabs |
Ctrl+S |
Open the server picker (add / delete / switch) |
Tab-specific
| Tab | Key | Action |
|---|---|---|
| Proxies | t |
Test latency for the focused group's normal members (parallel, capped at 8) |
| Proxies | Enter |
On a member of a Selector group: switch to it (PUT /proxies/<group>) |
| Proxies | s / Shift+s |
Cycle member sort (name / type / delay × asc / desc) |
| Rules | s / Shift+s |
Cycle sort (payload / type / proxy × asc / desc) |
| Rules | Space |
Toggle HOLD mode (cursor pinned across refreshes) |
| Rules | ↑↓ |
Navigate (in HOLD) |
| Rules | Esc |
Exit HOLD |
| Conns | s / Shift+s |
Cycle sort across 11 keys (host, ▼, ▲, speeds, time, rule, chain, src, dst, type) × asc/desc |
| Conns | Space, Esc, ↑↓ |
Same HOLD behavior as Rules |
Server picker
| Key | Action |
|---|---|
a |
Add a new server (opens form) |
d |
Delete the focused server |
u / Enter |
Use the focused server (swaps connections) |
Esc |
Close (or quit on first run) |
Config
Stored at ~/.config/clashctl/config.toml by default (respects $XDG_CONFIG_HOME; on macOS it lands under ~/Library/Application Support/clashctl/). Override the path with -c <path> or CLASHCTL_CONFIG_PATH=....
Schema:
using = "http://127.0.0.1:9090"
[[servers]]
name = "local"
url = "http://127.0.0.1:9090"
secret = "" # empty == no auth
[[servers]]
name = "remote"
url = "https://proxy.example.com:9090"
secret = "hunter2"
[ui]
test_url = "https://www.gstatic.com/generate_204" # fallback; groups' own `url` wins
test_timeout_ms = 5000
log_buffer = 2000 # in-memory log line cap
traffic_history = 500 # sparkline samples
refresh_slow_secs = 5.0 # /version, /configs, /proxies, /rules
refresh_fast_secs = 1.0 # /connections
[ui.sort]
proxies = { by = "delay", order = "asc" }
rules = { by = "payload", order = "asc" }
connections = { by = "time", order = "desc" }
The TOML can be hand-edited; the TUI rewrites it via tomli_w whenever you add/use/delete a server.
Architecture
src/clashctl/
├── api/ # httpx async client + line-delimited JSON streams
├── models/ # pydantic v2 models for every Clash payload
├── state/ # AppState (single source of truth) + sort + speed tracker
├── config/ # TOML load/save + AppConfig schema
├── tui/ # Textual app, screens, widgets, polling worker
└── utils/ # bytesize / duration helpers
- Pollers are plain asyncio classes that post
Messages up the Textual tree; the App reduces messages intoAppStateand tells the active screen what to refresh. - Layering rule:
api/,models/,state/,config/never import Textual — they're unit-testable without spinning up anApp. - Hidden tabs:
RichLog.write()produces empty strips when the widget has no width, so the Logs tab buffers inAppState.logsand replays on tab activation.
Test
pytest # 213 tests, ~9s
ruff check src tests # lint
mypy src # type check (strict mode)
Tests are split between pure unit coverage (models, sort, speed tracker, config round-trip) and Textual run_test() harnesses for widgets and screens. The API client is exercised against httpx.MockTransport with recorded fixtures under tests/fixtures/.
License
MIT — see LICENSE.
Metadata
Release files for clashctl 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| clashctl-0.1.4.tar.gz | 56.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clashctl-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.8 kB
Release files / clashctl-0.1.4.tar.gz
| Download URL | clashctl-0.1.4.tar.gz |
|---|---|
| Size | 56.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d47331fdc40fe550b246769b5acb87fbf156cfe2a30b82d4519c181583c407e
|
|
BLAKE2b-256 checksum How to use checksums |
3711b7e7f1d9898937cf33849f3dd655497a86526532fe296b6c5d3dcfe4e393
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / clashctl-0.1.4-py3-none-any.whl
| Download URL | clashctl-0.1.4-py3-none-any.whl |
|---|---|
| Size | 50.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef52ab0cd97805b159930231116cf9c135451094f066bf616b36ea255607fec0
|
|
BLAKE2b-256 checksum How to use checksums |
8da2d23b78a101473f13308d9353d5df3bb746727e56de55039d8dcdaf99b511
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log