This document details the core features implemented in Surge TUI.
- UI Interactions
- Policy Latency Testing
- Notification System
- Developer Tools
- Policy Group Management
- Data Caching
- Connection Management
- Feature Toggles
- Request Details Enhancement
- DNS Cache Management
| Key | Function | Description |
|---|---|---|
q |
Quit program | Exit directly |
r |
Refresh data | Manually refresh snapshot data |
1-5 |
Switch views | Overview/Policies/Requests/Connections/DNS |
↑/↓ |
Navigate list | Move selection up/down |
Enter |
Enter/Confirm | Enter policy group or switch policy |
Esc |
Back/Close | Exit policy group or close popup |
t / T |
Test latency | Non-blocking test all policy latencies |
m / M |
Toggle mode | Cycle through Direct/Proxy/Rule |
i / I |
Toggle MITM | Switch MITM status in Overview view |
c / C |
Toggle Capture | Switch traffic capture in Overview view |
k / K |
Kill connection | Terminate selected connection in Connections view |
f / F |
Flush cache | Flush DNS cache in DNS view |
n / N |
Notification history | View complete notification history |
` |
DevTools | Open developer debugging tools |
s / S |
Start Surge | Only available when Alert prompts |
- Surge running status
- HTTP API availability
- Current outbound mode (supports
mkey quick toggle) - MITM status (supports
ikey quick toggle) - Capture status (supports
ckey quick toggle) - System statistics
Left: Policy Group List
- Policy group name (blue bold)
- Currently selected policy (green)
- Latency display (color-coded):
< 100ms- Cyan (fast)100-300ms- Yellow (medium)> 300ms- Red (slow)
- Status markers:
✓- Available✗- Unavailable
Right: Policies within Policy Group
- Policy name
- Protocol type (color-coded)
- Latency and availability status
- Nested policy groups recursively display final policy latency
- Recent request records (limited to 50)
- URL, policy, traffic statistics
- Request details panel:
- HTTP Body markers (whether request/response has data)
- Connection logs (Notes) highlighting
- TLS, DNS, rule matching details
- Current active network connections (limited to 50)
- Supports
kkey to terminate selected connection (with confirmation dialog) - Connection details panel:
- URL, process, traffic statistics
- HTTP Body markers
- Connection log highlighting
- DNS cache record list
- Shows domain name, IP address, TTL (remaining time)
- Supports search filtering (
/key) - Supports
fkey to flush DNS cache
Non-blocking Architecture
- Press
Tkey to trigger test, UI remains responsive - Uses
tokio::spawnto execute tests in background - Passes test results through
mpscmessage channel
Test Flow
User presses T → Background task starts → Notify "Testing"
↓
Execute surge-cli test-all-policies
↓
Parse test results (policy name, latency, status)
↓
Send completion message → UI auto-refreshes → Display latency data
Real-time Updates
- UI immediately redraws after test completion, no need to wait for user interaction
- Status bar on right displays latest test result notification
Recursive Policy Chain Resolution
When policy groups contain other policy groups (e.g., Proxy → US_Servers → us-server-1), the system will:
- Recursively find the final real policy
- Display the final policy's latency data
- Prevent circular references (max 10 levels of recursion)
Example
Policy Group View:
Proxy → US_Servers (176ms) ← Shows final policy latency
AIProxy → jp-server-1 (206ms)
Policy Group Details (after entering Proxy):
✓ Direct [Available]
AIProxy 281ms ← Nested group shows its selected policy latency
US_Servers 176ms ← Nested group shows its selected policy latency
surge-cli Mode
/Applications/Surge.app/Contents/Applications/surge-cli test-all-policiesOutput Format Parsing
ProxyName: RTT 123 ms, Total 456 ms → Success, extract RTT value
ProxyName: Failed → Failure, mark unavailable
Two-tier Display
- Status bar real-time notifications - Bottom right shows latest notification (displays time within 60 seconds)
- History popup - Press
Nkey to view complete history (max 50 entries)
Notification Levels
Info(ℹ) - Cyan - Information promptSuccess(✓) - Green - Operation successfulError(✗) - Red - Error warning
DevTools and History
[2026-02-15 16:59:35] ✓ Test completed: 5/10 available
Status Bar Notifications (within 60 seconds)
✓ Test completed (16:59:35)
- Retain latest 50 notifications
- Automatically delete old notifications exceeding limit
- Won't auto-hide (unless replaced by new notifications)
Press ` key to open DevTools panel (covers bottom 70%).
Log Levels
DEBUG- Dark gray - Debug informationINFO- Cyan - Regular logsWARN- Yellow - Warning informationERROR- Red - Error information
Log Format
[2026-02-15 16:59:35] INFO Background test task started
[2026-02-15 16:59:38] INFO Test completed: group=AIProxy, total=5, available=4
Feature Capabilities
- Display latest 100 log entries
- Support auto line-wrapping
- Absolute timestamps (year-month-day-hour-minute-second)
- Press
Escto close
Use Cases
- Debug policy name matching issues
- View test result details
- Diagnose API call errors
- Track background task status
Operation Flow
- In Policies view (View 2), press
↑/↓to select policy group - Press
Enterto enter policy group - Right side displays all policies in that group
- Press
↑/↓to select target policy - Press
Enterto switch to selected policy - Press
Escto return to policy group list
API Call
client.select_policy_group(group_name, policy_name).awaitUI Feedback
- Immediately refresh data
- Update policy group's
selectedfield - Mark selected policy with
✓in policy list
Right-side policy list displays different colors based on protocol type:
| Protocol | Color |
|---|---|
| Shadowsocks | Blue |
| VMess | Magenta |
| Trojan | Yellow |
| DIRECT | Green |
| REJECT | Red |
| Other | Gray |
Problem
refresh()completely replacessnapshot- Causes test results to be lost after refresh or switching pages
Solution
// App state
policy_test_cache: HashMap<String, PolicyDetail>
// Cache when test completes
for policy in &results {
self.policy_test_cache.insert(policy.name.clone(), policy.clone());
}
// Restore cache during refresh
if !self.policy_test_cache.is_empty() {
self.snapshot.policies = self.policy_test_cache.values().cloned().collect();
}Effect
- Test results permanently saved (until overwritten by new test)
- Data refresh doesn't affect latency display
- Can still see test results after switching views
Configuration
[ui]
refresh_interval = 1 # secondsRefresh Strategy
- Only refresh when user inactive and timeout occurs
- List remains stable during user operations (won't suddenly jump)
- Maintain test result cache
Problem
- Fixed column width causes long text overlap
- CJK characters (Chinese) occupy 2 display widths
Solution
// Calculate dynamically based on terminal width
fn calculate_policy_column_widths(area_width: u16) -> (usize, usize, usize) {
let available = (area_width as usize).saturating_sub(fixed_overhead);
let status_width = 10;
let name_width = (remaining * 0.6) as usize;
let protocol_width = remaining - name_width;
(name_width, protocol_width, status_width)
}
// Use unicode-width to calculate display width
use unicode_width::UnicodeWidthStr;
fn truncate_text(text: &str, max_width: usize) -> String {
let mut width = 0;
for (idx, ch) in text.char_indices() {
width += UnicodeWidthStr::width(ch.encode_utf8(&mut [0; 4]));
if width > max_width {
return format!("{}...", &text[..idx]);
}
}
text.to_string()
}Dependency
unicode-width = "0.2"Event Loop
loop {
terminal.draw(|f| self.render(f))?; // 1. Render UI
while let Ok(msg) = self.test_rx.try_recv() { // 2. Process background messages
self.handle_test_message(msg);
}
if has_test_message { // 3. Immediate redraw
terminal.draw(|f| self.render(f))?;
}
if event::poll(refresh_interval)? { // 4. Wait for event or timeout
self.handle_key(event::read()?).await;
} else {
self.refresh().await; // 5. Refresh on timeout
}
}TestMessage Enum
enum TestMessage {
Started, // Test started
Completed { // Test completed
group_name: String,
results: Vec<PolicyDetail>,
},
Failed { error: String }, // Test failed
}Background Task
tokio::spawn(async move {
let _ = tx.send(TestMessage::Started).await;
match client.test_all_policies_with_latency().await {
Ok(results) => {
tx.send(TestMessage::Completed { group_name, results }).await;
}
Err(e) => {
tx.send(TestMessage::Failed { error: e.to_string() }).await;
}
}
});[dependencies]
tokio = { version = "1.45", features = ["full"] }
ratatui = "0.29"
crossterm = "0.28"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
chrono = "0.4"
unicode-width = "0.2"Key Dependency Roles
tokio- Async runtime, supports non-blocking testsratatui- TUI frameworkcrossterm- Terminal controlchrono- Date/time handling (absolute timestamps)unicode-width- CJK character width calculation
Operation Flow
- In Connections view (View 4), press
↑/↓to select connection - Press
kkey to popup confirmation dialog - Confirmation dialog shows connection details:
- Target URL/Host
- Process path
- Upload/download traffic
- Press
Enterto confirm termination, pressEscto cancel
Security Design
- Destructive operations require confirmation to prevent accidents
- Display detailed information to help user double-check
- Supports use in both grouped and normal modes
API Call
client.kill_connection(connection_id).awaitOverview View Display
- MITM status:
✓ Enabled/✗ Disabled - Capture status:
✓ Enabled/✗ Disabled - Inline shortcut hints (btop style)
Quick Toggle
- Press
ikey to toggle MITM status - Press
ckey to toggle Capture status - Auto refresh status after successful operation
- Display notification feedback
Use Cases
- Quickly disable MITM when accessing bank websites
- Quickly enable Capture when debugging APIs
- No need to leave TUI to toggle features
Refresh Strategy
- Use real-time refresh strategy, not optimistic updates
- Immediately refresh snapshot after successful API call
- Avoid inconsistent state due to network latency
Feature Capabilities
- Display first 10 connection logs to avoid overly long panels
- Auto-highlight key tags
- Timestamps displayed in gray
- Add blank lines every 3 logs to enhance readability
Highlighted Tags
| Tag | Color | Description |
|---|---|---|
[Connection] |
Cyan | Connection protocol and status |
[TLS] |
Green | TLS handshake, SNI, cipher suites |
[DNS] |
Magenta | DNS resolution details |
[Rule] |
Yellow | Rule matching path |
[Socket] |
Blue | Socket connection details |
[HTTP] |
Light Green | HTTP connection info |
[Policy] |
Light Yellow | Policy decision path |
Log Example
14:49:36.011669 [Connection] Incoming proxy protocol: HTTP
14:49:36.012836 [TLS] TLS Client Hello SNI: www.googleapis.com
14:49:36.013548 [Rule] Sub-rule matched: DOMAIN-SUFFIX googleapis.com
14:49:36.013699 [Rule] Policy decision path: AIProxy -> 🇺🇸 ...
14:49:36.184591 [Socket] Connected to address 67.230.160.137 in 168.0ms
Function
- Display whether request contains body data
- Display whether response contains body data
- Quickly determine HTTP transaction type
Display Format
- ✓ Has request data (streamHasRequestBody = true)
- ✓ Has response data (streamHasResponseBody = true)
Debugging Value
Although full HTTP headers/body cannot be viewed (limited by Surge API), Notes provide rich debugging information:
- TLS issues: Handshake failure reasons, protocol versions, cipher suites
- DNS issues: Resolution results, DNS servers, query times
- Routing issues: Rule matching path, policy decision chain
- Performance issues: Connection establishment time, socket connection details
- Connection issues: Disconnect reasons, error messages
Complementary to Surge Dashboard
For scenarios requiring complete HTTP headers/body viewing:
- TUI: Quick monitoring and control, view connection logs
- Surge Dashboard: Detailed HTTP debugging, view headers and body
Both complement each other, each serving its purpose.
Advanced Feature (Optional)
For complete HTTP headers/body data, refer to Scripting integration solution:
- Documentation:
docs/design/http-headers-capture-via-scripting.md - Requires manual Surge script configuration
- Estimated implementation time: 7-11 hours
- Has performance and storage overhead
Feature Capabilities
- Display all DNS cache records
- Left list: domain name, IP address, TTL
- Right details: complete information for selected record
- Support search filtering (by domain and IP)
Data Parsing
pub struct DnsRecord {
pub domain: String,
pub ip: Vec<String>, // "data" field
pub ttl: Option<f64>, // "expiresTime" Unix timestamp
pub server: Option<String>, // DNS server
pub logs: Vec<String>, // Query logs
pub path: Option<String>, // Resolution path
pub time_cost: Option<f64>, // Query duration
}TTL Display
expiresTimeis Unix timestamp (float)- Calculate
expiresTime - current timeto get remaining seconds - Display format:
123 s
Flush Cache
- Press
fkey to flush all DNS cache - Display confirmation notification
- Auto refresh list
API Call
client.get_dns_cache().await // Get cache
client.flush_dns().await // Flush cacheUse Cases
- Verify DNS configuration is in effect
- Debug DNS pollution issues
- Immediately flush cache after modifying DNS rules to verify
- Policy latency test concurrency control (avoid testing too many policies simultaneously)
- Test result incremental updates (only update changed policies)
- Policy latency history trend chart
- Custom latency test target URL
- Policy group batch testing (instead of global testing)
- Policy search/filtering function
- Configurable color themes
- Latency value threshold configuration
- Notification persistence storage
- More detailed error diagnostic information
Generated with Claude Code via Happy
Co-Authored-By: Claude noreply@anthropic.com