TUI for the Clash/Mihomo proxy
Project description
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.11 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 = "http://www.gstatic.com/generate_204"
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clashctl-0.1.0.tar.gz.
File metadata
- Download URL: clashctl-0.1.0.tar.gz
- Upload date:
- Size: 54.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
666cb0b7660cc2d7ceab1f57d633400ced95e5cacd585e8075eefe87076b2c5d
|
|
| MD5 |
eb421babeb2abc2c0464e570067e9b03
|
|
| BLAKE2b-256 |
4fc2dc48a33939ff4c786023a5e7f256996bd10d89f877b8c1c19da2713cbc6f
|
Provenance
The following attestation bundles were made for clashctl-0.1.0.tar.gz:
Publisher:
python-publish.yml on frezcirno/clashctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clashctl-0.1.0.tar.gz -
Subject digest:
666cb0b7660cc2d7ceab1f57d633400ced95e5cacd585e8075eefe87076b2c5d - Sigstore transparency entry: 1440118869
- Sigstore integration time:
-
Permalink:
frezcirno/clashctl@cc01462358e1879745e7f08b44c469d77fac57b2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/frezcirno
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@cc01462358e1879745e7f08b44c469d77fac57b2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file clashctl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: clashctl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 49.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d7dabc943c92b73c93142eb2dbd98cde97f1eb48fdc917568c7f4a5cd372dee
|
|
| MD5 |
9c91bd19b82400acf0daf6bd4e231461
|
|
| BLAKE2b-256 |
6f3831372bb15a33b5ae036f6532f2026760fc29e3272bc9fb9d2282c9d82e3a
|
Provenance
The following attestation bundles were made for clashctl-0.1.0-py3-none-any.whl:
Publisher:
python-publish.yml on frezcirno/clashctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clashctl-0.1.0-py3-none-any.whl -
Subject digest:
5d7dabc943c92b73c93142eb2dbd98cde97f1eb48fdc917568c7f4a5cd372dee - Sigstore transparency entry: 1440118892
- Sigstore integration time:
-
Permalink:
frezcirno/clashctl@cc01462358e1879745e7f08b44c469d77fac57b2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/frezcirno
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@cc01462358e1879745e7f08b44c469d77fac57b2 -
Trigger Event:
release
-
Statement type: