Add gum TUI (default mode) + Walker floating launcher
Context-aware menu: Connect phone (USB wait + Trust walkthrough), Switch internet → iPhone / back, live status header. install now writes a TUI.float .desktop entry and restarts elephant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b0374f87e7
commit
52c21e2467
2 changed files with 303 additions and 36 deletions
51
README.md
51
README.md
|
|
@ -1,8 +1,34 @@
|
||||||
# omatether
|
# omatether
|
||||||
|
|
||||||
iPhone USB tethering for **Omarchy / Arch** (systemd-networkd + iwd stack).
|
iPhone USB tethering for **Omarchy / Arch** (systemd-networkd + iwd stack),
|
||||||
Plug an iPhone in over USB and use its internet connection — the phone's
|
with a gum TUI. Plug an iPhone in over USB and use its internet connection —
|
||||||
Wi-Fi if it's on one, cellular otherwise. Fully headless, no GUI required.
|
the phone's Wi-Fi if it's on one, cellular otherwise. Works headless too.
|
||||||
|
|
||||||
|
## TUI
|
||||||
|
|
||||||
|
Run `omatether.sh` with no arguments (or launch **iPhone Tether** from Walker —
|
||||||
|
it opens as a floating window). The dashboard shows phone / tether / route
|
||||||
|
state live and offers only the actions that make sense right now:
|
||||||
|
|
||||||
|
```
|
||||||
|
╭───────────────────────╮
|
||||||
|
│ OMATETHER │
|
||||||
|
│ iPhone USB internet │
|
||||||
|
╰───────────────────────╯
|
||||||
|
Cowboy Bebop — paired
|
||||||
|
Tether up: eth0 172.20.10.11/28
|
||||||
|
Internet via enp6s0 (phone is fallback)
|
||||||
|
|
||||||
|
> Switch internet → iPhone
|
||||||
|
Status (full check)
|
||||||
|
Refresh
|
||||||
|
Quit
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Connect phone** — waits for USB, walks you through unlock + Trust, brings
|
||||||
|
the connection up (shown only when not yet connected)
|
||||||
|
- **Switch internet → iPhone** — routes all traffic via the phone
|
||||||
|
- **Switch back → ethernet/wifi** — back to normal (tether stays as fallback)
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
|
|
@ -10,28 +36,31 @@ Wi-Fi if it's on one, cellular otherwise. Fully headless, no GUI required.
|
||||||
- the kernel's `ipheth` driver exposes the phone as a normal network interface
|
- the kernel's `ipheth` driver exposes the phone as a normal network interface
|
||||||
- a `systemd-networkd` profile (`10-iphone-tether.network`) matches by
|
- a `systemd-networkd` profile (`10-iphone-tether.network`) matches by
|
||||||
`Driver=ipheth` and DHCPs it, at **fallback priority** (route metric 750)
|
`Driver=ipheth` and DHCPs it, at **fallback priority** (route metric 750)
|
||||||
so it never hijacks ethernet (100) or Wi-Fi (600)
|
so it never hijacks ethernet (100) or Wi-Fi (600); "Switch → iPhone"
|
||||||
|
rewrites the metric to 50 and reloads networkd
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./omatether.sh install # packages + networkd profile
|
./omatether.sh install # packages + networkd profile + Walker launcher
|
||||||
./omatether.sh pair # unlock phone, tap Trust
|
./omatether.sh # TUI: pick "Connect phone", unlock, tap Trust
|
||||||
```
|
```
|
||||||
|
|
||||||
Then on the phone: **Settings → Personal Hotspot → Allow Others to Join: ON**.
|
Then on the phone: **Settings → Personal Hotspot → Allow Others to Join: ON**.
|
||||||
The connection comes up automatically whenever the phone is plugged in.
|
After that it's zero-touch — plug in and the connection comes up by itself.
|
||||||
|
|
||||||
## Commands
|
## CLI
|
||||||
|
|
||||||
|
Everything is scriptable without the TUI:
|
||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `install` | Install usbmuxd/libimobiledevice, write networkd profile |
|
| `install` | Install packages, networkd profile, Walker launcher |
|
||||||
| `pair` / `unpair` | Manage the Trust pairing |
|
| `pair` / `unpair` | Manage the Trust pairing |
|
||||||
| `status` | Device, interface, IP, route, live connectivity test |
|
| `status` | Device, interface, IP, route, live connectivity test |
|
||||||
| `priority high` | Route all traffic via the phone even when ethernet is up |
|
| `priority high` | Route all traffic via the phone even when ethernet is up |
|
||||||
| `priority low` | Back to fallback-only (default) |
|
| `priority low` | Back to fallback-only (default) |
|
||||||
| `uninstall` | Remove the networkd profile |
|
| `uninstall` | Remove the networkd profile + launcher |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|
@ -39,3 +68,5 @@ The connection comes up automatically whenever the phone is plugged in.
|
||||||
- The tether interface usually appears as `eth0`; the profile matches by driver, not name.
|
- The tether interface usually appears as `eth0`; the profile matches by driver, not name.
|
||||||
- `RequiredForOnline=no` — an unplugged phone never blocks boot.
|
- `RequiredForOnline=no` — an unplugged phone never blocks boot.
|
||||||
- DNS follows the Omarchy convention (`UseDNS=no`; global resolvers via systemd-resolved).
|
- DNS follows the Omarchy convention (`UseDNS=no`; global resolvers via systemd-resolved).
|
||||||
|
- If the phone was plugged in before usbmuxd existed it can show as unresponsive —
|
||||||
|
the tool restarts usbmuxd automatically to rescan.
|
||||||
|
|
|
||||||
288
omatether.sh
288
omatether.sh
|
|
@ -5,17 +5,19 @@
|
||||||
# Plug an iPhone in over USB and use its internet connection (the phone's
|
# Plug an iPhone in over USB and use its internet connection (the phone's
|
||||||
# Wi-Fi, or cellular if it isn't on Wi-Fi). Works headless — no GUI needed.
|
# Wi-Fi, or cellular if it isn't on Wi-Fi). Works headless — no GUI needed.
|
||||||
#
|
#
|
||||||
# Usage: omatether.sh <install|pair|unpair|status|priority|uninstall|help>
|
# Run with no arguments for the interactive TUI (requires gum), or:
|
||||||
|
# omatether.sh <install|pair|unpair|status|priority|uninstall|help>
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
NETFILE="/etc/systemd/network/10-iphone-tether.network"
|
NETFILE="/etc/systemd/network/10-iphone-tether.network"
|
||||||
|
DESKTOP_FILE="$HOME/.local/share/applications/omatether.desktop"
|
||||||
|
|
||||||
# Route metrics: Omarchy ships ethernet=100, wifi=600, wwan=700.
|
# Route metrics: Omarchy ships ethernet=100, wifi=600, wwan=700.
|
||||||
METRIC_FALLBACK=750 # tether used only when nothing better is up (default)
|
METRIC_FALLBACK=750 # tether used only when nothing better is up (default)
|
||||||
METRIC_PREFERRED=50 # tether beats ethernet/wifi while plugged in
|
METRIC_PREFERRED=50 # tether beats ethernet/wifi while plugged in
|
||||||
|
|
||||||
RED=$'\e[31m'; GRN=$'\e[32m'; YLW=$'\e[33m'; RST=$'\e[0m'
|
RED=$'\e[31m'; GRN=$'\e[32m'; YLW=$'\e[33m'; DIM=$'\e[2m'; RST=$'\e[0m'
|
||||||
say() { printf '%s\n' "$*"; }
|
say() { printf '%s\n' "$*"; }
|
||||||
ok() { printf '%s\n' "${GRN}✓${RST} $*"; }
|
ok() { printf '%s\n' "${GRN}✓${RST} $*"; }
|
||||||
warn() { printf '%s\n' "${YLW}!${RST} $*"; }
|
warn() { printf '%s\n' "${YLW}!${RST} $*"; }
|
||||||
|
|
@ -25,6 +27,8 @@ as_root() {
|
||||||
if [[ $EUID -eq 0 ]]; then "$@"; else sudo "$@"; fi
|
if [[ $EUID -eq 0 ]]; then "$@"; else sudo "$@"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- probes ---
|
||||||
|
|
||||||
# Find the network interface backed by the ipheth driver, if any.
|
# Find the network interface backed by the ipheth driver, if any.
|
||||||
tether_iface() {
|
tether_iface() {
|
||||||
local dev drv
|
local dev drv
|
||||||
|
|
@ -35,6 +39,41 @@ tether_iface() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
phone_on_usb() { lsusb 2>/dev/null | grep -qi 'apple.*iphone'; }
|
||||||
|
phone_visible() { idevice_id -l 2>/dev/null | grep -q .; }
|
||||||
|
phone_paired() { idevicepair validate >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
phone_name() { ideviceinfo -k DeviceName 2>/dev/null || echo "iPhone"; }
|
||||||
|
|
||||||
|
tether_ip() {
|
||||||
|
local iface=$1
|
||||||
|
ip -4 -br addr show "$iface" 2>/dev/null | awk '{print $3}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Interface currently carrying the default route.
|
||||||
|
active_uplink() {
|
||||||
|
ip route get 1.1.1.1 2>/dev/null \
|
||||||
|
| awk '{for(i=1;i<NF;i++) if($i=="dev"){print $(i+1); exit}}'
|
||||||
|
}
|
||||||
|
|
||||||
|
current_metric() {
|
||||||
|
awk -F= '/^RouteMetric/{print $2; exit}' "$NETFILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
net_test() {
|
||||||
|
local iface=$1
|
||||||
|
curl -sf --interface "$iface" --max-time 5 -o /dev/null https://www.google.com
|
||||||
|
}
|
||||||
|
|
||||||
|
# usbmuxd drops devices that were plugged in before it started
|
||||||
|
# ("device unconfigured") — a restart rescans the bus.
|
||||||
|
revive_usbmuxd() {
|
||||||
|
as_root systemctl restart usbmuxd
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ networkd -----
|
||||||
|
|
||||||
write_netfile() {
|
write_netfile() {
|
||||||
local metric=$1
|
local metric=$1
|
||||||
as_root tee "$NETFILE" >/dev/null <<EOF
|
as_root tee "$NETFILE" >/dev/null <<EOF
|
||||||
|
|
@ -71,38 +110,76 @@ reload_networkd() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------- CLI commands ----
|
||||||
|
|
||||||
cmd_install() {
|
cmd_install() {
|
||||||
say "Installing iPhone USB tethering support…"
|
say "Installing iPhone USB tethering support…"
|
||||||
as_root pacman -S --needed --noconfirm usbmuxd libimobiledevice
|
as_root pacman -S --needed --noconfirm usbmuxd libimobiledevice gum
|
||||||
ok "usbmuxd + libimobiledevice installed (usbmuxd starts on demand via udev)"
|
ok "usbmuxd + libimobiledevice installed (usbmuxd starts on demand via udev)"
|
||||||
|
|
||||||
write_netfile "$METRIC_FALLBACK"
|
write_netfile "$METRIC_FALLBACK"
|
||||||
reload_networkd
|
reload_networkd
|
||||||
ok "networkd profile written: $NETFILE (fallback priority, metric $METRIC_FALLBACK)"
|
ok "networkd profile written: $NETFILE (fallback priority, metric $METRIC_FALLBACK)"
|
||||||
|
|
||||||
|
# Floating TUI launcher for Walker (Omarchy TUI.float pattern).
|
||||||
|
local self
|
||||||
|
self=$(realpath "$0")
|
||||||
|
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
||||||
|
cat > "$DESKTOP_FILE" <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=iPhone Tether
|
||||||
|
Comment=Use an iPhone's internet over USB
|
||||||
|
Exec=xdg-terminal-exec --app-id=TUI.float -e $self
|
||||||
|
Icon=phone
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Network;
|
||||||
|
EOF
|
||||||
|
systemctl --user restart elephant.service 2>/dev/null || true
|
||||||
|
ok "Walker launcher installed: 'iPhone Tether' (floating window)"
|
||||||
|
|
||||||
# Nudge udev in case the phone was already plugged in before install.
|
# Nudge udev in case the phone was already plugged in before install.
|
||||||
as_root udevadm trigger -s usb --action=add 2>/dev/null || true
|
as_root udevadm trigger -s usb --action=add 2>/dev/null || true
|
||||||
|
|
||||||
say ""
|
say ""
|
||||||
say "Next: unlock the iPhone, then run: $0 pair"
|
say "Run '$0' (no arguments) for the TUI, or '$0 pair' to pair now."
|
||||||
|
}
|
||||||
|
|
||||||
|
do_pair_once() {
|
||||||
|
# Returns 0 on success; prints the failure reason otherwise.
|
||||||
|
local out
|
||||||
|
if out=$(idevicepair pair 2>&1); then
|
||||||
|
ok "$out"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case $out in
|
||||||
|
*passcode*) warn "Phone is locked — unlock it." ;;
|
||||||
|
*user\ denied*|*denied\ the\ trust*) warn "Trust was declined on the phone." ;;
|
||||||
|
*Please\ accept*) warn "Trust dialog showing on the phone — tap Trust." ;;
|
||||||
|
*) warn "$out" ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retry pairing until it succeeds or times out (arg: seconds, default 120).
|
||||||
|
pair_wait() {
|
||||||
|
local deadline=$(( SECONDS + ${1:-120} ))
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
idevicepair pair >/dev/null 2>&1 && return 0
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_pair() {
|
cmd_pair() {
|
||||||
command -v idevicepair >/dev/null || die "libimobiledevice not installed — run: $0 install"
|
command -v idevicepair >/dev/null || die "libimobiledevice not installed — run: $0 install"
|
||||||
idevice_id -l 2>/dev/null | grep -q . || die "No iPhone detected over USB. Plug it in and try again."
|
if ! phone_visible; then
|
||||||
|
phone_on_usb && revive_usbmuxd
|
||||||
|
phone_visible || die "No iPhone detected over USB. Plug it in and try again."
|
||||||
|
fi
|
||||||
|
|
||||||
say "Pairing — unlock the iPhone and tap ${GRN}Trust${RST} when prompted…"
|
say "Pairing — unlock the iPhone and tap ${GRN}Trust${RST} when prompted…"
|
||||||
local out
|
do_pair_once || exit 1
|
||||||
if out=$(idevicepair pair 2>&1); then
|
|
||||||
ok "$out"
|
|
||||||
else
|
|
||||||
case $out in
|
|
||||||
*passcode*) die "Phone is locked. Unlock it, then re-run: $0 pair" ;;
|
|
||||||
*user\ denied*|*denied\ the\ trust*) die "Trust was declined on the phone. Re-run and tap Trust." ;;
|
|
||||||
*Please\ accept*) warn "Trust dialog is showing on the phone — tap Trust, then re-run: $0 pair"; exit 1 ;;
|
|
||||||
*) die "$out" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
say ""
|
say ""
|
||||||
say "Now enable the hotspot on the phone:"
|
say "Now enable the hotspot on the phone:"
|
||||||
|
|
@ -119,9 +196,9 @@ cmd_status() {
|
||||||
|
|
||||||
udid=$(idevice_id -l 2>/dev/null | head -1 || true)
|
udid=$(idevice_id -l 2>/dev/null | head -1 || true)
|
||||||
if [[ -n $udid ]]; then
|
if [[ -n $udid ]]; then
|
||||||
name=$(ideviceinfo -k DeviceName 2>/dev/null || echo "unknown")
|
name=$(phone_name)
|
||||||
ok "iPhone connected over USB: $name"
|
ok "iPhone connected over USB: $name"
|
||||||
if idevicepair validate >/dev/null 2>&1; then
|
if phone_paired; then
|
||||||
ok "Paired (trusted)"
|
ok "Paired (trusted)"
|
||||||
else
|
else
|
||||||
warn "Not paired — run: $0 pair"
|
warn "Not paired — run: $0 pair"
|
||||||
|
|
@ -134,11 +211,11 @@ cmd_status() {
|
||||||
local state addr
|
local state addr
|
||||||
state=$(networkctl status "$iface" 2>/dev/null | awk '/State:/{print $2; exit}' || true)
|
state=$(networkctl status "$iface" 2>/dev/null | awk '/State:/{print $2; exit}' || true)
|
||||||
ok "Tether interface: $iface ($state)"
|
ok "Tether interface: $iface ($state)"
|
||||||
addr=$(ip -4 -br addr show "$iface" | awk '{print $3}')
|
addr=$(tether_ip "$iface")
|
||||||
if [[ -n $addr ]]; then
|
if [[ -n $addr ]]; then
|
||||||
ok "IPv4: $addr"
|
ok "IPv4: $addr"
|
||||||
ip route show default dev "$iface" | sed 's/^/ /'
|
ip route show default dev "$iface" | sed 's/^/ /'
|
||||||
if curl -sf --interface "$iface" --max-time 5 -o /dev/null https://www.google.com; then
|
if net_test "$iface"; then
|
||||||
ok "Internet via $iface works"
|
ok "Internet via $iface works"
|
||||||
else
|
else
|
||||||
warn "No internet through $iface yet (is Personal Hotspot on?)"
|
warn "No internet through $iface yet (is Personal Hotspot on?)"
|
||||||
|
|
@ -165,9 +242,7 @@ cmd_priority() {
|
||||||
ok "Tether now FALLBACK (metric $METRIC_FALLBACK) — ethernet/wifi win when available"
|
ok "Tether now FALLBACK (metric $METRIC_FALLBACK) — ethernet/wifi win when available"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
local cur
|
say "Current metric: $(current_metric) (ethernet=100, wifi=600)"
|
||||||
cur=$(awk -F= '/^RouteMetric/{print $2; exit}' "$NETFILE")
|
|
||||||
say "Current metric: $cur (ethernet=100, wifi=600)"
|
|
||||||
say "Usage: $0 priority <high|low>"
|
say "Usage: $0 priority <high|low>"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -175,11 +250,172 @@ cmd_priority() {
|
||||||
|
|
||||||
cmd_uninstall() {
|
cmd_uninstall() {
|
||||||
as_root rm -f "$NETFILE"
|
as_root rm -f "$NETFILE"
|
||||||
|
rm -f "$DESKTOP_FILE"
|
||||||
as_root networkctl reload
|
as_root networkctl reload
|
||||||
ok "Removed $NETFILE (usbmuxd/libimobiledevice left installed)"
|
systemctl --user restart elephant.service 2>/dev/null || true
|
||||||
|
ok "Removed $NETFILE and the Walker launcher (usbmuxd/libimobiledevice left installed)"
|
||||||
}
|
}
|
||||||
|
|
||||||
case ${1:-help} in
|
# ----------------------------------------------------------------- TUI -----
|
||||||
|
|
||||||
|
# Foot spawns TUI.float windows at 80 cols and snaps to real geometry over
|
||||||
|
# ~40ms; render before that settles and gum boxes fragment. Poll until two
|
||||||
|
# consecutive width reads agree (max 250ms).
|
||||||
|
poll_terminal_size() {
|
||||||
|
local prev curr i
|
||||||
|
prev=$(tput cols 2>/dev/null || echo 80)
|
||||||
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
sleep 0.025
|
||||||
|
curr=$(tput cols 2>/dev/null || echo 80)
|
||||||
|
[[ $curr == "$prev" && $i -ge 2 ]] && return 0
|
||||||
|
prev=$curr
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
tui_header() {
|
||||||
|
gum style --border rounded --padding "0 2" --margin "0 0 1 0" \
|
||||||
|
--border-foreground 212 --bold " OMATETHER" "${DIM}iPhone USB internet${RST}"
|
||||||
|
}
|
||||||
|
|
||||||
|
tui_connect() {
|
||||||
|
say ""
|
||||||
|
if ! phone_visible; then
|
||||||
|
phone_on_usb && revive_usbmuxd
|
||||||
|
fi
|
||||||
|
if ! phone_visible; then
|
||||||
|
# shellcheck disable=SC2016 # expansion happens in the child shell
|
||||||
|
gum spin --title "Plug the iPhone in via USB…" -- bash -c \
|
||||||
|
'for i in $(seq 1 24); do idevice_id -l 2>/dev/null | grep -q . && exit 0; sleep 5; done; exit 1' \
|
||||||
|
|| { warn "No iPhone appeared. Check the cable and try again."; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! phone_paired; then
|
||||||
|
say "Unlock the phone and tap ${GRN}Trust${RST} when the dialog appears."
|
||||||
|
idevicepair pair >/dev/null 2>&1 || true # trigger the Trust dialog
|
||||||
|
# shellcheck disable=SC2016 # expansion happens in the child shell
|
||||||
|
gum spin --title "Waiting for Trust… (unlock the phone)" -- bash -c \
|
||||||
|
'for i in $(seq 1 24); do idevicepair pair >/dev/null 2>&1 && exit 0; sleep 5; done; exit 1' \
|
||||||
|
|| { warn "Pairing timed out. Unlock the phone and pick Connect again."; return 1; }
|
||||||
|
ok "Paired with $(phone_name)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Give networkd a moment to DHCP the freshly-authorised interface.
|
||||||
|
local iface=""
|
||||||
|
gum spin --title "Bringing the connection up…" -- sleep 4 || true
|
||||||
|
if iface=$(tether_iface) && [[ -n $(tether_ip "$iface") ]] && net_test "$iface"; then
|
||||||
|
ok "Connected — internet available through the phone ($iface)"
|
||||||
|
else
|
||||||
|
warn "Paired, but no connection yet. On the phone enable:"
|
||||||
|
say " Settings → Personal Hotspot → Allow Others to Join: ON"
|
||||||
|
fi
|
||||||
|
gum input --placeholder "Press Enter to continue…" >/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
tui_switch() {
|
||||||
|
local target=$1 iface
|
||||||
|
say ""
|
||||||
|
if [[ $target == phone ]]; then
|
||||||
|
write_netfile "$METRIC_PREFERRED"
|
||||||
|
else
|
||||||
|
write_netfile "$METRIC_FALLBACK"
|
||||||
|
fi
|
||||||
|
reload_networkd
|
||||||
|
gum spin --title "Applying routes…" -- sleep 3 || true
|
||||||
|
|
||||||
|
local uplink
|
||||||
|
uplink=$(active_uplink)
|
||||||
|
iface=$(tether_iface || true)
|
||||||
|
if [[ $target == phone && $uplink == "$iface" ]]; then
|
||||||
|
ok "All traffic now routes via the iPhone ($iface)"
|
||||||
|
elif [[ $target == normal && $uplink != "$iface" ]]; then
|
||||||
|
ok "Back to normal — traffic routes via ${uplink:-your usual connection}"
|
||||||
|
else
|
||||||
|
warn "Route didn't settle as expected (uplink: ${uplink:-none}). Check Status."
|
||||||
|
fi
|
||||||
|
gum input --placeholder "Press Enter to continue…" >/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_tui() {
|
||||||
|
command -v gum >/dev/null || die "gum is required for the TUI — run: $0 install"
|
||||||
|
poll_terminal_size
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
clear
|
||||||
|
tui_header
|
||||||
|
|
||||||
|
# --- gather state ---
|
||||||
|
local iface="" addr="" uplink="" metric mode phone_line tether_line uplink_line
|
||||||
|
local connected=false paired=false
|
||||||
|
metric=$(current_metric || true)
|
||||||
|
mode="fallback"; [[ $metric == "$METRIC_PREFERRED" ]] && mode="preferred"
|
||||||
|
iface=$(tether_iface || true)
|
||||||
|
[[ -n $iface ]] && addr=$(tether_ip "$iface")
|
||||||
|
uplink=$(active_uplink)
|
||||||
|
|
||||||
|
if phone_visible; then
|
||||||
|
paired=false; phone_paired && paired=true
|
||||||
|
if $paired; then
|
||||||
|
phone_line="${GRN}${RST} $(phone_name) — paired"
|
||||||
|
else
|
||||||
|
phone_line="${YLW}${RST} iPhone detected — not trusted yet"
|
||||||
|
fi
|
||||||
|
elif phone_on_usb; then
|
||||||
|
phone_line="${YLW}${RST} iPhone on USB — not responding (will retry)"
|
||||||
|
else
|
||||||
|
phone_line="${DIM} No iPhone plugged in${RST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n $iface && -n $addr ]]; then
|
||||||
|
connected=true
|
||||||
|
tether_line="${GRN}${RST} Tether up: $iface $addr"
|
||||||
|
elif [[ -n $iface ]]; then
|
||||||
|
tether_line="${YLW}${RST} Interface $iface up, no IP (hotspot off?)"
|
||||||
|
else
|
||||||
|
tether_line="${DIM} No tether connection${RST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n $uplink && $uplink == "$iface" ]]; then
|
||||||
|
uplink_line="${GRN}${RST} Internet via iPhone"
|
||||||
|
elif [[ -n $uplink ]]; then
|
||||||
|
uplink_line=" Internet via $uplink ${DIM}(phone is $mode)${RST}"
|
||||||
|
else
|
||||||
|
uplink_line="${RED}${RST} No internet route"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n%s\n%s\n\n' "$phone_line" "$tether_line" "$uplink_line"
|
||||||
|
|
||||||
|
# --- context-aware menu ---
|
||||||
|
local -a menu=()
|
||||||
|
[[ -f $NETFILE ]] || menu+=("Install tethering support")
|
||||||
|
if ! $connected; then
|
||||||
|
menu+=("Connect phone")
|
||||||
|
fi
|
||||||
|
if $connected && [[ $uplink != "$iface" ]]; then
|
||||||
|
menu+=("Switch internet → iPhone")
|
||||||
|
fi
|
||||||
|
if [[ $mode == preferred ]]; then
|
||||||
|
menu+=("Switch back → ethernet/wifi")
|
||||||
|
fi
|
||||||
|
menu+=("Status (full check)" "Refresh" "Quit")
|
||||||
|
|
||||||
|
local choice
|
||||||
|
choice=$(gum choose --header "What do you want to do?" "${menu[@]}") || break
|
||||||
|
case $choice in
|
||||||
|
"Install tethering support") cmd_install; gum input --placeholder "Press Enter…" >/dev/null || true ;;
|
||||||
|
"Connect phone") tui_connect ;;
|
||||||
|
"Switch internet → iPhone") tui_switch phone ;;
|
||||||
|
"Switch back → ethernet/wifi") tui_switch normal ;;
|
||||||
|
"Status (full check)") say ""; cmd_status || true; gum input --placeholder "Press Enter…" >/dev/null || true ;;
|
||||||
|
"Refresh") ;;
|
||||||
|
"Quit"|"") break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- main -----
|
||||||
|
|
||||||
|
case ${1:-tui} in
|
||||||
|
tui) cmd_tui ;;
|
||||||
install) cmd_install ;;
|
install) cmd_install ;;
|
||||||
pair) cmd_pair ;;
|
pair) cmd_pair ;;
|
||||||
unpair) cmd_unpair ;;
|
unpair) cmd_unpair ;;
|
||||||
|
|
@ -187,7 +423,7 @@ case ${1:-help} in
|
||||||
priority) shift; cmd_priority "${1:-}" ;;
|
priority) shift; cmd_priority "${1:-}" ;;
|
||||||
uninstall) cmd_uninstall ;;
|
uninstall) cmd_uninstall ;;
|
||||||
help|-h|--help)
|
help|-h|--help)
|
||||||
sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'
|
sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
;;
|
;;
|
||||||
*) die "Unknown command: $1 (try: $0 help)" ;;
|
*) die "Unknown command: $1 (try: $0 help)" ;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue