Skip to main content

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 /connections polls).
  • ConnSort is actually implemented (the Rust counterpart was todo!() 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 into AppState and tells the active screen what to refresh.
  • Layering rule: api/, models/, state/, config/ never import Textual — they're unit-testable without spinning up an App.
  • Hidden tabs: RichLog.write() produces empty strips when the widget has no width, so the Logs tab buffers in AppState.logs and 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)

Source distribution for clashctl 0.1.4
File Size Uploaded
clashctl-0.1.4.tar.gz 56.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for clashctl 0.1.4
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.4 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page