mihomo/mh
2026-07-01 12:18:49 +08:00

1408 lines
42 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
resolve_mihomo_root() {
if [[ -n "${MIHOMO_ROOT:-}" ]]; then
printf "%s\n" "$MIHOMO_ROOT"
elif [[ -x "${PWD}/mihomo" ]]; then
printf "%s\n" "$PWD"
elif [[ "$SCRIPT_DIR" != "/usr/local/bin" && -x "${SCRIPT_DIR}/mihomo" ]]; then
printf "%s\n" "$SCRIPT_DIR"
else
echo "mh: cannot locate mihomo root; cd into the project dir or set MIHOMO_ROOT=/path/to/mihomo" >&2
exit 1
fi
}
MIHOMO_ROOT="$(resolve_mihomo_root)"
MIHOMO_BIN=${MIHOMO_ROOT}/mihomo # 指向二进制文件
PID_FILE=${MIHOMO_ROOT}/run.pid # 固定pid文件
THEME_FILE=${MIHOMO_ROOT}/theme.current
API_HOST="127.0.0.1"
API_PORT="9090"
MIXED_PORT="7890" # 默认main
SOCK_PORT="7891"
API_BASE="http://${API_HOST}:${API_PORT}"
API_SECRET="" # 这是密钥用于验证对应yaml中设置的secret字段保持一致即可如果不设置默认空字符串表示不验证
# 简单彩色(不喜欢颜色就把这些变量都设为空字符串)
urlenc() {
python3 - <<'PY' "$1"
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=''))
PY
}
#===============测试小工具,下======================================
theme_current() {
local theme="basic"
if [[ -f "$THEME_FILE" ]]; then
theme="$(tr -d '[:space:]' < "$THEME_FILE")"
fi
case "$theme" in
basic|ganyu|kaomoji|minimal) printf '%s\n' "$theme" ;;
*) printf 'basic\n' ;;
esac
}
theme_prefix() {
local level="$1"
case "$(theme_current):$level" in
basic:DEBUG) printf '[debug] ' ;;
basic:INFO) printf '[info] ' ;;
basic:WARN) printf '[warn] ' ;;
basic:ERROR) printf '[error] ' ;;
basic:OK) printf '[ok] ' ;;
ganyu:DEBUG) printf '(甘雨观察中) ' ;;
ganyu:INFO) printf '❄️ 甘雨小秘书:' ;;
ganyu:WARN) printf '🍧 甘雨提醒:' ;;
ganyu:ERROR) printf '(甘雨皱眉) ' ;;
ganyu:OK) printf '🍡 甘雨点头:' ;;
kaomoji:DEBUG) printf '(눈_눈) ' ;;
kaomoji:INFO) printf '(。・ω・。) ' ;;
kaomoji:WARN) printf '(;´д`)ゞ ' ;;
kaomoji:ERROR) printf '(╯°□°)╯ ' ;;
kaomoji:OK) printf '(๑•̀ㅂ•́)و✧ ' ;;
minimal:*) printf '' ;;
*) printf '' ;;
esac
}
log() {
local level=$1
shift
echo "[$(date +%H:%M:%S)] [$level] $(theme_prefix "$level")$*"
}
debug() { [[ "${DEBUG:-0}" == "1" ]] && log "DEBUG" "$@"; }
info() { log "INFO" "$@"; }
warn() { log "WARN" "$@"; }
error() { log "ERROR" "$@"; }
die() { error "$@"; exit 1; }
ok() { log "OK" "$@"; }
need() {
command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"
}
test_env() { # 测试一下环境
need ss
need yq
need jq
need curl
need find
[[ -x "$MIHOMO_BIN" ]] || die "mihomo not executable: $MIHOMO_BIN"
echo "环境正确 ; env is ok"
}
test_port() {
local port
for port in "$@"; do
if ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(:|\\.)${port}\$"; then
die "port already in use: $port"
fi
done
}
api_get() {
local path="$1"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS --noproxy '*' -H "Authorization: Bearer ${API_SECRET}" "${API_BASE}${path}"
else
curl -fsS --noproxy '*' "${API_BASE}${path}"
fi
}
api_put() {
local path="$1"
local data="$2"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS --noproxy '*' -X PUT \
-H "Authorization: Bearer ${API_SECRET}" \
-H 'Content-Type: application/json' \
-d "$data" "${API_BASE}${path}"
else
curl -fsS --noproxy '*' -X PUT \
-H 'Content-Type: application/json' \
-d "$data" "${API_BASE}${path}"
fi
}
api_put_empty() {
local path="$1"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS --noproxy '*' -X PUT \
-H "Authorization: Bearer ${API_SECRET}" "${API_BASE}${path}"
else
curl -fsS --noproxy '*' -X PUT "${API_BASE}${path}"
fi
}
sync_global_group() {
local pjson
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
jq -e '.proxies["GLOBAL"]?.all? | index("Proxy") != null' >/dev/null <<<"$pjson" || return 0
local now
now="$(jq -r '.proxies["GLOBAL"].now // empty' <<<"$pjson")"
if [[ "$now" != "Proxy" ]]; then
api_put "/proxies/GLOBAL" '{"name":"Proxy"}' >/dev/null
info "Synced global selector [GLOBAL] -> [Proxy]"
fi
}
api_delay() {
local name="$1"
local timeout="${2:-2000}"
local url="http://www.gstatic.com/generate_204"
local enc_name enc_url
enc_name="$(urlenc "$name")"
enc_url="$(urlenc "$url")"
local out
out="$(api_get "/proxies/${enc_name}/delay?timeout=${timeout}&url=${enc_url}" 2>/dev/null || true)"
jq -r 'if (.delay|type)=="number" then (.delay|tostring) else "-" end' <<<"$out" 2>/dev/null || echo "-"
}
switch_group_node() {
# 用法switch_group_node "<group>" "<node>"
local group="$1"
local node="$2"
[[ -n "$group" && -n "$node" ]] || return 1
local enc_group
enc_group="$(urlenc "$group")"
api_put "/proxies/${enc_group}" "$(jq -nc --arg name "$node" '{name:$name}')" >/dev/null
info "Switched group [$group] -> [$node]"
}
has_mihomo_cap_net_admin() {
command -v getcap >/dev/null 2>&1 || return 1
getcap "$MIHOMO_BIN" 2>/dev/null | grep -q 'cap_net_admin'
}
disable_dns_fallback_geoip() {
local cfg="$1"
[[ -f "$cfg" ]] || return 1
yq -y -i '
.dns.enable = false |
.dns.fallback = [] |
.dns."fallback-filter" = null
' "$cfg"
}
#===============测试小工具,上======================================
#==============================配置文件读取,下======================================
get_main_group() {
local pjson
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
# 1) prefer group named "Proxy"
if jq -e '.proxies["Proxy"]? != null and .proxies["Proxy"].all? != null' >/dev/null <<<"$pjson"; then
echo "Proxy"
return 0
fi
# 2) fallback to your heuristic
jq -r '
.proxies
| to_entries
| map(select(.value.all? != null))
| (map(select(.value.type?=="Selector" or .value.type?=="URLTest" or .value.type?=="Fallback" or .value.type?=="LoadBalance")) + .)
| .[0].key // empty
' <<<"$pjson"
}
get_now_node() {
# 输出“当前节点名”;失败输出空并返回非 0
local group pjson
group="$(get_main_group)" || return 1
[[ -n "$group" ]] || return 1
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
jq -r --arg g "$group" '.proxies[$g].now // empty' <<<"$pjson"
}
#==============================配置文件读取,上======================================
#============================测速、测速结果处理,下=================================
speedtest_node_delay() {
# 用 mihomo 内置 delay API 测某个节点,输出 ms 或 "-"
# 用法speedtest_node_delay "<node_name>" [timeout_ms]
local node="${1:-}"
local timeout_ms="${2:-1000}"
[[ -n "$node" ]] || { echo "-"; return 1; }
# DIRECT/REJECT 没必要测
if [[ "$node" == "DIRECT" || "$node" == "REJECT" ]]; then
echo "-"
return 0
fi
api_delay "$node" "$timeout_ms"
}
speedtest_now() {
# 用法speedtest_now [timeout_ms]
local timeout_ms="${1:-1000}"
local node delay
node="$(get_now_node || true)"
if [[ -z "$node" ]]; then
echo "⚠️ 无法获取当前节点mihomo API 不通或没有可用 group"
return 1
fi
delay="$(speedtest_node_delay "$node" "$timeout_ms")"
# 简单打个标
local badge="🟡"
if [[ "$delay" =~ ^[0-9]+$ ]]; then
if (( delay <= 150 )); then badge="🟢"
elif (( delay <= 400 )); then badge="🟡"
else badge="🔴"
fi
else
badge="⚪"
fi
echo "👉 当前节点:${node}"
echo "${badge} 延迟:${delay} ms (timeout=${timeout_ms})"
}
wait_api() {
local t="${1:-5}" # seconds
local i=0
while (( i < t*10 )); do
if api_get "/proxies" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
i=$((i+1))
done
return 1
}
#============================测速、测速结果处理,上=================================
#============================mihomo配置下==================
write_cfg() { # name url file
local name="$1"
local url="$2"
cat > "${MIHOMO_ROOT}/$name/config.yaml" <<YAML # 直接使用这个即可
mixed-port: ${MIXED_PORT}
socks-port: ${SOCK_PORT}
allow-lan: false
bind-address: ${API_HOST}
mode: rule
log-level: error
external-controller: ${API_HOST}:${API_PORT}
secret: ""
proxy-providers:
${name}:
type: http
url: "${url}"
interval: 86400
path: "${MIHOMO_ROOT}/$name/providers/$name.yaml"
proxy-groups:
- name: Proxy
type: select
use:
- ${name}
- name: Final
type: select
proxies:
- Proxy
- DIRECT
rules:
- DOMAIN-SUFFIX,localhost,DIRECT
- DOMAIN-SUFFIX,local,DIRECT
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
- IP-CIDR,224.0.0.0/4,DIRECT,no-resolve
- IP-CIDR6,::1/128,DIRECT,no-resolve
- IP-CIDR6,fc00::/7,DIRECT,no-resolve
- IP-CIDR6,fe80::/10,DIRECT,no-resolve
- MATCH,Final
YAML
}
ensure_profile_config() {
local profile_dir="$1"
local cfg="${profile_dir}/config.yaml"
[[ -d "$profile_dir" ]] || die "profile not found: $profile_dir"
[[ -f "$cfg" ]] || die "missing config: $cfg"
mkdir -p "${profile_dir}/providers"
local -a providers
mapfile -t providers < <(yq -r '."proxy-providers" | keys[]?' "$cfg" 2>/dev/null)
(( ${#providers[@]} > 0 )) || die "missing proxy-providers in $cfg"
local provider ptype purl ppath
for provider in "${providers[@]}"; do
ptype="$(yq -r --arg p "$provider" '."proxy-providers"[$p].type // ""' "$cfg")"
case "$ptype" in
http)
purl="$(yq -r --arg p "$provider" '."proxy-providers"[$p].url // ""' "$cfg")"
[[ -n "$purl" ]] || die "provider [$provider] is http but url is empty"
yq -y -i --arg p "$provider" --arg path "${profile_dir}/providers/${provider}.yaml" '."proxy-providers"[$p].path = $path | ."proxy-providers"[$p].interval = 86400' "$cfg"
;;
file)
ppath="$(yq -r --arg p "$provider" '."proxy-providers"[$p].path // ""' "$cfg")"
[[ -n "$ppath" ]] || ppath="${profile_dir}/providers/${provider}.yaml"
yq -y -i --arg p "$provider" --arg path "$ppath" '."proxy-providers"[$p].path = $path' "$cfg"
[[ -f "$ppath" ]] || die "provider [$provider] file not found: $ppath"
;;
*)
die "provider [$provider] has unsupported or empty type: ${ptype:-<empty>}"
;;
esac
done
local mode
mode="$(yq -r '.mode // ""' "$cfg" 2>/dev/null)"
case "$mode" in
rule|global|direct) ;;
"") mode="rule" ;;
*) warn "invalid mode [$mode], reset to rule"; mode="rule" ;;
esac
yq -y -i "
.mode = \"${mode}\" |
.[\"mixed-port\"] = ${MIXED_PORT} |
.[\"socks-port\"] = ${SOCK_PORT} |
.[\"external-controller\"] = \"${API_HOST}:${API_PORT}\" |
.secret = (.secret // \"\") |
.[\"log-level\"] = \"error\" |
.[\"allow-lan\"] = (.[\"allow-lan\"] // false) |
.[\"bind-address\"] = \"${API_HOST}\"
" "$cfg"
if ! yq -e '([."proxy-groups"[]?.name] | index("Proxy") != null and index("Final") != null) and (.rules | type == "array" and length > 0)' "$cfg" >/dev/null 2>&1; then
warn "proxy-groups or rules missing, applying basic rule config"
reset_basic_rules "$cfg" || die "failed to initialize basic rules"
fi
yq -e '.mode as $mode | ($mode == "rule" or $mode == "global" or $mode == "direct") and (."mixed-port" | type == "number") and (."socks-port" | type == "number") and (."proxy-providers" | type == "object" and length > 0) and (."proxy-groups" | type == "array" and length > 0) and (.rules | type == "array" and length > 0)' "$cfg" >/dev/null || die "invalid mihomo config after normalization: $cfg"
}
#=====================mihomo配置上====================================
print_mihomo_status() {
local cfg pjson
cfg="$(api_get "/configs" 2>/dev/null)" || { echo "❌ API 不通: ${API_BASE}"; return 1; }
pjson="$(api_get "/proxies" 2>/dev/null)" || { echo "❌ 读取 /proxies 失败"; return 1; }
local allow_lan bind_addr mode mixed_port ec
allow_lan="$(jq -r '."allow-lan" // false' <<<"$cfg")"
bind_addr="$(jq -r '."bind-address" // "-"' <<<"$cfg")"
mode="$(jq -r '.mode // "-"' <<<"$cfg")"
mixed_port="$(jq -r '."mixed-port" // .port // "-"' <<<"$cfg")"
ec="$(jq -r '."external-controller" // "-"' <<<"$cfg")"
local group now node_count
group="$(get_main_group 2>/dev/null || true)"
[[ -n "$group" ]] || { echo "❌ 找不到主组(/proxies 里没有可用 group"; return 1; }
now="$(jq -r --arg g "$group" '.proxies[$g].now // "-"' <<<"$pjson")"
node_count="$(jq -r --arg g "$group" '.proxies[$g].all | length' <<<"$pjson" 2>/dev/null || echo "-")"
local lan_icon="🔒"
[[ "$allow_lan" == "true" ]] && lan_icon="📡"
echo "🧠 Mihomo Status"
echo "🌐 API: ${API_BASE} (EC=${ec})"
echo "🚪 Port: mixed-port=${mixed_port}"
echo "${lan_icon} LAN: allow-lan=${allow_lan} bind=${bind_addr}"
echo "🧭 Mode: ${mode}"
echo "🧩 Group: ${group}"
echo "📦 Nodes: ${node_count}"
echo "✅ Now: ${now}"
}
port_open_local() {
local port="$1"
ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(:|\\.)${port}\$"
}
show_runtime_routing_status() {
local cfg pjson
cfg="$(api_get "/configs" 2>/dev/null)" || {
echo "Routing: API unavailable"
return 0
}
pjson="$(api_get "/proxies" 2>/dev/null)" || {
echo "Routing: /proxies unavailable"
return 0
}
local mode global_now final_now proxy_now
mode="$(jq -r '.mode // "-"' <<<"$cfg")"
global_now="$(jq -r '.proxies["GLOBAL"].now // empty' <<<"$pjson")"
final_now="$(jq -r '.proxies["Final"].now // empty' <<<"$pjson")"
proxy_now="$(jq -r '.proxies["Proxy"].now // empty' <<<"$pjson")"
echo "Mode: ${mode}"
[[ -n "$global_now" ]] && echo "GLOBAL -> ${global_now}"
[[ -n "$final_now" ]] && echo "Final -> ${final_now}"
[[ -n "$proxy_now" ]] && echo "Proxy -> ${proxy_now}"
}
extract_proxy_port() {
local value="${1:-}"
[[ -n "$value" ]] || return 1
sed -E 's#^[[:alpha:]][[:alnum:]+.-]*://[^:/]+:([0-9]+)/*$#\1#' <<<"$value"
}
proxy_env_status_line() {
local name="$1"
local value="$2"
if [[ -z "$value" ]]; then
echo "${name}: OFF"
return 0
fi
local port
port="$(extract_proxy_port "$value" 2>/dev/null || true)"
if [[ -n "$port" ]] && port_open_local "$port"; then
echo "${name}: ON (${value}, port ${port} reachable)"
elif [[ -n "$port" ]]; then
echo "${name}: STALE (${value}, port ${port} not listening)"
else
echo "${name}: ON (${value})"
fi
}
start_end(){ # start/end name/none 文件
local start0Rend="${1:-}"
local name_file=${2:-}
local cfg=$MIHOMO_ROOT/$name_file
[[ -n "$start0Rend" ]] || die "usage: $0 start|end"
if [[ "$start0Rend" == "start" ]]; then
start_end "end" # 先停止当前
[[ -d "$cfg" ]] || die "profile not found: $cfg"
[[ -f "$cfg/config.yaml" ]] || die "missing config: $cfg/config.yaml"
ensure_profile_config "$cfg"
if [[ "$(yq -r '.tun.enable // false' "$cfg/config.yaml" 2>/dev/null || echo false)" != "true" ]]; then
disable_dns_fallback_geoip "$cfg/config.yaml"
fi
# 进入的前提配置没有问题,可以直接运行
info "start mihomo with config=$cfg/config.yaml data=$cfg"
# 前台跑不方便,这里后台启动并记录 pid
test_port "$API_PORT" "$MIXED_PORT" "$SOCK_PORT"
"$MIHOMO_BIN" -d "$cfg" -f "$cfg/config.yaml" >"$cfg/mihomo.log" 2>&1 & # 每次启动情况记录
echo $! >"$PID_FILE" # 保存到定点位置
elif [[ "$start0Rend" == "end" ]]; then
if [[ -f "$PID_FILE" ]]; then # 存在就终止
local pid
pid="$(cat "$PID_FILE" || true)"
if [[ -n "${pid}" ]] && kill "$pid" 2>/dev/null; then
info "stop process pid=$pid"
# 等一下让它退出
for _ in {1..20}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$pid" 2>/dev/null; then
warn "force kill pid=$pid"
kill -9 "$pid" 2>/dev/null || true
fi
fi
rm -f "$PID_FILE"
fi
else
die "usage: $0 start|end"
fi
}
cfg_add() { # add name src
if [[ "$1" == "add" ]]; then
shift # 去掉 "add" 参数
fi
local name="${1:-}"
local src="${2:-}"
local cfg_root="${MIHOMO_ROOT}/${name}"
[[ -n "$name" ]] || die "usage: $0 add <name> <url|file>"
[[ -n "$src" ]] || die "usage: $0 add <name> <url|file>"
if [[ -d "$cfg_root" ]]; then
die "profile exists: $cfg_root ,请换一个名称"
fi
mkdir -p "$cfg_root/providers"
write_cfg "$name" "$src"
if [[ -f "$src" ]]; then #file: 直接复制,不用下载了
info "copy file -> ${cfg_root}/providers/$name.yaml"
cp -f "$src" "$cfg_root/providers/$name.yaml"
yq -y -i "
.[\"proxy-providers\"][\"$name\"].type = \"file\" |
del(.[\"proxy-providers\"][\"$name\"].url) |
del(.[\"proxy-providers\"][\"$name\"].interval)
" "$cfg_root/config.yaml"
info "saved: ${cfg_root}/providers/$name.yaml" # 有了文件,等会挂载
fi
ensure_bypass_list "$cfg_root"
reset_basic_rules "$cfg_root/config.yaml"
# 启动这个配置
select_one "select" "$name" # 启动这个新的节点
print_mihomo_status 1000
}
get_group_nodes_json() {
local pjson group nodes
pjson="$(api_get "/proxies")" || return 1
group="$(get_main_group)" || return 1
[[ -n "$group" ]] || return 1
nodes="$(jq -c --arg g "$group" '.proxies[$g].all' <<<"$pjson")" || return 1
jq -nc --arg group "$group" --argjson nodes "$nodes" '{group:$group, nodes:$nodes}'
}
provider_http_list() {
local cfg="$1"
yq -r '."proxy-providers" | to_entries[]? | select(.value.type == "http") | .key' "$cfg"
}
provider_in_list() {
local needle="$1"
shift
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
update_one_provider() {
local provider="$1"
local enc_provider
enc_provider="$(urlenc "$provider")"
api_put_empty "/providers/proxies/${enc_provider}" >/dev/null
info "updated provider: $provider"
}
update_providers_cmd() {
local target="${1:-}"
local prof cfg_dir cfg
prof="$(current_profile)" || die "no current profile"
cfg_dir="${MIHOMO_ROOT}/${prof}"
cfg="${cfg_dir}/config.yaml"
ensure_profile_config "$cfg_dir"
local -a providers selected
mapfile -t providers < <(provider_http_list "$cfg")
(( ${#providers[@]} > 0 )) || die "no http proxy-providers in profile=${prof}"
if ! wait_api 3; then
info "mihomo API not ready, starting current profile=${prof} first..."
start_end start "$prof"
wait_api 5 || die "mihomo API not ready after start"
fi
case "$target" in
"" )
echo "📦 HTTP providers (profile=${prof}):"
echo " 0) ALL"
local i pick
for i in "${!providers[@]}"; do
printf " %d) %s\n" "$((i+1))" "${providers[$i]}"
done
while true; do
read -r -p "👉 请输入序号 (0-${#providers[@]}): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick>=0 && pick<=${#providers[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
if (( pick == 0 )); then
selected=("${providers[@]}")
else
selected=("${providers[$((pick-1))]}")
fi
;;
all|ALL)
selected=("${providers[@]}")
;;
*)
provider_in_list "$target" "${providers[@]}" || die "provider not found or not http: $target"
selected=("$target")
;;
esac
local provider
for provider in "${selected[@]}"; do
update_one_provider "$provider"
done
}
theme_cmd() {
local action="${1:-show}"
case "$action" in
show|current)
echo "current theme: $(theme_current)"
echo "available: basic ganyu kaomoji minimal"
;;
list)
cat <<THEMES
basic - 稳定基础版ASCII 前缀,最不挑终端
ganyu - 原神甘雨风格,少量 emoji + 轻提示
kaomoji - 颜文字版,适合 emoji 显示不完整的终端
minimal - 极简版,不加额外表情
THEMES
;;
basic|ganyu|kaomoji|minimal)
printf '%s\n' "$action" > "$THEME_FILE"
info "theme set to ${action}"
;;
*)
die "usage: $0 theme [show|list|basic|ganyu|kaomoji|minimal]"
;;
esac
}
help() {
cat <<EOF
Usage:
mh [command] [args...]
Commands:
add <name> <url|file> 新建 profile
select [name] 切换/启动 profile不带 name 时交互选择
start <name> 等价于 select <name>
end 停止当前 mihomo
env 输出代理 export常用eval "\$(mh env)"
theme [name] 查看或切换输出主题
sysproxy [on|off|status] 开关 GNOME 系统代理
update [all|provider] 更新当前 profile 的 HTTP provider
mode rule|global|direct 切换运行模式并重启
rules show|reset 查看或重置基础规则
tun [on|off|toggle] 开关 TUN
list | -l | --list [kw] 列出节点并可输入序号切换
doctor 输出诊断信息
db 调试菜单
help | -h | --help 显示帮助
Default:
mh 显示 TUN/路由/代理环境/当前节点延迟
mh <keyword> 搜索节点并按延迟排序选择切换
Examples:
mh add demo "https://example.com/sub.yaml"
mh add local "/path/to/sub.yaml"
mh select
mh select demo
mh 美国
eval "\$(mh env)"
Notes:
root=${MIHOMO_ROOT}
api=${API_BASE}
mixed-port=${MIXED_PORT}
mihomo=${MIHOMO_BIN}
EOF
}
select_one(){ # select name
shift
local name
if [[ -z "${1:-}" ]];then
local -a files
mapfile -t files < <(find "$MIHOMO_ROOT" -mindepth 1 -maxdepth 1 -type d -exec test -f '{}/config.yaml' ';' -print | sort)
if (( ${#files[@]} == 0 )); then
echo "⚠️ ${MIHOMO_ROOT} 下没有找到任何 目录"
return 1
fi
echo "📄 可用配置(输入序号选择,例如 1"
local i
for i in "${!files[@]}"; do
printf " %d) %s\n" "$((i+1))" "$(basename "${files[$i]}")"
done
local pick
while true; do
read -r -p "👉 请输入序号 (1-${#files[@]}): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick>=1 && pick<=${#files[@]} )) || { echo "❌ 超出范围"; continue; }
name="$(basename "${files[$((pick-1))]}")"
break
done
else
local file_cfg="$MIHOMO_ROOT/$1"
if [[ -d $file_cfg ]];then
name="$1"
else
echo "❌ 不存在: $file_cfg"# 输入name有无
return 1
fi
fi
echo "$name" > "${MIHOMO_ROOT}/current.profile"
start_end "start" "$name"
wait_api 5 || warn "mihomo API not ready yet"
sync_global_group || warn "failed to sync GLOBAL -> Proxy"
speedtest_now 1000
}
#====================tun控制下=========================
current_profile() {
[[ -f "${MIHOMO_ROOT}/current.profile" ]] || return 1
cat "${MIHOMO_ROOT}/current.profile"
}
ensure_bypass_list() {
local profile_dir="$1"
local file="${profile_dir}/bypass.list"
[[ -f "$file" ]] && return 0
cat > "$file" <<BYPASS
*zhihu.com
*zhimg.com
*jd.com
100ime-iat-api.xfyun.cn
*360buyimg.com
localhost
*.local
127.*
10.*
172.*
192.168.*
BYPASS
}
compile_bypass_rules() {
local file="$1"
local line value prefix
[[ -f "$file" ]] || return 0
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
line="$(printf "%s" "$line" | sed -E "s/^[[:space:]]+|[[:space:]]+$//g")"
[[ -z "$line" ]] && continue
case "$line" in
*/*)
printf "%s\n" "IP-CIDR,${line},DIRECT,no-resolve"
;;
[0-9]*.*\*)
prefix="${line%%.*}"
case "$prefix" in
10) printf "%s\n" "IP-CIDR,10.0.0.0/8,DIRECT,no-resolve" ;;
127) printf "%s\n" "IP-CIDR,127.0.0.0/8,DIRECT,no-resolve" ;;
172) printf "%s\n" "IP-CIDR,172.16.0.0/12,DIRECT,no-resolve" ;;
192) printf "%s\n" "IP-CIDR,192.168.0.0/16,DIRECT,no-resolve" ;;
*) warn "unsupported bypass IP wildcard: $line" ;;
esac
;;
[0-9]*.[0-9]*.[0-9]*.[0-9]*)
printf "%s\n" "IP-CIDR,${line}/32,DIRECT,no-resolve"
;;
\**)
value="${line#\*}"
value="${value#.}"
[[ -n "$value" ]] && printf "%s\n" "DOMAIN-SUFFIX,${value},DIRECT"
;;
.*)
value="${line#.}"
[[ -n "$value" ]] && printf "%s\n" "DOMAIN-SUFFIX,${value},DIRECT"
;;
*)
printf "%s\n" "DOMAIN,${line},DIRECT"
;;
esac
done < "$file" | awk "!seen[\$0]++"
}
rules_json_from_lines() {
jq -R -s -c "split(\"\n\") | map(select(. != \"\"))"
}
reset_basic_rules() {
local cfg="$1"
[[ -f "$cfg" ]] || return 1
local providers_json profile_dir bypass_rules rules_json
providers_json="$(yq -c '."proxy-providers" | keys' "$cfg" 2>/dev/null)"
[[ "$providers_json" != "null" && "$providers_json" != "[]" ]] || { warn "cannot detect proxy-provider names from $cfg"; return 1; }
profile_dir="$(dirname "$cfg")"
ensure_bypass_list "$profile_dir"
bypass_rules="$(compile_bypass_rules "${profile_dir}/bypass.list")"
rules_json="$({
printf "%s\n" "$bypass_rules"
printf "%s\n" \
"IP-CIDR,100.64.0.0/10,DIRECT,no-resolve" \
"IP-CIDR,224.0.0.0/4,DIRECT,no-resolve" \
"IP-CIDR6,::1/128,DIRECT,no-resolve" \
"IP-CIDR6,fc00::/7,DIRECT,no-resolve" \
"IP-CIDR6,fe80::/10,DIRECT,no-resolve" \
"MATCH,Final"
} | rules_json_from_lines)"
yq -y -i --argjson providers "$providers_json" --argjson rules "$rules_json" ".mode = \"rule\" | .[\"proxy-groups\"] = [{\"name\":\"Proxy\",\"type\":\"select\",\"use\":\$providers},{\"name\":\"Final\",\"type\":\"select\",\"proxies\":[\"Proxy\",\"DIRECT\"]}] | .rules = \$rules" "$cfg"
}
mode_set() {
local next_mode="${1:-}"
case "$next_mode" in
rule|global|direct) ;;
*) die "usage: $0 mode rule|global|direct" ;;
esac
local prof cfg
prof="$(current_profile)" || die "no current profile"
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || die "missing config: $cfg"
if [[ "$next_mode" == "rule" ]] && ! yq -e '.rules | length > 0' "$cfg" >/dev/null 2>&1; then
info "No rules found in profile=${prof}, applying basic rules first..."
reset_basic_rules "$cfg" || die "failed to initialize basic rules"
fi
yq -y -i ".mode = \"${next_mode}\"" "$cfg"
info "Mode set to ${next_mode} (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
if [[ "$next_mode" == "global" ]]; then
sync_global_group || warn "failed to sync GLOBAL -> Proxy"
fi
print_mihomo_status || true
}
rules_cmd() {
local action="${1:-show}"
local cfg prof
prof="$(current_profile)" || die "no current profile"
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || die "missing config: $cfg"
case "$action" in
show)
echo "Rules (profile=${prof}):"
yq -r '.rules[]?' "$cfg"
;;
reset)
reset_basic_rules "$cfg" || die "failed to reset basic rules"
info "Basic rules reset (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
print_mihomo_status || true
;;
*)
die "usage: $0 rules show|reset"
;;
esac
}
tun() {
# 用法sudo mh tun on|off|toggle
local action="${1:-toggle}"
local prof
prof="$(current_profile)" || { warn "no current profile"; return 1; }
local cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || { warn "missing config: $cfg"; return 1; }
local cur
cur="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)"
local next
case "$action" in
on) next=true ;;
off) next=false ;;
toggle) [[ "$cur" == "true" ]] && next=false || next=true ;;
*) warn "usage: mh tun on|off|toggle"; return 1 ;;
esac
if [[ "$next" == "true" && "${EUID:-$(id -u)}" -ne 0 ]] && ! has_mihomo_cap_net_admin; then
warn "开启 TUN 通常需要 root或提前给 mihomo 设置 cap_net_admin/cap_net_raw"
warn "继续尝试启动若失败请执行sudo setcap cap_net_admin,cap_net_raw+ep \"$MIHOMO_BIN\""
fi
yq -y -i "
.tun.enable = ${next} |
.tun.stack = (.tun.stack // \"system\") |
.tun.device = (.tun.device // \"mihomo\") |
.tun[\"auto-route\"] = (.tun[\"auto-route\"] // true) |
.tun[\"auto-redir\"] = (.tun[\"auto-redir\"] // true) |
.tun[\"auto-detect-interface\"] = (.tun[\"auto-detect-interface\"] // true) |
.tun[\"dns-hijack\"] = (.tun[\"dns-hijack\"] // []) |
( .tun[\"dns-hijack\"] |= ( . + [\"any:53\"] | unique ) ) |
.dns.enable = ${next} |
.dns.listen = (.dns.listen // \"127.0.0.1:1053\") |
.dns[\"enhanced-mode\"] = \"redir-host\" |
.dns.nameserver = (.dns.nameserver // [\"223.5.5.5\",\"119.29.29.29\"]) |
.dns.fallback = (.dns.fallback // [\"1.1.1.1\",\"8.8.8.8\"]) |
.dns[\"fallback-filter\"].geoip = false
" "$cfg"
if [[ "$next" != "true" ]]; then
disable_dns_fallback_geoip "$cfg"
fi
info "TUN set to ${next} (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
sync_global_group || warn "failed to sync GLOBAL -> Proxy"
print_mihomo_status 1200 || true
}
#=============================tun控制上===================
list_nodes() {
local kw="${1:-}"
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local pjson group now
pjson="$(api_get "/proxies")" || { warn "cannot read /proxies"; return 1; }
group="$(get_main_group)" || { warn "cannot detect main group"; return 1; }
now="$(jq -r --arg g "$group" '.proxies[$g].now // ""' <<<"$pjson")"
local -a nodes
if [[ -n "$kw" ]]; then
mapfile -t nodes < <(jq -r --arg g "$group" --arg kw "$kw" '
def lc: ascii_downcase;
.proxies[$g].all
| map(select((contains($kw)) or ((lc) | contains($kw | lc))))
| .[]
' <<<"$pjson")
else
mapfile -t nodes < <(jq -r --arg g "$group" '.proxies[$g].all[]' <<<"$pjson")
fi
(( ${#nodes[@]} > 0 )) || { warn "No nodes matched: ${kw:-<all>}"; return 1; }
echo "📌 Group: ${group}"
[[ -n "$now" ]] && echo "⭐ Now: ${now}"
[[ -n "$kw" ]] && echo "🔎 Filter: ${kw}"
echo
local i
for i in "${!nodes[@]}"; do
printf " %d) %s%s\n" "$((i+1))" "$(if [[ "${nodes[$i]}" == "$now" ]]; then printf "⭐ "; else printf " "; fi)" "${nodes[$i]}"
done
[[ -t 0 ]] || return 0
local pick
while true; do
read -r -p "👉 选择节点 (1-${#nodes[@]}, 0 cancel): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick == 0 )) && { echo "cancelled"; return 0; }
(( pick>=1 && pick<=${#nodes[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
switch_group_node "$group" "${nodes[$((pick-1))]}"
speedtest_now 1500 || true
}
#===============================搜索测速排序,下======================
# 批量测延迟(默认并发 6输出"<delay>\t<node>"
batch_delay() {
# 用法batch_delay <timeout_ms> <max_jobs> <node1> <node2> ...
local timeout_ms="${1:-1200}"; shift || true
local max_jobs="${1:-6}"; shift || true
local -a nodes=( "$@" )
# 兼容:没有节点直接返回
(( ${#nodes[@]} == 0 )) && return 0
# 并发测
local tmp
tmp="$(mktemp)"
rm -f "$tmp"; : >"$tmp"
# 简单并发控制:后台跑,超过 max_jobs 就 wait 一个
local running=0
for n in "${nodes[@]}"; do
(
local d
d="$(api_delay "$n" "$timeout_ms")"
# 统一成数值:"-" 变成 999999便于排序
if [[ "$d" =~ ^[0-9]+$ ]]; then
printf "%s\t%s\n" "$d" "$n"
else
printf "999999\t%s\n" "$n"
fi
) >>"$tmp" &
running=$((running+1))
if (( running >= max_jobs )); then
wait -n 2>/dev/null || wait
running=$((running-1))
fi
done
wait
# 排序输出
sort -n "$tmp"
rm -f "$tmp"
}
# 关键词匹配 + 测速排序 + 选择切换
pick_switch_with_delay() {
# 用法pick_switch_with_delay "<keyword>" [timeout_ms]
local kw="${1:-}"
local timeout_ms="${2:-1200}"
[[ -n "$kw" ]] || { warn "usage: mh <keyword>"; return 1; }
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local gj group
gj="$(get_group_nodes_json)" || { warn "cannot read proxies/groups"; return 1; }
group="$(jq -r '.group' <<<"$gj")"
# 匹配候选:中文用 contains英文用 lower-case contains
local candidates
candidates="$(jq -r --arg kw "$kw" '
def lc: ascii_downcase;
.nodes
| map(select(. != "DIRECT" and . != "REJECT"))
| map(select(
(contains($kw)) or ((lc) | contains($kw | lc))
))
| .[]
' <<<"$gj")"
if [[ -z "$candidates" ]]; then
warn "No nodes matched keyword: $kw"
info "Tip: try: 美国 / 香港 / 日本 / 狮城 / 韩国 / 台湾 / US / HK / JP / SG"
return 1
fi
mapfile -t arr <<<"$candidates"
info "Testing delay for ${#arr[@]} nodes (timeout=${timeout_ms}ms)..."
# 并发 6你可以改大/改小
local lines
lines="$(batch_delay "$timeout_ms" 6 "${arr[@]}")"
# 组装排序后的节点数组(按 delay asc
local -a sorted_nodes=()
local -a sorted_delay=()
while IFS=$'\t' read -r d n; do
sorted_nodes+=( "$n" )
sorted_delay+=( "$d" )
done <<<"$lines"
echo "🔎 Matched nodes in group [$group] for keyword: [$kw] (sorted by delay)"
local i showd
for i in "${!sorted_nodes[@]}"; do
showd="${sorted_delay[$i]}"
[[ "$showd" == "999999" ]] && showd="-" # 显示时还原
printf " %d) %s\t%s ms\n" "$((i+1))" "${sorted_nodes[$i]}" "$showd"
done
local pick
while true; do
read -r -p "👉 choose (1-${#sorted_nodes[@]}, 0 cancel): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick==0 )) && { echo "cancelled"; return 0; }
(( pick>=1 && pick<=${#sorted_nodes[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
switch_group_node "$group" "${sorted_nodes[$((pick-1))]}"
speedtest_now 1500 || true
}
#==============================搜索测速排序,上======================
#============================状态检查,下=====================
tun_status() {
local prof cfg enabled dev
prof="$(current_profile 2>/dev/null || true)"
[[ -n "$prof" ]] || { echo "TUN: unknown (no current profile)"; return 0; }
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || { echo "TUN: unknown (missing config)"; return 0; }
enabled="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)"
dev="$(yq -r '.tun.device // "mihomo"' "$cfg" 2>/dev/null || echo mihomo)"
if [[ "$enabled" != "true" ]]; then
echo "TUN: OFF (profile=${prof})"
return 0
fi
echo "TUN: ON (profile=${prof})"
echo "Expect iface: ${dev}"
if ip link show "$dev" >/dev/null 2>&1; then
echo "Iface: ✅ ${dev} exists"
else
echo "Iface: ❌ ${dev} not found (mihomo tun may not be running/privileged)"
fi
echo "Default route:"
ip route show default 2>/dev/null | sed 's/^/ /' || true
}
proxy_env_status() {
proxy_env_status_line "HTTP_PROXY" "${HTTP_PROXY:-}"
proxy_env_status_line "HTTPS_PROXY" "${HTTPS_PROXY:-}"
proxy_env_status_line "ALL_PROXY" "${ALL_PROXY:-}"
}
show_proxy_exports() {
cat <<EOF
export http_proxy="http://127.0.0.1:${MIXED_PORT}"
export https_proxy="http://127.0.0.1:${MIXED_PORT}"
export all_proxy="socks5h://127.0.0.1:${MIXED_PORT}"
export HTTP_PROXY="\$http_proxy"
export HTTPS_PROXY="\$https_proxy"
export ALL_PROXY="\$all_proxy"
export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export no_proxy="\$NO_PROXY"
EOF
}
net_test() {
# 常用网站连通性测试:输出 “URL -> OK/FAIL”
local -a urls=(
"https://www.google.com"
"https://www.cloudflare.com"
"https://www.github.com"
"https://www.youtube.com"
"https://www.baidu.com"
"https://www.bing.com"
)
echo "== Net Test (timeout=10s) =="
local url
for url in "${urls[@]}"; do
if curl -fsS -I -m 10 "$url" >/dev/null 2>&1; then
printf "%s -> ✅ OK\n" "$url"
else
printf "%s -> ❌ FAIL\n" "$url"
fi
done
}
net_test_double() {
local -a urls=(
"https://www.google.com"
"https://www.cloudflare.com"
"https://www.github.com"
"https://www.youtube.com"
"https://www.baidu.com"
"https://www.bing.com"
)
echo "== Net Test (timeout=10s) =="
echo "Format: URL -> DIRECT | PROXY(mixed-port=${MIXED_PORT})"
local url ok1 ok2
for url in "${urls[@]}"; do
# 直连(完全忽略环境代理)
if curl -fsS -I -m 10 --noproxy '*' "$url" >/dev/null 2>&1; then
ok1="✅"
else
ok1="❌"
fi
# 强制走 mihomo mixed-port明确指定代理
if curl -fsS -I -m 10 -x "http://127.0.0.1:${MIXED_PORT}" "$url" >/dev/null 2>&1; then
ok2="✅"
else
ok2="❌"
fi
printf "%-28s -> DIRECT:%s PROXY:%s\n" "$url" "$ok1" "$ok2"
done
}
#============================状态检查,上=====================
#=========================debug,下===============================
debug_one() {
shift
local -a debug_code=()
debug_code[1]="ss -lntp | grep -E ':(9090|9091|7890|7891)'|| echo \"❌ 未找到代理服务\""
debug_code[2]="ps -fp \${1:-} -o pid,ppid,cmd"
debug_code[3]="net_test"
debug_code[4]="net_test_double"
debug_code[5]="test_env"
debug_code[6]="ip -o link show"
debug_code[7]="resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf"
local -a debug_desc=()
debug_desc[1]="检查 9090/9091/7890/7891 端口监听:"
debug_desc[2]="查看指定 PID 的进程信息:"
debug_desc[3]="测试一下网络(仅proxy)"
debug_desc[4]="测试一下网络(直连+proxy)"
debug_desc[5]="测试需要的软件是否安装:"
debug_desc[6]="看看是否有虚拟网卡 mihomo:"
debug_desc[7]="dns debug"
echo "== debug list =="
local i
for i in "${!debug_code[@]}"; do
printf " [%s] %s\n %s\n" "$i" "${debug_desc[$i]:-}" "${debug_code[$i]}"
done
local idx
read -r -p "select index: " idx
for i in "${!debug_code[@]}"; do
if [[ "$i" == "$idx" ]]; then
echo "== run [$i] =="
eval "${debug_code[$i]}"
return $?
fi
done
echo "invalid index: $idx"
return 1
}
sysproxy_cmd() {
local action="${1:-status}"
command -v gsettings >/dev/null 2>&1 || die "gsettings not found; this sysproxy command is for GNOME/Ubuntu desktops"
case "$action" in
on)
gsettings set org.gnome.system.proxy mode 'manual'
gsettings set org.gnome.system.proxy.http host '127.0.0.1'
gsettings set org.gnome.system.proxy.http port "${MIXED_PORT}"
gsettings set org.gnome.system.proxy.https host '127.0.0.1'
gsettings set org.gnome.system.proxy.https port "${MIXED_PORT}"
gsettings set org.gnome.system.proxy.socks host '127.0.0.1'
gsettings set org.gnome.system.proxy.socks port "${SOCK_PORT}"
gsettings set org.gnome.system.proxy ignore-hosts "['localhost','127.0.0.0/8','::1','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16']"
info "system proxy enabled: http=127.0.0.1:${MIXED_PORT}, socks=127.0.0.1:${SOCK_PORT}"
;;
off)
gsettings set org.gnome.system.proxy mode 'none'
info "system proxy disabled"
;;
status)
echo "mode=$(gsettings get org.gnome.system.proxy mode)"
echo "http=$(gsettings get org.gnome.system.proxy.http host):$(gsettings get org.gnome.system.proxy.http port)"
echo "https=$(gsettings get org.gnome.system.proxy.https host):$(gsettings get org.gnome.system.proxy.https port)"
echo "socks=$(gsettings get org.gnome.system.proxy.socks host):$(gsettings get org.gnome.system.proxy.socks port)"
;;
*)
die "usage: $0 sysproxy [on|off|status]"
;;
esac
}
doctor() {
local prof cfg
prof="$(current_profile 2>/dev/null || true)"
cfg=""
[[ -n "$prof" ]] && cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
echo "== mh doctor =="
echo "root: ${MIHOMO_ROOT}"
echo "bin: ${MIHOMO_BIN}"
echo "pid: ${PID_FILE}"
[[ -n "$prof" ]] && echo "profile: ${prof}" || echo "profile: <none>"
[[ -n "$cfg" ]] && echo "config: ${cfg}"
echo
echo "== capability =="
if command -v getcap >/dev/null 2>&1; then
getcap "$MIHOMO_BIN" 2>/dev/null || true
else
echo "getcap not found"
fi
echo
echo "== ports =="
ss -lnt 2>/dev/null | grep -E ":(9090|7890|7891|1053)\\b" || echo "no mh ports listening"
echo
echo "== runtime =="
tun_status
show_runtime_routing_status
proxy_env_status
echo
echo "== api =="
api_get "/configs" 2>/dev/null | jq -r '{mode, "mixed-port": ."mixed-port", "socks-port": ."socks-port", "external-controller": ."external-controller"}' 2>/dev/null || echo "api unavailable"
echo
echo "== route =="
ip route show default 2>/dev/null || true
echo
echo "== dns =="
resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf
echo
echo "== log tail =="
if [[ -n "$prof" && -f "${MIHOMO_ROOT}/${prof}/mihomo.log" ]]; then
tail -n 40 "${MIHOMO_ROOT}/${prof}/mihomo.log"
else
echo "no log file"
fi
}
#=========================debug,上===============================
main() {
if [[ -z "${1:-}" ]]; then
tun_status
show_runtime_routing_status
proxy_env_status
speedtest_now 1000 || true
# print_mihomo_status
exit 0
fi
case "${1:-}" in
add)
cfg_add "$@";;
start)
[[ -n "${2:-}" ]] || die "usage: $0 start <name>"
select_one "select" "${2}";;
end)
start_end "$@";;
select)
select_one "$@";;
db)
debug_one "$@";;
doctor)
doctor ;;
env)
show_proxy_exports ;;
theme)
shift; theme_cmd "${1:-show}" ;;
sysproxy)
shift; sysproxy_cmd "${1:-status}" ;;
update)
shift; update_providers_cmd "${1:-}" ;;
mode)
shift; mode_set "${1:-}" ;;
rules)
shift; rules_cmd "${1:-show}" ;;
help|-h|--help) help ;;
-l|--list|list) shift; list_nodes "${1:-}" ;;
tun) shift; tun "${1:-toggle}" ;;
*) pick_switch_with_delay "${1:-}" 1200 ;;
esac
}
main "$@"