//
// syd-tui: Syd's Terminal User Interface
// tui/src/main.rs: Main entry point
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    borrow::Cow,
    cmp::{max, min},
    env,
    ffi::OsString,
    fmt::Display,
    io::{self, Write},
    os::{
        fd::{AsFd, AsRawFd, FromRawFd, OwnedFd, RawFd},
        unix::process::{CommandExt, ExitStatusExt},
    },
    process::Stdio,
    sync::LazyLock,
    time::Duration,
};

use data_encoding::HEXLOWER;
use libc::{
    syscall, SYS_ioctl, SYS_pidfd_open, SYS_pidfd_send_signal, STDIN_FILENO, TIOCGWINSZ, TIOCSCTTY,
    TIOCSWINSZ,
};
use nix::{
    errno::Errno,
    fcntl::{fcntl, openat2, FcntlArg, FdFlag, OFlag, OpenHow, ResolveFlag, AT_FDCWD},
    libc,
    pty::{openpty, OpenptyResult, Winsize},
    sys::{
        signal::{raise, Signal},
        socket::{
            connect, getsockopt, socket, sockopt::SocketError, AddressFamily, SockFlag, SockType,
            UnixAddr,
        },
        stat::Mode as OpenMode,
        termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg, Termios},
    },
    unistd::{dup, isatty, pipe2, setsid, Pid},
};
use ratatui::{
    backend::TermionBackend,
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span, Text},
    widgets::{Block, Borders, Clear, Paragraph, Wrap},
    Terminal,
};
use termion::{
    raw::{IntoRawMode, RawTerminal},
    screen::{AlternateScreen, IntoAlternateScreen},
};
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::UnixStream,
    runtime::Builder,
    sync::mpsc,
    task::JoinHandle,
    time::{interval, MissedTickBehavior},
};

//
// Modules
//

// OS Random Number Generator (RNG) interface
mod rng;

//
// Compile-time tunables
//

const PKG_NAME: &str = env!("CARGO_PKG_NAME");
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
const PKG_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
const PKG_LICENSE: &str = env!("CARGO_PKG_LICENSE");

static PKG_HEADER_1: LazyLock<String> = LazyLock::new(|| format!("{PKG_NAME} {PKG_VERSION}"));
static PKG_HEADER_2: LazyLock<String> = LazyLock::new(|| PKG_DESCRIPTION.to_string());
static PKG_HEADER_3: LazyLock<String> =
    LazyLock::new(|| format!("Copyright (c) 2025 {PKG_AUTHORS}"));
static PKG_HEADER_4: LazyLock<String> =
    LazyLock::new(|| format!("SPDX-License-Identifier: {PKG_LICENSE}"));

// Stack size for the worker threads.
// Defaults to 256k.
const TUI_STACK_SIZE: usize = 256 * 1024;

const INITIAL_TEXTBUF_RESERVE: usize = 64 * 1024;
const CMD_BUFFER_CAP: usize = 2 * 1024 * 1024;
const LOG_BUFFER_CAP: usize = 8 * 1024 * 1024;
const IPC_BUFFER_CAP: usize = 2 * 1024 * 1024;
const API_BUFFER_CAP: usize = 8 * 1024 * 1024;
const SYS_BUFFER_CAP: usize = 2 * 1024 * 1024;
const MSG_BUFFER_CAP: usize = 1024 * 1024;

const IO_READ_CHUNK: usize = 8192;
const SAVE_WRITE_CHUNK: usize = 1024 * 1024;

const PROMPT_PERCENT_X: u16 = 70;
const PROMPT_PERCENT_Y: u16 = 20;

const TICK_MS: u64 = 33;
const CHAN_CAPACITY: usize = 1024;
const RAND_HEX_LEN: usize = 96;

const RAINBOW: &[Color] = &[
    Color::Red,
    Color::Yellow,
    Color::Green,
    Color::Cyan,
    Color::Blue,
    Color::Magenta,
    Color::White,
];

//
// Tabs & Modes
//

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Tab {
    Help = 0,
    Cmd = 1,
    Log = 2,
    Ipc = 3,
    Api = 4,
    Sys = 5,
    Msg = 6,
}

impl Tab {
    fn next(self) -> Self {
        match self {
            Self::Help => Self::Cmd,
            Self::Cmd => Self::Log,
            Self::Log => Self::Ipc,
            Self::Ipc => Self::Api,
            Self::Api => Self::Sys,
            Self::Sys => Self::Msg,
            Self::Msg => Self::Help,
        }
    }
    fn prev(self) -> Self {
        match self {
            Self::Help => Self::Msg,
            Self::Cmd => Self::Help,
            Self::Log => Self::Cmd,
            Self::Ipc => Self::Log,
            Self::Api => Self::Ipc,
            Self::Sys => Self::Api,
            Self::Msg => Self::Sys,
        }
    }
    fn from_index(n: u8) -> Option<Self> {
        match n {
            0 => Some(Self::Help),
            1 => Some(Self::Cmd),
            2 => Some(Self::Log),
            3 => Some(Self::Ipc),
            4 => Some(Self::Api),
            5 => Some(Self::Sys),
            6 => Some(Self::Msg),
            _ => None,
        }
    }
    fn is_content(self) -> bool {
        matches!(self, Self::Log | Self::Api | Self::Sys | Self::Msg)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Mode {
    Normal,
    Insert,
}

//
// Tiny bounded text buffer
//

struct TextBuffer {
    data: String,
    cap_bytes: usize,
    // number of lines scrolled from bottom (0 = bottom)
    scroll: u16,
}

impl TextBuffer {
    fn new(cap_bytes: usize) -> Self {
        Self {
            data: String::with_capacity(min(cap_bytes, INITIAL_TEXTBUF_RESERVE)),
            cap_bytes,
            scroll: 0,
        }
    }

    fn push_bytes(&mut self, bytes: &[u8]) {
        if bytes.is_empty() {
            return;
        }
        let s = String::from_utf8_lossy(bytes);
        self.push_str(&s);
    }

    fn push_str(&mut self, s: &str) {
        self.data.push_str(s);
        if self.data.len() > self.cap_bytes {
            let excess = self.data.len() - self.cap_bytes;
            // Drop up to next character boundary.
            let cut = self
                .data
                .char_indices()
                .skip_while(|(i, _)| *i < excess)
                .map(|(i, _)| i)
                .next()
                .unwrap_or(excess);
            self.data.drain(..cut);
        }
    }

    fn lines_count(&self) -> usize {
        self.data.lines().count().max(1)
    }

    /*
    /// Visible Text for viewport of height `h` lines (no highlighting).
    fn view(&self, h: u16) -> Text<'static> {
        self.view_with_options(h, u16::MAX, 0, None, None)
    }
    */

    // Visible Text with highlight + horizontal scrolling + optional line numbers.
    fn view_with_options(
        &self,
        h: u16,
        w: u16,
        hscroll: u16,
        pat: Option<&str>,
        number_width_opt: Option<usize>,
    ) -> Text<'static> {
        let total = self.lines_count() as i64;
        let h_i = h as i64;
        let scroll = self.scroll as i64;
        let bottom = total - scroll;
        let top = max(0, bottom - h_i);

        let mut txt = Text::default();
        let pat = pat.unwrap_or("");
        let do_hl = !pat.is_empty();
        let have_w = w < u16::MAX;
        let w_us = w as usize;
        let hscroll_us = hscroll as usize;

        let number_width = number_width_opt.unwrap_or(0);
        let number_prefix = if number_width > 0 {
            number_width + 1
        } else {
            0
        }; // +1 for space

        for (i, line) in self.data.lines().enumerate() {
            let i64i = i as i64;
            if i64i < top || i64i >= bottom {
                continue;
            }

            let mut spans: Vec<Span> = Vec::new();

            // numbers like vim
            if number_width > 0 {
                spans.push(Span::styled(
                    format!("{:>width$} ", i + 1, width = number_width),
                    Style::default().fg(Color::DarkGray),
                ));
            }

            let content = line;

            // Compute visible slice by chars.
            let content_chars: Vec<char> = content.chars().collect();
            let mut right_marker = false;

            let avail = if have_w {
                w_us.saturating_sub(number_prefix)
            } else {
                usize::MAX
            };

            let mut visible: String = String::new();
            if have_w && avail == 0 {
                // Nothing to draw beyond numbers (unlikely).
            } else {
                let len = content_chars.len();
                let start = min(hscroll_us, len);
                if have_w {
                    let take_cap = avail;
                    let end = min(start + take_cap, len);
                    let slice: String = content_chars[start..end].iter().collect();
                    visible.push_str(&slice);
                    right_marker = end < len;
                    if hscroll_us > 0 {
                        spans.push(Span::styled("←", Style::default().fg(Color::DarkGray)));
                        if visible.len() >= avail && !visible.is_empty() {
                            visible.remove(0);
                        }
                    }
                } else {
                    visible.push_str(&content[start..]);
                }
            }

            if do_hl && !pat.is_empty() {
                // Highlight inside the visible part.
                let mut start = 0usize;
                while let Some(idx) = visible[start..].find(pat) {
                    let abs = start + idx;
                    if abs > start {
                        spans.push(Span::raw(visible[start..abs].to_string()));
                    }
                    spans.push(Span::styled(
                        pat.to_string(),
                        Style::default()
                            .fg(Color::Yellow)
                            .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
                    ));
                    start = abs + pat.len();
                }
                if start < visible.len() {
                    spans.push(Span::raw(visible[start..].to_string()));
                }
            } else {
                spans.push(Span::raw(visible));
            }

            if have_w && right_marker {
                spans.push(Span::styled("→", Style::default().fg(Color::DarkGray)));
            }

            txt.lines.push(Line::from(spans));
        }
        txt
    }

    fn visible_bounds(&self, h: u16) -> (usize, usize) {
        let total = self.lines_count() as i64;
        let h = h as i64;
        let scroll = self.scroll as i64;
        let bottom = (total - scroll - 1).max(0) as usize;
        let top = (bottom as i64 - (h - 1)).max(0) as usize;
        (top, bottom)
    }

    fn scroll_up(&mut self, n: u16) {
        let total = self.lines_count() as u16;
        self.scroll = min(self.scroll.saturating_add(n), total.saturating_sub(1));
    }

    fn scroll_down(&mut self, n: u16) {
        self.scroll = self.scroll.saturating_sub(n);
    }

    fn scroll_to_bottom(&mut self) {
        self.scroll = 0;
    }

    fn scroll_to_top(&mut self) {
        let total = self.lines_count() as u16;
        self.scroll = total.saturating_sub(1);
    }

    fn to_owned_string(&self) -> String {
        self.data.clone()
    }

    // Search helpers
    fn find_next_line(&self, pat: &str, after: Option<usize>) -> Option<usize> {
        let start = after.map_or(0, |i| i.saturating_add(1));
        for (i, line) in self.data.lines().enumerate().skip(start) {
            if line.contains(pat) {
                return Some(i);
            }
        }
        None
    }

    fn find_prev_line(&self, pat: &str, before: Option<usize>) -> Option<usize> {
        let total = self.lines_count();
        if total == 0 {
            return None;
        }
        let end = before.unwrap_or_else(|| total.saturating_sub(1));
        let mut found = None;
        for (i, line) in self.data.lines().enumerate() {
            if i > end {
                break;
            }
            if line.contains(pat) {
                found = Some(i);
            }
        }
        found
    }
}

//
// Search state per content tab
//

#[derive(Default)]
struct SearchState {
    pattern: Option<String>,
    preview: Option<String>,
    last_match: Option<usize>,
    last_forward: bool,
}

//
// UI input mux
//

#[derive(Debug)]
enum UiInput {
    Bytes(Vec<u8>),
    Resize(u16, u16), // cols, rows
    Quit,
    ChildExit(Option<i32>, Option<i32>),
    IpcExit(Option<i32>, Option<i32>),
    ApiData(Vec<u8>),
    SysData(Vec<u8>),
    ForceRedraw,
    Reconnect(bool), // force reconnect
    Suspend,         // ^Z
}

#[derive(Debug)]
enum Event {
    Cmd(Vec<u8>),
    Log(Vec<u8>),
    Ipc(Vec<u8>),
    Ui(UiInput),
    Tick,
}

// Backend type
type TuiBackend = TermionBackend<AlternateScreen<RawTerminal<io::Stdout>>>;

//
// App state
//

struct RenderSnapshot {
    title_line: Line<'static>,
    main_text: Text<'static>,
    status_text: Text<'static>,
    right_status: Line<'static>,
    bottom_prompt: Option<Line<'static>>,
    show_save_prompt: bool,
    save_prompt_text: Text<'static>,
}

struct App {
    terminal: Terminal<TuiBackend>,
    title: String,
    cmd_label: String, // "syd <args...>" for title
    active_tab: Tab,
    mode: Mode,

    buf_cmd: TextBuffer,
    buf_log: TextBuffer,
    buf_ipc: TextBuffer,
    buf_api: TextBuffer,
    buf_sys: TextBuffer,
    buf_msg: TextBuffer,

    hscroll_cmd: u16,
    hscroll_log: u16,
    hscroll_api: u16,
    hscroll_sys: u16,
    hscroll_msg: u16,

    // Numbering flags (default on)
    num_log: bool,
    num_api: bool,
    num_sys: bool,
    num_msg: bool,
    num_ipc: bool,

    // Save prompt
    save_prompt_active: bool,
    save_prompt_input: String,

    help_text: Text<'static>,

    pty_master_cmd: OwnedFd,
    pty_master_ipc: Option<OwnedFd>,

    area: Rect,
    status_line: Option<String>,
    status_error: bool,

    // ^Z is pressed.
    suspend: bool,

    cmd_dead: bool,
    ipc_dead: bool,

    // Per-tab search
    search_log: SearchState,
    search_api: SearchState,
    search_sys: SearchState,
    search_msg: SearchState,

    // IPC address (with @) for title
    ipc_addr: String,

    // IPC input + history
    ipc_input: String,
    ipc_hist: Vec<String>,
    ipc_hist_pos: Option<usize>,

    // CMD prompt input + history
    cmd_input: String,
    cmd_hist: Vec<String>,
    cmd_hist_pos: Option<usize>,

    // Ex ':' history
    ex_hist: Vec<String>,
    ex_hist_pos: Option<usize>,

    // Log line splitter for msg/tip extraction.
    log_accum: String,

    // Saved original termios to suspend/resume.
    saved_termios: Termios,

    // Syd version (first line)
    syd_version: Option<String>,

    // Child pid and pidfd cached
    child_pid: Option<Pid>,
    child_pfd: Option<OwnedFd>,

    // Stdin reader task
    stdin_task: Option<JoinHandle<()>>,

    // UI sender (to respawn stdin reader as needed)
    ui_tx: mpsc::Sender<UiInput>,
}

impl App {
    #[expect(clippy::too_many_arguments)]
    fn new(
        terminal: Terminal<TuiBackend>,
        title: &str,
        cmd_label: String,
        ipc_addr: String,
        pty_master_cmd: OwnedFd,
        pty_master_ipc: Option<OwnedFd>,
        saved_termios: Termios,
        child_pid: Option<Pid>,
        child_pfd: Option<OwnedFd>,
        ui_tx: mpsc::Sender<UiInput>,
    ) -> io::Result<Self> {
        let mut help = Text::default();
        help.lines.push(Line::from(PKG_HEADER_1.as_str()));
        help.lines.push(Line::from(PKG_HEADER_2.as_str()));
        help.lines.push(Line::from(PKG_HEADER_3.as_str()));
        help.lines.push(Line::from(PKG_HEADER_4.as_str()));
        help.lines.push(Line::from(""));
        help.lines.push(Line::from(
            " Windows: 0 help  1 cmd  2 log  3 ipc  4 api  5 sys  6 msg",
        ));
        help.lines.push(Line::from("  H/L    Prev/Next tab"));
        help.lines.push(Line::from("  0..6   Switch to tab index"));
        help.lines
            .push(Line::from("  g/G    Top/Bottom (content & ipc)"));
        help.lines.push(Line::from(
            "  i/Esc  Insert/Normal in cmd/ipc; others are Normal only",
        ));
        help.lines.push(Line::from(
            "  Ctrl-L Force redraw; Ctrl-G Cancel input; Ctrl-Z Suspend TUI",
        ));
        help.lines.push(Line::from(" Content (log/api/sys/msg):"));
        help.lines.push(Line::from(
            "  / ?    Search fwd/back;  n/N  next/prev (wrap); g/G top/bot",
        ));
        help.lines.push(Line::from(
            "  Up/Down PgUp/PgDn Home/End scroll; ←/→ horizontal scroll",
        ));
        help.lines
            .push(Line::from("  :w[!] FILE  Write buffer to FILE"));
        help.lines.push(Line::from(
            "  :set nu[mber]|nonu[mber]  Toggle line numbers (current tab)",
        ));
        help.lines.push(Line::from(" Cmd / IPC prompts:"));
        help.lines.push(Line::from(
            "  Cmd: Type is buffered; Enter sends. Ctrl-W delete word; Ctrl-G clear.",
        ));
        help.lines.push(Line::from(
            "  Cmd: Ctrl-C/Ctrl-Q/etc pass through immediately to Syd.",
        ));
        help.lines.push(Line::from(
            "  IPC: Line-edited; arrows/home/end/pgup/pgdn navigate history.",
        ));
        help.lines.push(Line::from(" Ex commands:"));
        help.lines.push(Line::from(
            "  :q   quit (refuses if Syd running)   :q!  kill Syd and quit",
        ));
        help.lines.push(Line::from(
            "  :next / :prev  tab cycle   :tab N  switch to N",
        ));
        help.lines.push(Line::from(
            "  :redr[aw][!] force redraw  :ve[rsion]  syd -V",
        ));
        help.lines.push(Line::from(
            "  :!CMD  run external interactive CMD; :rc, :sh shortcuts",
        ));
        help.lines
            .push(Line::from("  :e[dit] [FILE]            open $EDITOR"));
        help.lines.push(Line::from(
            "  :kill, :stop, :cont[inue] terminate, stop or resume Syd",
        ));
        help.lines
            .push(Line::from("  :re[connect][!]           reconnect ipc"));

        Ok(Self {
            terminal,
            title: format!("{PKG_NAME}: {title}"),
            cmd_label,
            active_tab: Tab::Cmd,
            mode: Mode::Normal,

            buf_cmd: TextBuffer::new(CMD_BUFFER_CAP),
            buf_log: TextBuffer::new(LOG_BUFFER_CAP),
            buf_ipc: TextBuffer::new(IPC_BUFFER_CAP),
            buf_api: TextBuffer::new(API_BUFFER_CAP),
            buf_sys: TextBuffer::new(SYS_BUFFER_CAP),
            buf_msg: TextBuffer::new(MSG_BUFFER_CAP),

            hscroll_cmd: 0,
            hscroll_log: 0,
            hscroll_api: 0,
            hscroll_sys: 0,
            hscroll_msg: 0,

            num_log: true,
            num_api: true,
            num_sys: true,
            num_msg: true,
            num_ipc: true,

            save_prompt_active: false,
            save_prompt_input: String::new(),

            help_text: help,

            pty_master_cmd,
            pty_master_ipc,

            area: Rect::new(0, 0, 80, 24),
            status_line: None,
            status_error: false,

            suspend: false,

            cmd_dead: false,
            ipc_dead: false,

            search_log: SearchState::default(),
            search_api: SearchState::default(),
            search_sys: SearchState::default(),
            search_msg: SearchState::default(),

            ipc_addr,

            ipc_input: String::new(),
            ipc_hist: Vec::new(),
            ipc_hist_pos: None,

            cmd_input: String::new(),
            cmd_hist: Vec::new(),
            cmd_hist_pos: None,

            ex_hist: Vec::new(),
            ex_hist_pos: None,

            log_accum: String::new(),

            saved_termios,

            syd_version: None,

            child_pid,
            child_pfd,

            stdin_task: None,

            ui_tx,
        })
    }

    fn set_title(&mut self, title: &str) {
        self.title = format!("{PKG_NAME}: {title}");
        // Set OSC title.
        let _ = io::Write::write_all(
            self.terminal.backend_mut(),
            format!("\x1b]0;{}\x07", self.title).as_bytes(),
        );
        let _ = self.terminal.backend_mut().flush();
    }

    fn update_osc_title_for_tab(&mut self) {
        let t = match self.active_tab {
            Tab::Cmd => format!("syd-tui: {}", self.cmd_label),
            Tab::Log => "syd-log".to_string(),
            Tab::Ipc => format!("syd-ipc: {}", self.ipc_addr),
            Tab::Api => "syd-api".to_string(),
            Tab::Sys => "syd-sys".to_string(),
            Tab::Msg => "syd-msg".to_string(),
            Tab::Help => "syd-help".to_string(),
        };
        self.set_title(&t);
    }

    fn mode_span(&self) -> Option<Span<'static>> {
        // Only show mode on tabs where switches make sense (Cmd, Ipc).
        match self.active_tab {
            Tab::Cmd | Tab::Ipc => match self.mode {
                Mode::Normal => Some(Span::styled(
                    "-- NORMAL --",
                    Style::default()
                        .fg(Color::LightBlue)
                        .add_modifier(Modifier::BOLD),
                )),
                Mode::Insert => Some(Span::styled(
                    "-- INSERT --",
                    Style::default()
                        .fg(Color::LightGreen)
                        .add_modifier(Modifier::BOLD),
                )),
            },
            _ => None,
        }
    }

    fn push_msg<S: AsRef<str>>(&mut self, msg: S) {
        self.buf_msg.push_str(&format!("{}\r\n", msg.as_ref()));
    }

    fn push_msg_ipc<S: AsRef<str> + Display>(&mut self, msg: S) {
        self.push_msg(format!("ipc: {msg}"));
    }

    fn push_msg_syd<S: AsRef<str> + Display>(&mut self, msg: S) {
        self.push_msg(format!("syd: {msg}"));
    }

    fn push_msg_tip<S: AsRef<str> + Display>(&mut self, msg: S) {
        self.push_msg(format!("tip: {msg}"));
    }

    fn push_msg_tui<S: AsRef<str> + Display>(&mut self, msg: S) {
        self.push_msg(format!("tui: {msg}"));
    }

    fn set_status<S: Into<String>>(&mut self, s: S) {
        self.status_line = Some(s.into());
        self.status_error = false;
    }

    fn set_error_status<S: Into<String>>(&mut self, s: S) {
        self.status_line = Some(s.into());
        self.status_error = true;
    }

    fn cycle_next(&mut self) {
        self.active_tab = self.active_tab.next();
        self.after_tab_switch();
    }

    fn cycle_prev(&mut self) {
        self.active_tab = self.active_tab.prev();
        self.after_tab_switch();
    }

    fn switch_to(&mut self, tab: Tab) {
        self.active_tab = tab;
        self.after_tab_switch();
    }

    fn after_tab_switch(&mut self) {
        // Non-interactive tabs force NORMAL mode.
        match self.active_tab {
            Tab::Cmd | Tab::Ipc => {}
            _ => self.mode = Mode::Normal,
        }
        match self.active_tab {
            Tab::Cmd => self.buf_cmd.scroll_to_bottom(),
            Tab::Log => self.buf_log.scroll_to_bottom(),
            Tab::Ipc => self.buf_ipc.scroll_to_bottom(),
            Tab::Api => self.buf_api.scroll_to_bottom(),
            Tab::Sys => self.buf_sys.scroll_to_bottom(),
            Tab::Help => {}
            Tab::Msg => self.buf_msg.scroll_to_bottom(),
        }
        self.update_osc_title_for_tab();
    }

    fn rainbow_spans(text: &str, bold: bool) -> Vec<Span<'static>> {
        let mut spans = Vec::with_capacity(text.chars().count());
        for (i, ch) in text.chars().enumerate() {
            let mut style = Style::default().fg(RAINBOW[i % RAINBOW.len()]);
            if bold {
                style = style.add_modifier(Modifier::BOLD);
            }
            spans.push(Span::styled(ch.to_string(), style));
        }
        spans
    }

    fn tab_title_line(&self, tab: Tab) -> Line<'static> {
        match tab {
            Tab::Help => {
                let mut spans = Self::rainbow_spans("syd-help: ", true);
                spans.extend(Self::rainbow_spans("Welcome to the machine!", true));
                Line::from(spans)
            }
            Tab::Cmd => {
                let mut spans = Self::rainbow_spans("syd-tui: ", true);
                let style = if self.cmd_dead {
                    Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
                } else {
                    Style::default()
                        .fg(Color::Green)
                        .add_modifier(Modifier::BOLD)
                };
                spans.push(Span::styled(self.cmd_label.clone(), style));
                Line::from(spans)
            }
            Tab::Log => Line::from(Self::rainbow_spans("syd-log", true)),
            Tab::Ipc => {
                let mut spans = Self::rainbow_spans("syd-ipc: ", true);
                spans.push(Span::styled(
                    self.ipc_addr.clone(),
                    Style::default().add_modifier(Modifier::BOLD),
                ));
                Line::from(spans)
            }
            Tab::Api => Line::from(Self::rainbow_spans("syd-api", true)),
            Tab::Sys => Line::from(Self::rainbow_spans("syd-sys", true)),
            Tab::Msg => Line::from(Self::rainbow_spans("syd-msg", true)),
        }
    }

    fn snapshot(&self) -> RenderSnapshot {
        let main_area_height = self
            .area
            .height
            .saturating_sub(2 /*borders*/ + 1 /*status*/);
        let inner_w = self.area.width.saturating_sub(2);
        let title_line = self.tab_title_line(self.active_tab);

        // number widths
        let num_width_log = if self.num_log {
            Some(num_digits(self.buf_log.lines_count()))
        } else {
            None
        };
        let num_width_api = if self.num_api {
            Some(num_digits(self.buf_api.lines_count()))
        } else {
            None
        };
        let num_width_sys = if self.num_sys {
            Some(num_digits(self.buf_sys.lines_count()))
        } else {
            None
        };
        let num_width_msg = if self.num_msg {
            Some(num_digits(self.buf_msg.lines_count()))
        } else {
            None
        };
        let num_width_ipc = if self.num_ipc {
            Some(num_digits(self.buf_ipc.lines_count()))
        } else {
            None
        };

        let (main_text, bottom_prompt, show_save_prompt, save_prompt_text, pct_opt) =
            match self.active_tab {
                Tab::Cmd => {
                    let t = self.buf_cmd.view_with_options(
                        main_area_height.saturating_sub(1),
                        inner_w,
                        self.hscroll_cmd,
                        None,
                        None,
                    );
                    let prompt = Some(bottom_prompt_line(&self.cmd_input));
                    (t, prompt, false, Text::default(), None)
                }
                Tab::Log => {
                    let pat = self
                        .search_log
                        .preview
                        .as_deref()
                        .or(self.search_log.pattern.as_deref());
                    let t = self.buf_log.view_with_options(
                        main_area_height,
                        inner_w,
                        self.hscroll_log,
                        pat,
                        num_width_log,
                    );
                    let pct = percentage_right(&self.buf_log, self.area.height.saturating_sub(3));
                    (
                        t,
                        None,
                        self.save_prompt_active,
                        build_save_prompt(self),
                        Some(pct),
                    )
                }
                Tab::Api => {
                    let pat = self
                        .search_api
                        .preview
                        .as_deref()
                        .or(self.search_api.pattern.as_deref());
                    let t = self.buf_api.view_with_options(
                        main_area_height,
                        inner_w,
                        self.hscroll_api,
                        pat,
                        num_width_api,
                    );
                    let pct = percentage_right(&self.buf_api, self.area.height.saturating_sub(3));
                    (t, None, false, Text::default(), Some(pct))
                }
                Tab::Sys => {
                    let pat = self
                        .search_sys
                        .preview
                        .as_deref()
                        .or(self.search_sys.pattern.as_deref());
                    let t = self.buf_sys.view_with_options(
                        main_area_height,
                        inner_w,
                        self.hscroll_sys,
                        pat,
                        num_width_sys,
                    );
                    let pct = percentage_right(&self.buf_sys, self.area.height.saturating_sub(3));
                    (t, None, false, Text::default(), Some(pct))
                }
                Tab::Ipc => {
                    let t = self.buf_ipc.view_with_options(
                        main_area_height.saturating_sub(1),
                        inner_w,
                        0,
                        None,
                        num_width_ipc,
                    );
                    let prompt = Some(bottom_prompt_line(&self.ipc_input));
                    (t, prompt, false, Text::default(), None)
                }
                Tab::Help => (self.help_text.clone(), None, false, Text::default(), None),
                Tab::Msg => {
                    let pat = self
                        .search_msg
                        .preview
                        .as_deref()
                        .or(self.search_msg.pattern.as_deref());
                    let t = self.buf_msg.view_with_options(
                        main_area_height,
                        inner_w,
                        self.hscroll_msg,
                        pat,
                        num_width_msg,
                    );
                    let pct = percentage_right(&self.buf_msg, self.area.height.saturating_sub(3));
                    (t, None, false, Text::default(), Some(pct))
                }
            };

        // Bottom status: Mode (when meaningful) + optional status text.
        let mut status_line = Line::default();
        if let Some(mode_span) = self.mode_span() {
            status_line.spans.push(mode_span);
            status_line.spans.push(Span::raw("  "));
        }
        if let Some(s) = &self.status_line {
            if self.status_error {
                status_line.spans.push(Span::styled(
                    s.clone(),
                    Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
                ));
            } else {
                status_line.spans.push(Span::raw(s.clone()));
            }
        }

        // Right status: pct + rainbow "Syd:" + pid (green/red)
        let mut right = Line::default();
        if let Some(pct) = pct_opt {
            right.spans.push(Span::styled(
                format!("{:>3}%  ", pct),
                Style::default().fg(Color::DarkGray),
            ));
        }
        for (i, ch) in "Syd:".chars().enumerate() {
            right.spans.push(Span::styled(
                ch.to_string(),
                Style::default().fg(RAINBOW[i % RAINBOW.len()]),
            ));
        }
        let pid_txt = if let Some(pid) = self.child_pid {
            pid.to_string()
        } else {
            "-".into()
        };
        let pid_style = if self.cmd_dead {
            Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
        } else {
            Style::default()
                .fg(Color::Green)
                .add_modifier(Modifier::BOLD)
        };
        right.spans.push(Span::styled(pid_txt, pid_style));

        let mut status_text = Text::default();
        status_text.lines.push(status_line);

        RenderSnapshot {
            title_line,
            main_text,
            status_text,
            right_status: right,
            bottom_prompt,
            show_save_prompt,
            save_prompt_text,
        }
    }

    fn draw(&mut self) -> io::Result<()> {
        let atab = self.active_tab;
        let snap = self.snapshot();

        let res = self.terminal.draw(|f| {
            let full = f.area();

            let outer = Layout::default()
                .direction(Direction::Vertical)
                .constraints([Constraint::Min(1), Constraint::Length(1)].as_ref())
                .split(full);

            // Main block with borders
            let block = Block::default()
                .title_alignment(Alignment::Left)
                .title(snap.title_line.clone())
                .borders(Borders::ALL);
            let inner = block.inner(outer[0]);
            f.render_widget(block, outer[0]);

            // If a tab with prompt, split inner:
            // Content + fixed single-line prompt.
            if (matches!(atab, Tab::Ipc | Tab::Cmd)) && snap.bottom_prompt.is_some() {
                let s = Layout::default()
                    .direction(Direction::Vertical)
                    .constraints([Constraint::Min(1), Constraint::Length(1)].as_ref())
                    .split(inner);
                let para_main = Paragraph::new(snap.main_text.clone()).wrap(Wrap { trim: false });
                f.render_widget(para_main, s[0]);
                #[expect(clippy::disallowed_methods)]
                let prompt_para = Paragraph::new(Text::from(snap.bottom_prompt.clone().unwrap()))
                    .wrap(Wrap { trim: false });
                f.render_widget(prompt_para, s[1]);
            } else {
                let para = Paragraph::new(snap.main_text.clone()).wrap(Wrap { trim: false });
                f.render_widget(para, inner);
            }

            // Status line: left + right aligned
            let status_chunks = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Min(1), Constraint::Length(18)].as_ref())
                .split(outer[1]);

            let status_para_left = Paragraph::new(snap.status_text.clone());
            f.render_widget(status_para_left, status_chunks[0]);

            let right_para = Paragraph::new(snap.right_status.clone()).alignment(Alignment::Right);
            f.render_widget(right_para, status_chunks[1]);

            if snap.show_save_prompt {
                let area = centered_rect(PROMPT_PERCENT_X, PROMPT_PERCENT_Y, full);
                let block = Block::default()
                    .title_alignment(Alignment::Center)
                    .title(Line::from(Span::styled(
                        "Save",
                        Style::default().add_modifier(Modifier::BOLD),
                    )))
                    .borders(Borders::ALL);
                f.render_widget(Clear, area);
                let para = Paragraph::new(snap.save_prompt_text.clone())
                    .block(block)
                    .wrap(Wrap { trim: false });
                f.render_widget(para, area);
            }
        });

        match res {
            Ok(cf) => {
                self.area = cf.area; // CompletedFrame exposes area
                Ok(())
            }
            Err(e) => Err(e),
        }
    }

    fn force_redraw(&mut self) {
        let _ = io::Write::write_all(self.terminal.backend_mut(), b"\x1b[2J\x1b[H");
        let _ = self.terminal.backend_mut().flush();
        let _ = self.terminal.clear();
        let _ = self.draw();
    }
}

fn bottom_prompt_line(text: &str) -> Line<'static> {
    let mut l = Line::default();
    l.spans.push(Span::styled(
        "; ",
        Style::default().add_modifier(Modifier::BOLD),
    ));
    l.spans.push(Span::raw(text.to_string()));
    l
}

fn build_save_prompt(app: &App) -> Text<'static> {
    let mut prompt = Text::default();
    prompt
        .lines
        .push(Line::from("Enter file path and press <Enter> to save."));
    prompt.lines.push(Line::from(""));
    prompt
        .lines
        .push(Line::from(format!("Path: {}", app.save_prompt_input)));
    prompt
}

//
// Layout helpers
//

fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
    let vert = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage((100 - percent_y) / 2),
                Constraint::Percentage(percent_y),
                Constraint::Percentage((100 - percent_y) / 2),
            ]
            .as_ref(),
        )
        .split(r);

    let horz = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(
            [
                Constraint::Percentage((100 - percent_x) / 2),
                Constraint::Percentage(percent_x),
                Constraint::Percentage((100 - percent_x) / 2),
            ]
            .as_ref(),
        )
        .split(vert[1]);

    horz[1]
}

//
// Files/PTY helpers
//

// Get window-size from the given FD.
fn winsize_get<Fd: AsFd>(fd: Fd) -> io::Result<Winsize> {
    let fd = fd.as_fd().as_raw_fd();
    let req = TIOCGWINSZ;
    let mut ws = Winsize {
        ws_row: 0,
        ws_col: 0,
        ws_xpixel: 0,
        ws_ypixel: 0,
    };

    // SAFETY: `fd` is a valid fd from `AsFd`; `TIOCGWINSZ` is a
    // valid ioctl request; `ws` is a valid, writable pointer.
    Errno::result(unsafe { syscall(SYS_ioctl, fd, req, &mut ws) }).map_err(errno2io)?;

    Ok(ws)
}

// Set window-size for the given FD.
fn set_winsize<Fd: AsFd>(fd: Fd, ws: &Winsize) -> io::Result<()> {
    let fd = fd.as_fd().as_raw_fd();
    let req = TIOCSWINSZ;

    // SAFETY: `fd` is a valid fd from `AsFd`; `TIOCSWINSZ` is a
    // valid ioctl request; `ws` is a valid, readable pointer.
    Errno::result(unsafe { syscall(SYS_ioctl, fd, req, ws) })
        .map(drop)
        .map_err(errno2io)
}

fn set_pty_winsize<Fd: AsFd>(master_fd: Fd, area: Rect) {
    let ws = Winsize {
        ws_row: area.height.saturating_sub(2),
        ws_col: area.width.saturating_sub(2),
        ws_xpixel: 0,
        ws_ypixel: 0,
    };
    let _ = set_winsize(master_fd, &ws);
}

fn make_controlling_tty_on_stdin() -> io::Result<()> {
    // setsid makes us session leader;
    // TIOCSCTTY on stdin assigns as controlling TTY.
    setsid().map_err(errno2io)?;

    // SAFETY: stdin is a TTY slave we set up for the child.
    Errno::result(unsafe { syscall(SYS_ioctl, STDIN_FILENO, TIOCSCTTY, 0) })
        .map(drop)
        .map_err(errno2io)
}

async fn save_to_file_async(path: &str, content: String, overwrite: bool) -> io::Result<()> {
    let mut flags = OFlag::O_CREAT | OFlag::O_NOFOLLOW | OFlag::O_NOCTTY;
    if overwrite {
        // :w! overwrites.
        flags.insert(OFlag::O_TRUNC);
    } else {
        // :w doesn't clobber.
        flags.insert(OFlag::O_EXCL);
    }
    let how = OpenHow::new()
        .flags(flags)
        .mode(OpenMode::from_bits_truncate(0o600))
        .resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
    #[expect(clippy::disallowed_methods)]
    let mut file = openat2(AT_FDCWD, path, how)
        .map(std::fs::File::from)
        .map(tokio::fs::File::from_std)
        .map_err(errno2io)?;
    let bytes = content.into_bytes();
    let mut off = 0usize;
    while off < bytes.len() {
        let end = min(off + SAVE_WRITE_CHUNK, bytes.len());
        AsyncWriteExt::write_all(&mut file, &bytes[off..end]).await?;
        off = end;
    }
    AsyncWriteExt::flush(&mut file).await?;
    Ok(())
}

fn set_cloexec<Fd: AsFd>(fd: Fd, on: bool) -> io::Result<()> {
    let flags = fcntl(&fd, FcntlArg::F_GETFD).map_err(errno2io)?;
    let mut f = FdFlag::from_bits_truncate(flags);
    if on {
        f.insert(FdFlag::FD_CLOEXEC);
    } else {
        f.remove(FdFlag::FD_CLOEXEC);
    }
    fcntl(fd, FcntlArg::F_SETFD(f)).map(drop).map_err(errno2io)
}

//
// Pid FD helpers
//

// Safe wrapper for pidfd_open(2).
//
// This function requires Linux 5.3+.
// Only valid flag is PIDFD_THREAD, equivalent to O_EXCL.
fn pidfd_open(pid: Pid, flags: u32) -> Result<OwnedFd, Errno> {
    // SAFETY: libc does not have a pidfd_open(2) wrapper yet.
    #[expect(clippy::cast_possible_truncation)]
    Errno::result(unsafe { syscall(SYS_pidfd_open, pid.as_raw(), flags) }).map(|fd| {
        // SAFETY: pidfd_open(2) returned success, fd is valid.
        unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
    })
}

// Safe wrapper for pidfd_send_signal(2).
//
// This function requires Linux 5.1+.
fn pidfd_send_signal<Fd: AsFd>(pid_fd: Fd, sig: Signal) -> Result<(), Errno> {
    // SAFETY: libc does not have a wrapper for pidfd_send_signal yet.
    Errno::result(unsafe { syscall(SYS_pidfd_send_signal, pid_fd.as_fd().as_raw_fd(), sig, 0, 0) })
        .map(drop)
}

//
// Keys
//

fn is_ctrl_l(bytes: &[u8]) -> bool {
    bytes == [0x0c]
}

fn is_ctrl_g(bytes: &[u8]) -> bool {
    bytes == [0x07]
}

fn is_ctrl_z(bytes: &[u8]) -> bool {
    bytes == [0x1a]
}

//
// ANSI scrubber + newline normalizer
//

fn strip_ansi(input: &[u8]) -> Vec<u8> {
    // Very small, stateful scrubber for ESC, CSI, OSC, ST.
    let mut out = Vec::with_capacity(input.len());
    let mut i = 0;
    while i < input.len() {
        let b = input[i];
        if b == 0x1b {
            // ESC
            i += 1;
            if i >= input.len() {
                break;
            }
            let b1 = input[i];
            match b1 {
                b'[' => {
                    // CSI: ESC [ ... final 0x40..0x7E
                    i += 1;
                    while i < input.len() {
                        let c = input[i];
                        if (0x40..=0x7e).contains(&c) {
                            i += 1;
                            break;
                        } else {
                            i += 1;
                        }
                    }
                }
                b']' => {
                    // OSC: ESC ] ... BEL or ESC \
                    i += 1;
                    while i < input.len() {
                        if input[i] == 0x07 {
                            i += 1;
                            break;
                        } // BEL
                        if input[i] == 0x1b && i + 1 < input.len() && input[i + 1] == b'\\' {
                            i += 2;
                            break; // ST
                        }
                        i += 1;
                    }
                }
                b'(' | b')' | b'*' | b'+' | b',' | b'-' | b'.' => {
                    // Charset selectors: one following byte
                    i += 2;
                }
                b'c' => {
                    i += 1;
                } // RIS
                _ => {
                    i += 1;
                } // Swallow simple ESC X
            }
            continue;
        } else if b == 0x9b {
            // C1 CSI: 0x9b ... final 0x40..0x7E
            i += 1;
            while i < input.len() {
                let c = input[i];
                if (0x40..=0x7e).contains(&c) {
                    i += 1;
                    break;
                } else {
                    i += 1;
                }
            }
            continue;
        } else if b == 0x9d {
            // C1 OSC: 0x9d ... BEL
            i += 1;
            while i < input.len() {
                if input[i] == 0x07 {
                    i += 1;
                    break;
                }
                i += 1;
            }
            continue;
        }
        out.push(b);
        i += 1;
    }
    out
}

fn clean_bytes_for_cmd(input: &[u8]) -> Vec<u8> {
    let no_ansi = strip_ansi(input);
    let mut out = Vec::with_capacity(no_ansi.len());
    let mut i = 0usize;
    while i < no_ansi.len() {
        match no_ansi[i] {
            b'\r' => {
                if i + 1 < no_ansi.len() && no_ansi[i + 1] == b'\n' {
                    out.push(b'\n');
                    i += 2;
                } else {
                    i += 1;
                }
            }
            0x08 => {
                if !out.is_empty() {
                    out.pop();
                }
                i += 1;
            }
            0x07 => {
                i += 1;
            }
            b => {
                out.push(b);
                i += 1;
            }
        }
    }
    out
}

fn clean_bytes_for_plain(input: &[u8]) -> Vec<u8> {
    let no_ansi = strip_ansi(input);
    let mut out = Vec::with_capacity(no_ansi.len());
    let mut i = 0usize;
    while i < no_ansi.len() {
        match no_ansi[i] {
            b'\r' => {
                if i + 1 < no_ansi.len() && no_ansi[i + 1] == b'\n' {
                    out.push(b'\n');
                    i += 2;
                } else {
                    i += 1;
                }
            }
            0x08 => {
                if !out.is_empty() {
                    out.pop();
                }
                i += 1;
            }
            0x07 => {
                i += 1;
            }
            b => {
                out.push(b);
                i += 1;
            }
        }
    }
    out
}

//
// Help (-h)
//

fn print_help() {
    let mut out = io::stdout();

    let header_1 = PKG_HEADER_1.as_str();
    let header_2 = PKG_HEADER_2.as_str();
    let header_3 = PKG_HEADER_3.as_str();
    let header_4 = PKG_HEADER_4.as_str();

    let _ = writeln!(
        out,
        "\
{header_1}
{header_2}
{header_3}
{header_4}

Usage: {PKG_NAME} [-h] [<args>...]
Windows:
  0 help  1 cmd  2 log  3 ipc  4 api  5 sys  6 msg
Keys:
  H/L  prev/next tab     0..6  switch tab
  g/G  top/bottom (content & ipc)
  i/Esc  insert/normal (cmd/ipc)
  / ? n N   search (wrap) in content tabs (log/api/sys/msg)
  Up/Down PgUp/PgDn Home/End  scroll; ←/→ horizontal
  :w[!] FILE   write buffer;  :set (nu[mber]|nonu[mber])
  :!CMD / :sh  run CMD;       :e[dit] [FILE]
  :redr[aw][!] force redraw   :ve[rsion]  syd -V
  :kill, :stop, :cont[inue]   terminate, stop or resume Syd
  :re[connect][!]             reconnect ipc
  Ctrl-L force redraw; Ctrl-G cancel input; Ctrl-Z suspend tui
Environment:
  SYD_QUIET_TTY unset -> sets SYD_FORCE_TTY=1 for Syd.
  SYD_LOG (defaults to \"info\" if not set).
  SYD_IPC, SYD_LOG_FD are set automatically.",
    );
}

//
// Main entry point.
//

// Synchronous entry point that builds and drives the Tokio runtime.
fn main() -> io::Result<()> {
    #[expect(clippy::disallowed_methods)]
    let rt = Builder::new_multi_thread()
        .enable_io()
        .enable_time()
        .thread_name("syd_tui")
        .thread_stack_size(TUI_STACK_SIZE)
        .build()
        .expect("build tokio runtime");
    rt.block_on(async_main())
}

async fn async_main() -> io::Result<()> {
    // CLI args (after program name)
    let args: Vec<OsString> = env::args_os().skip(1).collect();

    if args.first().map(|a| a == "-h").unwrap_or(false) {
        print_help();
        return Ok(());
    }

    // If STDIN is not a TTY, exec Syd directly.
    if !isatty(io::stdin()).unwrap_or(true) {
        let mut c = std::process::Command::new("syd");
        c.args(&args);

        // exec only returns on error.
        return Err(c.exec());
    }

    let args_str = args
        .iter()
        .map(|s| shell_escape(s).into_owned())
        .collect::<Vec<String>>()
        .join(" ");
    let cmd_label = if args_str.is_empty() {
        "syd".to_string()
    } else {
        format!("syd {args_str}")
    };

    // Create Syd log pipe.
    let (log_r, log_w) = pipe2(OFlag::O_CLOEXEC)?;

    // Child must inherit write end, clear CLOEXEC.
    set_cloexec(&log_w, false)?;

    // Create PTY for syd (cmd).
    let OpenptyResult {
        master: pty_master_cmd,
        slave: pty_slave_cmd,
        ..
    } = openpty(None, None)?;

    // Keep a PTY for ipc area sizing; not used for IO.
    let OpenptyResult {
        master: pty_master_ipc,
        slave: _pty_slave_ipc,
        ..
    } = openpty(None, None)?;

    // Abstract socket name for syd: starts with '@'.
    let syd_ipc_env = make_abstract_socket_name();

    // Respect SYD_QUIET_TTY; if not set, force TTY for Syd.
    let force_tty = env::var_os("SYD_QUIET_TTY").is_none();

    // Prepare Syd command.
    let mut cmd = tokio::process::Command::new("syd");
    cmd.args(&args);

    // TERM: inherit or set to xterm-256color.
    if let Some(term) = env::var_os("TERM") {
        cmd.env("TERM", term);
    } else {
        cmd.env("TERM", "xterm-256color");
    }

    // Ensure SYD_LOG defaults to 'info' if not set.
    if env::var_os("SYD_LOG").is_none() {
        cmd.env("SYD_LOG", "info");
    }

    let log_fd_num = log_w.as_raw_fd();
    cmd.env("SYD_LOG_FD", log_fd_num.to_string());
    cmd.env("SYD_IPC", &syd_ipc_env);
    if force_tty {
        cmd.env("SYD_FORCE_TTY", "1");
    }

    // Hook child's stdio to PTY slave,
    // and make controlling TTY.
    let dup_stdin = dup(&pty_slave_cmd)?;
    let dup_stdout = dup(&pty_slave_cmd)?;
    let dup_stderr = dup(&pty_slave_cmd)?;

    cmd.stdin(dup_stdin);
    cmd.stdout(dup_stdout);
    cmd.stderr(dup_stderr);

    // SAFETY: Child side becomes session leader, and
    // gets controlling TTY on standard input.
    unsafe {
        cmd.pre_exec(make_controlling_tty_on_stdin);
    }

    // Spawn Syd.
    let mut child = cmd.spawn()?;

    // Capture pid BEFORE moving child into the wait task.
    let child_pid = child.id().map(|id| Pid::from_raw(id as i32));
    // Open a PIDFd to mitigate recycles during signal send.
    let child_pfd = if let Some(child_pid) = child_pid {
        Some(pidfd_open(child_pid, 0)?)
    } else {
        None
    };

    // Write end is not needed after spawn.
    drop(log_w);

    // Save original termios BEFORE switching to raw/alternate.
    let saved_termios = tcgetattr(io::stdin()).map_err(errno2io)?;

    // Prepare terminal: Raw mode + Alternate screen.
    let raw_stdout: RawTerminal<io::Stdout> = io::stdout().into_raw_mode()?;
    let alt_screen: AlternateScreen<RawTerminal<io::Stdout>> =
        raw_stdout.into_alternate_screen()?;
    let backend = TermionBackend::new(alt_screen);
    let terminal = Terminal::new(backend)?;

    // Duplicate master fds for async read & write sides.
    let cmd_r_fd = dup(&pty_master_cmd)?;
    let cmd_w_fd = dup(&pty_master_cmd)?;

    // Channels (UI).
    let (ui_tx, ui_rx) = mpsc::channel::<UiInput>(CHAN_CAPACITY);

    // Initialize App
    let mut app = App::new(
        terminal,
        "Welcome to the machine!",
        cmd_label.clone(),
        syd_ipc_env.clone(),
        pty_master_cmd,
        Some(pty_master_ipc),
        saved_termios,
        child_pid,
        child_pfd,
        ui_tx.clone(),
    )?;
    app.update_osc_title_for_tab();

    // Initial winsize.
    if let Ok(sz) = app.terminal.size() {
        let area = Rect::new(0, 0, sz.width, sz.height);
        app.area = area;
        set_pty_winsize(&app.pty_master_cmd, area);
        if let Some(ref fd) = app.pty_master_ipc {
            set_pty_winsize(fd, area);
        }
    }

    // Hide cursor.
    let _ = app.terminal.hide_cursor();

    // Startup messages in syd-msg.
    app.push_msg_tui("TUI started.");
    app.push_msg_tui(format!("Syd CMD: {cmd_label}"));
    app.push_msg_tui(format!("Syd IPC: {syd_ipc_env}"));
    app.push_msg_tui(format!("Syd LOG: {log_fd_num}"));

    // Data channels.
    let (cmd_rx_tx, cmd_rx) = mpsc::channel::<Vec<u8>>(CHAN_CAPACITY);
    let (ipc_rx_tx, ipc_rx) = mpsc::channel::<Vec<u8>>(CHAN_CAPACITY);
    let (log_rx_tx, log_rx) = mpsc::channel::<Vec<u8>>(CHAN_CAPACITY);

    // Stdin reader: Store handle so :! can abort it to avoid SIGTTIN.
    spawn_stdin_reader(&mut app);

    // SIGWINCH: Resize window.
    {
        let ui_tx = ui_tx.clone();
        tokio::spawn(async move {
            if let Ok(mut sig) =
                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::window_change())
            {
                while sig.recv().await.is_some() {
                    if let Ok(ws) = winsize_get(io::stdout()) {
                        let w = ws.ws_col;
                        let h = ws.ws_row;
                        let _ = ui_tx.send(UiInput::Resize(w, h)).await;
                    } else {
                        let _ = ui_tx.send(UiInput::Resize(0, 0)).await;
                    }
                }
            }
        });
    }

    // SIGTERM/SIGINT/SIGHUP: Quit gracefully.
    {
        let ui_tx_clone = ui_tx.clone();
        tokio::spawn(async move {
            if let Ok(mut sig) =
                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            {
                if sig.recv().await.is_some() {
                    let _ = ui_tx_clone.send(UiInput::Quit).await;
                }
            }
        });
        let ui_tx_clone = ui_tx.clone();
        tokio::spawn(async move {
            if let Ok(mut sig) =
                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
            {
                if sig.recv().await.is_some() {
                    let _ = ui_tx_clone.send(UiInput::Quit).await;
                }
            }
        });
        let ui_tx_clone = ui_tx.clone();
        tokio::spawn(async move {
            if let Ok(mut sig) =
                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())
            {
                if sig.recv().await.is_some() {
                    let _ = ui_tx_clone.send(UiInput::Quit).await;
                }
            }
        });
    }

    // Child exit monitor.
    {
        let ui_tx = ui_tx.clone();
        tokio::spawn(async move {
            if let Ok(status) = child.wait().await {
                let code: Option<i32> = status.code();
                let sig: Option<i32> = status.signal();
                let _ = ui_tx.send(UiInput::ChildExit(code, sig)).await;
            }
        });
    }

    // Readers.
    tokio::spawn({
        let tx = cmd_rx_tx.clone();
        let mut file = tokio::fs::File::from_std(cmd_r_fd.into());
        async move {
            let mut buf = vec![0u8; IO_READ_CHUNK];
            loop {
                match AsyncReadExt::read(&mut file, &mut buf).await {
                    Ok(0) => break,
                    Ok(n) => {
                        let clean = clean_bytes_for_cmd(&buf[..n]);
                        let _ = tx.send(clean).await;
                    }
                    Err(_) => break,
                }
            }
        }
    });

    tokio::spawn({
        let tx = log_rx_tx.clone();
        let mut file = tokio::fs::File::from_std(log_r.into());
        async move {
            let mut buf = vec![0u8; IO_READ_CHUNK];
            loop {
                match AsyncReadExt::read(&mut file, &mut buf).await {
                    Ok(0) => break,
                    Ok(n) => {
                        let clean = clean_bytes_for_plain(&buf[..n]);
                        let _ = tx.send(clean).await;
                    }
                    Err(_) => break,
                }
            }
        }
    });

    //
    // Native IPC client
    //
    let mut ipc_started = false;
    let mut ipc_task: Option<JoinHandle<()>> = None;
    let mut ipc_tx_opt: Option<mpsc::Sender<Vec<u8>>> = None;

    // Writer for syd PTY (cmd tab).
    let mut cmd_writer = tokio::fs::File::from_std(cmd_w_fd.into());

    // Ex ':' buffer
    let mut colon_cmd: String = String::new();
    let mut collecting_colon = false;

    // Startup: syd -V / --api / --check
    {
        // syd -V
        let ui_tx_clone = ui_tx.clone();
        tokio::spawn(async move {
            if let Ok(out) = tokio::process::Command::new("syd")
                .arg("-V")
                .stderr(Stdio::inherit())
                .output()
                .await
            {
                let s = String::from_utf8_lossy(&out.stdout);
                let first = s.lines().next().unwrap_or("").to_string();
                let _ = ui_tx_clone
                    .send(UiInput::ApiData(
                        format!("__VERSION__:{}\n", first).into_bytes(),
                    ))
                    .await;
            }
        });

        // syd --api -> Api tab.
        let ui_tx_clone = ui_tx.clone();
        tokio::spawn(async move {
            #[expect(clippy::disallowed_methods)]
            let mut p = tokio::process::Command::new("syd")
                .arg("--api")
                .stdin(Stdio::null())
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()
                .expect("spawn syd --api");
            if let Some(mut out) = p.stdout.take() {
                let mut buf = vec![0u8; IO_READ_CHUNK];
                loop {
                    match AsyncReadExt::read(&mut out, &mut buf).await {
                        Ok(0) => break,
                        Ok(n) => {
                            let _ = ui_tx_clone
                                .send(UiInput::ApiData(clean_bytes_for_plain(&buf[..n])))
                                .await;
                        }
                        Err(_) => break,
                    }
                }
            }
            let _ = p.wait().await;
        });

        // syd --check -> Sys tab
        let ui_tx_clone = ui_tx.clone();
        tokio::spawn(async move {
            #[expect(clippy::disallowed_methods)]
            let mut p = tokio::process::Command::new("syd")
                .arg("--check")
                .stdin(Stdio::null())
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()
                .expect("spawn syd --check");
            if let Some(mut out) = p.stdout.take() {
                let mut buf = vec![0u8; IO_READ_CHUNK];
                loop {
                    match AsyncReadExt::read(&mut out, &mut buf).await {
                        Ok(0) => break,
                        Ok(n) => {
                            let _ = ui_tx_clone
                                .send(UiInput::SysData(clean_bytes_for_plain(&buf[..n])))
                                .await;
                        }
                        Err(_) => break,
                    }
                }
            }
            let _ = p.wait().await;
        });
    }

    // Fan-in: Forward all sources into one channel.
    let (ev_tx, mut ev_rx) = mpsc::channel::<Event>(CHAN_CAPACITY);

    // cmd_rx -> ev_tx
    tokio::spawn({
        let mut rx = cmd_rx;
        let tx = ev_tx.clone();
        async move {
            while let Some(bytes) = rx.recv().await {
                let _ = tx.send(Event::Cmd(bytes)).await;
            }
        }
    });

    // log_rx -> ev_tx
    tokio::spawn({
        let mut rx = log_rx;
        let tx = ev_tx.clone();
        async move {
            while let Some(bytes) = rx.recv().await {
                let _ = tx.send(Event::Log(bytes)).await;
            }
        }
    });

    // ipc_rx -> ev_tx
    tokio::spawn({
        let mut rx = ipc_rx;
        let tx = ev_tx.clone();
        async move {
            while let Some(bytes) = rx.recv().await {
                let _ = tx.send(Event::Ipc(bytes)).await;
            }
        }
    });

    // ui_rx -> ev_tx
    tokio::spawn({
        let mut rx = ui_rx;
        let tx = ev_tx.clone();
        async move {
            while let Some(evt) = rx.recv().await {
                let _ = tx.send(Event::Ui(evt)).await;
            }
        }
    });

    // ticker -> ev_tx
    tokio::spawn({
        let tx = ev_tx.clone();
        async move {
            let mut t = interval(Duration::from_millis(TICK_MS));
            t.set_missed_tick_behavior(MissedTickBehavior::Delay);
            loop {
                t.tick().await;
                let _ = tx.send(Event::Tick).await;
            }
        }
    });

    // Main loop
    while let Some(ev) = ev_rx.recv().await {
        match ev {
            Event::Cmd(bytes) => {
                app.buf_cmd.push_bytes(&bytes);
            }
            Event::Log(bytes) => {
                app.buf_log.push_bytes(&bytes);
                feed_msg_from_log_chunk(&mut app, &bytes);
            }
            Event::Ipc(bytes) => {
                app.buf_ipc.push_bytes(&bytes);
            }
            Event::Ui(evt) => {
                match evt {
                    UiInput::Quit => break,
                    UiInput::Suspend if !app.suspend => {
                        suspend_tui(&mut app).await;
                        raise(Signal::SIGTSTP)?;
                        restore_tui(&mut app).await;
                    }
                    UiInput::Suspend => {}
                    UiInput::Resize(w, h) => {
                        if w > 0 && h > 0 {
                            let area = Rect::new(0, 0, w, h);
                            app.area = area;
                            set_pty_winsize(&app.pty_master_cmd, area);
                            if let Some(ref fd) = app.pty_master_ipc {
                                set_pty_winsize(fd, area);
                            }
                        } else if let Ok(sz) = app.terminal.size() {
                            let area = Rect::new(0, 0, sz.width, sz.height);
                            app.area = area;
                            set_pty_winsize(&app.pty_master_cmd, area);
                            if let Some(ref fd) = app.pty_master_ipc {
                                set_pty_winsize(fd, area);
                            }
                        }
                    }
                    UiInput::ForceRedraw => {
                        app.force_redraw();
                    }
                    UiInput::Reconnect(force) => {
                        if app.cmd_dead {
                            app.set_error_status(
                                "ERROR: Syd is not running; refusing to reconnect.",
                            );
                            app.push_msg_ipc("Syd is not running; refusing to reconnect.");
                            continue;
                        }
                        if !force && ipc_started && !app.ipc_dead {
                            app.set_error_status(
                                "ERROR: IPC already connected; use :reconnect! to force.",
                            );
                            continue;
                        }
                        app.ipc_dead = true;
                        ipc_started = false;
                        ipc_tx_opt = None;
                        if let Some(h) = ipc_task.take() {
                            h.abort();
                            let _ = h.await;
                        }
                        match start_ipc_client(&syd_ipc_env, ui_tx.clone(), ipc_rx_tx.clone()).await
                        {
                            Ok((tx, task)) => {
                                app.ipc_dead = false;
                                ipc_started = true;
                                ipc_tx_opt = Some(tx);
                                ipc_task = Some(task);
                                app.buf_ipc.push_str("IPC reconnected.");
                            }
                            Err(e) => {
                                let msg = format!("IPC reconnect error: {e}!");
                                app.buf_ipc.push_str(&format!("\r\n{msg}\r\n"));
                                app.set_error_status(msg);
                            }
                        }
                    }
                    UiInput::ChildExit(code, sig) => {
                        app.cmd_dead = true;
                        let tail = match (code, sig) {
                            (Some(c), None) => format!("code {c}"),
                            (None, Some(s)) => format!("signal {s}"),
                            (Some(c), Some(s)) => format!("code {c} (signal {s})"),
                            (None, None) => "unknown".into(),
                        };
                        let msg = format!("+++ Syd exited with {tail} +++",);
                        let log = format!("\r\n{msg}\r\n");
                        app.buf_cmd.push_str(&log);
                        app.buf_ipc.push_str(&log);
                        app.push_msg(msg);
                    }
                    UiInput::IpcExit(code, sig) => {
                        app.ipc_dead = true;
                        let tail = match (code, sig) {
                            (Some(c), None) => format!("code {c}"),
                            (None, Some(s)) => format!("signal {s}"),
                            (Some(c), Some(s)) => format!("code {c} (signal {s})"),
                            (None, None) => "unknown".into(),
                        };
                        let msg = format!("Connection closed ({tail})");
                        app.buf_ipc.push_str(&format!("\r\n{msg}\r\n"));
                        app.push_msg_ipc(msg);
                        app.ipc_dead = true;
                        ipc_started = false;
                        ipc_tx_opt = None;
                        if let Some(h) = ipc_task.take() {
                            h.abort();
                            let _ = h.await;
                        }
                    }
                    UiInput::ApiData(data) => {
                        if let Ok(s) = std::str::from_utf8(&data) {
                            if let Some(rest) = s.strip_prefix("__VERSION__:") {
                                let v = rest.trim_end_matches('\n').to_string();
                                app.syd_version = Some(v.clone());
                                app.push_msg_tui(format!("Syd version is {v}."));
                                continue;
                            }
                        }
                        app.buf_api.push_bytes(&data);
                    }
                    UiInput::SysData(data) => {
                        app.buf_sys.push_bytes(&data);
                    }
                    UiInput::Bytes(data) => {
                        // Ctrl-L => Force redraw (any mode).
                        if is_ctrl_l(&data) {
                            let _ = ui_tx.send(UiInput::ForceRedraw).await;
                            continue;
                        }

                        // Ctrl-Z => Suspend (any mode).
                        if is_ctrl_z(&data) {
                            let _ = ui_tx.send(UiInput::Suspend).await;
                            continue;
                        }

                        // Ctrl-G => Cancel (any mode).
                        if is_ctrl_g(&data) {
                            colon_cmd.clear();
                            app.ex_hist_pos = None;
                            app.search_log.preview = None;
                            app.search_api.preview = None;
                            app.search_sys.preview = None;
                            app.search_msg.preview = None;
                            app.save_prompt_active = false;
                            app.save_prompt_input.clear();
                            if matches!(app.active_tab, Tab::Ipc) {
                                app.ipc_input.clear();
                                app.ipc_hist_pos = None;
                            }
                            if matches!(app.active_tab, Tab::Cmd) {
                                app.cmd_input.clear();
                                app.cmd_hist_pos = None;
                            }
                            app.set_status("");
                            continue;
                        }

                        // Single-digit tab switch (Normal mode).
                        // Disabled while collecting ':' Ex command.
                        if app.mode == Mode::Normal
                            && !collecting_colon
                            && data.len() == 1
                            && data[0].is_ascii_digit()
                        {
                            if let Some(tab) = Tab::from_index(data[0] - b'0') {
                                app.switch_to(tab);
                            }
                            continue;
                        }
                        match app.mode {
                            Mode::Insert => {
                                match app.active_tab {
                                    // Cmd Insert mode:
                                    // Buffered input; send only on Enter; pass through Ctrl-C/Q/etc.
                                    Tab::Cmd => {
                                        // ESC -> Switch to Normal mode.
                                        if data == [0x1b] {
                                            app.mode = Mode::Normal;
                                            continue;
                                        }

                                        // Handle ANSI sequences for history navigation.
                                        let s = String::from_utf8_lossy(&data);
                                        if s.contains("[A") {
                                            // Up -> older
                                            if !app.cmd_hist.is_empty() {
                                                let next = match app.cmd_hist_pos {
                                                    None => {
                                                        Some(app.cmd_hist.len().saturating_sub(1))
                                                    }
                                                    Some(0) => Some(0),
                                                    Some(p) => Some(p.saturating_sub(1)),
                                                };
                                                if let Some(p) = next {
                                                    app.cmd_hist_pos = Some(p);
                                                    app.cmd_input = app.cmd_hist[p].clone();
                                                }
                                            }
                                            continue;
                                        } else if s.contains("[B") {
                                            // Down -> newer
                                            if let Some(p) = app.cmd_hist_pos {
                                                let p2 = p.saturating_add(1);
                                                if p2 < app.cmd_hist.len() {
                                                    app.cmd_hist_pos = Some(p2);
                                                    app.cmd_input = app.cmd_hist[p2].clone();
                                                } else {
                                                    app.cmd_hist_pos = None;
                                                    app.cmd_input.clear();
                                                }
                                            }
                                            continue;
                                        } else if s.contains("[5~")
                                            || s.contains("[H")
                                            || s.contains("[1~")
                                        {
                                            // PageUp/Home -> oldest
                                            if !app.cmd_hist.is_empty() {
                                                app.cmd_hist_pos = Some(0);
                                                app.cmd_input = app.cmd_hist[0].clone();
                                            }
                                            continue;
                                        } else if s.contains("[6~")
                                            || s.contains("[F")
                                            || s.contains("[4~")
                                        {
                                            // PageDown/End -> newest (empty if beyond)
                                            app.cmd_hist_pos = None;
                                            app.cmd_input.clear();
                                            continue;
                                        }

                                        // Per-byte processing.
                                        for &b in &data {
                                            match b {
                                                b'\r' | b'\n' => {
                                                    let line = app.cmd_input.trim_end().to_string();
                                                    // Send the full line ONLY now
                                                    let mut tosend = line.clone().into_bytes();
                                                    tosend.push(b'\n');
                                                    if !app.cmd_dead {
                                                        let _ = AsyncWriteExt::write_all(
                                                            &mut cmd_writer,
                                                            &tosend,
                                                        )
                                                        .await;
                                                    }
                                                    if !line.is_empty()
                                                        && app
                                                            .cmd_hist
                                                            .last()
                                                            .map(|x| x != &line)
                                                            .unwrap_or(true)
                                                    {
                                                        app.cmd_hist.push(line);
                                                    }
                                                    app.cmd_input.clear();
                                                    app.cmd_hist_pos = None;
                                                }
                                                0x7f => {
                                                    let _ = app.cmd_input.pop();
                                                }
                                                0x17 => {
                                                    // Ctrl-W delete word.
                                                    while app
                                                        .cmd_input
                                                        .ends_with(char::is_whitespace)
                                                    {
                                                        app.cmd_input.pop();
                                                    }
                                                    while app
                                                        .cmd_input
                                                        .chars()
                                                        .last()
                                                        .map(|c| !c.is_whitespace())
                                                        .unwrap_or(false)
                                                    {
                                                        app.cmd_input.pop();
                                                    }
                                                }
                                                0x03 | 0x11 | 0x04 | 0x1a => {
                                                    // Ctrl-C / Ctrl-Q / Ctrl-D / Ctrl-Z:
                                                    // Pass through immediately.
                                                    if !app.cmd_dead {
                                                        let _ = AsyncWriteExt::write_all(
                                                            &mut cmd_writer,
                                                            &[b],
                                                        )
                                                        .await;
                                                    }
                                                }
                                                0x1b => {
                                                    // Ignore standalone ESC in insert buffer
                                                    // to avoid CSI leakage.
                                                }
                                                _ => {
                                                    if b.is_ascii_graphic()
                                                        || b == b' '
                                                        || b == b'\t'
                                                    {
                                                        app.cmd_input.push(b as char);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    // Ipc Insert Mode:
                                    // Line-edit + extended history keys.
                                    Tab::Ipc => {
                                        if data == [0x1b] {
                                            app.mode = Mode::Normal;
                                            continue;
                                        }
                                        if app.cmd_dead {
                                            continue;
                                        }
                                        if !ipc_started {
                                            match start_ipc_client(
                                                &syd_ipc_env,
                                                ui_tx.clone(),
                                                ipc_rx_tx.clone(),
                                            )
                                            .await
                                            {
                                                Ok((tx, task)) => {
                                                    app.ipc_dead = false;
                                                    ipc_started = true;
                                                    ipc_tx_opt = Some(tx);
                                                    ipc_task = Some(task);
                                                }
                                                Err(e) => {
                                                    let emsg = format!("IPC connect error: {e}!");
                                                    app.buf_ipc.push_str(&format!("{emsg}\r\n"));
                                                    app.push_msg_ipc(emsg);
                                                }
                                            }
                                        }
                                        let s = String::from_utf8_lossy(&data);
                                        if s.contains("[A") {
                                            if !app.ipc_hist.is_empty() {
                                                let next = match app.ipc_hist_pos {
                                                    None => {
                                                        Some(app.ipc_hist.len().saturating_sub(1))
                                                    }
                                                    Some(0) => Some(0),
                                                    Some(p) => Some(p.saturating_sub(1)),
                                                };
                                                if let Some(p) = next {
                                                    app.ipc_hist_pos = Some(p);
                                                    app.ipc_input = app.ipc_hist[p].clone();
                                                }
                                            }
                                        } else if s.contains("[B") {
                                            if let Some(p) = app.ipc_hist_pos {
                                                let p2 = p.saturating_add(1);
                                                if p2 < app.ipc_hist.len() {
                                                    app.ipc_hist_pos = Some(p2);
                                                    app.ipc_input = app.ipc_hist[p2].clone();
                                                } else {
                                                    app.ipc_hist_pos = None;
                                                    app.ipc_input.clear();
                                                }
                                            }
                                        } else if s.contains("[5~")
                                            || s.contains("[H")
                                            || s.contains("[1~")
                                        {
                                            if !app.ipc_hist.is_empty() {
                                                app.ipc_hist_pos = Some(0);
                                                app.ipc_input = app.ipc_hist[0].clone();
                                            }
                                        } else if s.contains("[6~")
                                            || s.contains("[F")
                                            || s.contains("[4~")
                                        {
                                            app.ipc_hist_pos = None;
                                            app.ipc_input.clear();
                                        } else {
                                            for &b in &data {
                                                match b {
                                                    b'\r' | b'\n' => {
                                                        let line =
                                                            app.ipc_input.trim_end().to_string();
                                                        if !line.is_empty() {
                                                            if let Some(tx) = &ipc_tx_opt {
                                                                let mut tosend =
                                                                    line.clone().into_bytes();
                                                                tosend.push(b'\n');
                                                                let _ = tx.send(tosend).await;
                                                            }
                                                            if app
                                                                .ipc_hist
                                                                .last()
                                                                .map(|x| x != &line)
                                                                .unwrap_or(true)
                                                            {
                                                                app.ipc_hist.push(line.clone());
                                                            }
                                                        }
                                                        app.ipc_input.clear();
                                                        app.ipc_hist_pos = None;
                                                    }
                                                    0x7f => {
                                                        let _ = app.ipc_input.pop();
                                                    }
                                                    0x17 => {
                                                        // Ctrl-W delete word.
                                                        while app
                                                            .ipc_input
                                                            .ends_with(char::is_whitespace)
                                                        {
                                                            app.ipc_input.pop();
                                                        }
                                                        while app
                                                            .ipc_input
                                                            .chars()
                                                            .last()
                                                            .map(|c| !c.is_whitespace())
                                                            .unwrap_or(false)
                                                        {
                                                            app.ipc_input.pop();
                                                        }
                                                    }
                                                    _ => {
                                                        if b.is_ascii_graphic()
                                                            || b == b' '
                                                            || b == b'\t'
                                                        {
                                                            app.ipc_input.push(b as char);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    _ => {}
                                }
                            }
                            Mode::Normal => {
                                // ':' Ex entry.
                                if data == [b':'] && !collecting_colon {
                                    collecting_colon = true;
                                    colon_cmd.clear();
                                    app.ex_hist_pos = None;
                                    app.set_status(":");
                                    continue;
                                }

                                // If in ex command collection:
                                if collecting_colon {
                                    // ENTER / ESC / BS / Up-Down history
                                    let mut done = false;
                                    let s = String::from_utf8_lossy(&data);
                                    if s.contains("[A") {
                                        // Up
                                        if !app.ex_hist.is_empty() {
                                            let next = match app.ex_hist_pos {
                                                None => Some(app.ex_hist.len().saturating_sub(1)),
                                                Some(0) => Some(0),
                                                Some(p) => Some(p.saturating_sub(1)),
                                            };
                                            if let Some(p) = next {
                                                app.ex_hist_pos = Some(p);
                                                colon_cmd = app.ex_hist[p].clone();
                                            }
                                        }
                                    } else if s.contains("[B") {
                                        // Down
                                        if let Some(p) = app.ex_hist_pos {
                                            let p2 = p.saturating_add(1);
                                            if p2 < app.ex_hist.len() {
                                                app.ex_hist_pos = Some(p2);
                                                colon_cmd = app.ex_hist[p2].clone();
                                            } else {
                                                app.ex_hist_pos = None;
                                                colon_cmd.clear();
                                            }
                                        }
                                    } else {
                                        for &b in &data {
                                            match b {
                                                b'\r' | b'\n' => {
                                                    done = true;
                                                    break;
                                                }
                                                0x1b => {
                                                    collecting_colon = false;
                                                    colon_cmd.clear();
                                                    app.ex_hist_pos = None;
                                                    app.set_status("");
                                                }
                                                0x7f => {
                                                    let _ = colon_cmd.pop();
                                                }
                                                _ => {
                                                    if b.is_ascii_graphic() || b == b' ' {
                                                        colon_cmd.push(b as char);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    app.set_status(format!(":{colon_cmd}"));
                                    if done {
                                        let cmdline = colon_cmd.clone();
                                        if !cmdline.is_empty()
                                            && app
                                                .ex_hist
                                                .last()
                                                .map(|x| x != &cmdline)
                                                .unwrap_or(true)
                                        {
                                            app.ex_hist.push(cmdline.clone());
                                        }
                                        colon_cmd.clear();
                                        app.ex_hist_pos = None;
                                        collecting_colon = false;
                                        let quit_action =
                                            ex_execute(&mut app, &cmdline, ui_tx.clone()).await;
                                        if quit_action {
                                            break;
                                        }
                                    }
                                    continue;
                                }

                                // 'g' -> Move to top (content & ipc).
                                if data == [b'g'] {
                                    match app.active_tab {
                                        Tab::Log => app.buf_log.scroll_to_top(),
                                        Tab::Api => app.buf_api.scroll_to_top(),
                                        Tab::Sys => app.buf_sys.scroll_to_top(),
                                        Tab::Msg => app.buf_msg.scroll_to_top(),
                                        Tab::Ipc => {
                                            let total = app.buf_ipc.lines_count() as u16;
                                            app.buf_ipc.scroll = total.saturating_sub(1);
                                        }
                                        _ => {}
                                    }
                                    continue;
                                }
                                // 'G' -> Move to bottom.
                                if data == [b'G'] {
                                    match app.active_tab {
                                        Tab::Log => app.buf_log.scroll_to_bottom(),
                                        Tab::Api => app.buf_api.scroll_to_bottom(),
                                        Tab::Sys => app.buf_sys.scroll_to_bottom(),
                                        Tab::Msg => app.buf_msg.scroll_to_bottom(),
                                        Tab::Ipc => app.buf_ipc.scroll_to_bottom(),
                                        _ => {}
                                    }
                                    continue;
                                }
                                // H/L prev/next tab.
                                if data == [b'H'] {
                                    app.cycle_prev();
                                    continue;
                                }
                                if data == [b'L'] {
                                    app.cycle_next();
                                    continue;
                                }

                                // Search begin on content tabs.
                                if app.active_tab.is_content() && (data == [b'/'] || data == [b'?'])
                                {
                                    let forward = data == [b'/'];
                                    let tab = app.active_tab;
                                    {
                                        let st = get_search_state_mut(&mut app, tab);
                                        st.last_forward = forward;
                                        st.preview = Some(String::new());
                                    }
                                    app.set_status(if forward { "/" } else { "?" }.to_string());
                                    continue;
                                }

                                // Live search preview (already started).
                                {
                                    let tab = app.active_tab;
                                    let (preview, last_forward) = {
                                        let st = get_search_state_mut(&mut app, tab);
                                        (st.preview.clone(), st.last_forward)
                                    };
                                    if let Some(mut pv) = preview {
                                        let mut done = false;
                                        for &b in &data {
                                            match b {
                                                b'\r' | b'\n' => {
                                                    done = true;
                                                    break;
                                                }
                                                0x1b => {
                                                    pv.clear();
                                                    done = true;
                                                }
                                                0x7f => {
                                                    let _ = pv.pop();
                                                }
                                                _ => {
                                                    if b.is_ascii_graphic() || b == b' ' {
                                                        pv.push(b as char);
                                                    }
                                                }
                                            }
                                        }
                                        if done {
                                            let pattern = pv.clone();
                                            {
                                                let st = get_search_state_mut(&mut app, tab);
                                                st.preview = None;
                                                if !pattern.is_empty() {
                                                    st.pattern = Some(pattern.clone());
                                                    st.last_match = None;
                                                }
                                            }
                                            if !pattern.is_empty() {
                                                let (buf, area_h) = get_active_buf_and_height(&app);
                                                let (_start, end) =
                                                    (0usize, buf.lines_count().saturating_sub(1));
                                                let (_top, bottom) = buf.visible_bounds(area_h);
                                                let hit = if last_forward {
                                                    buf.find_next_line(&pattern, Some(bottom))
                                                        .or_else(|| {
                                                            buf.find_next_line(&pattern, Some(end))
                                                        })
                                                } else {
                                                    buf.find_prev_line(
                                                        &pattern,
                                                        Some(bottom.saturating_sub(1)),
                                                    )
                                                    .or_else(|| {
                                                        buf.find_prev_line(&pattern, Some(end))
                                                    })
                                                };
                                                if let Some(line) = hit {
                                                    {
                                                        let st =
                                                            get_search_state_mut(&mut app, tab);
                                                        st.last_match = Some(line);
                                                    }
                                                    scroll_line_into_view(
                                                        get_active_buf_mut(&mut app),
                                                        line,
                                                        area_h,
                                                    );
                                                    app.set_status(format!("Found: {pattern}"));
                                                } else {
                                                    app.set_status(format!("Not found: {pattern}"));
                                                }
                                            } else {
                                                app.set_status("");
                                            }
                                        } else {
                                            {
                                                let st = get_search_state_mut(&mut app, tab);
                                                st.preview = Some(pv.clone());
                                            }
                                            app.set_status(if last_forward {
                                                format!("/{pv}")
                                            } else {
                                                format!("?{pv}")
                                            });
                                        }
                                        continue;
                                    }
                                }

                                // n / N in content tabs (with wrap).
                                if app.active_tab.is_content() && (data == [b'n'] || data == [b'N'])
                                {
                                    let tab = app.active_tab;
                                    let (pattern_opt, last_forward, last_match) = {
                                        let st = get_search_state_mut(&mut app, tab);
                                        (st.pattern.clone(), st.last_forward, st.last_match)
                                    };
                                    if let Some(pat) = pattern_opt {
                                        let forward = if data == [b'n'] {
                                            last_forward
                                        } else {
                                            !last_forward
                                        };
                                        let (buf, area_h) = get_active_buf_and_height(&app);
                                        let anchor = last_match.or_else(|| {
                                            let (_t, btm) = buf.visible_bounds(area_h);
                                            Some(btm)
                                        });
                                        let next = if forward {
                                            buf.find_next_line(&pat, anchor)
                                                .or_else(|| buf.find_next_line(&pat, None))
                                        } else {
                                            let before = anchor.map(|i| i.saturating_sub(1));
                                            buf.find_prev_line(&pat, before)
                                                .or_else(|| buf.find_prev_line(&pat, None))
                                        };
                                        if let Some(line) = next {
                                            {
                                                let st = get_search_state_mut(&mut app, tab);
                                                st.last_match = Some(line);
                                            }
                                            scroll_line_into_view(
                                                get_active_buf_mut(&mut app),
                                                line,
                                                area_h,
                                            );
                                            app.set_status(format!("Found: {pat}"));
                                        } else {
                                            app.set_status(format!("Not found: {pat}"));
                                        }
                                    }
                                    continue;
                                }

                                // i -> INSERT (only for cmd/ipc).
                                if data == [b'i'] && matches!(app.active_tab, Tab::Cmd | Tab::Ipc) {
                                    app.mode = Mode::Insert;
                                    continue;
                                }

                                // Esc keeps NORMAL mode.
                                if data == [0x1b] {
                                    app.mode = Mode::Normal;
                                    continue;
                                }

                                // Scrolling on all tabs.
                                if handle_scrolling_keys(&mut app, &data) {
                                    continue;
                                }
                            }
                        }
                    }
                }
            }
            Event::Tick => {
                let _ = app.draw();
            }
        }
    }

    // Terminate child and restore TTY.
    if let Some(pfd) = app.child_pfd {
        let _ = pidfd_send_signal(&pfd, Signal::SIGKILL);
    }

    // Stop stdin reader if alive.
    if let Some(h) = app.stdin_task.take() {
        h.abort();
        let _ = h.await;
    }

    // Restore terminal cleanly:
    // Drop the TUI, this exits raw mode & leaves the alt screen via RAII.
    let mut term = app.terminal;
    let _ = term.show_cursor();
    let _ = term.clear();
    drop(term);

    // Exit immediately to ensure stdin reader does not block.
    std::process::exit(0);
}

// Spawn/respawn stdin reader task (stored on App)
fn spawn_stdin_reader(app: &mut App) {
    let ui_tx = app.ui_tx.clone();
    let handle = tokio::spawn(async move {
        let mut stdin = tokio::io::stdin();
        let mut buf = vec![0u8; IO_READ_CHUNK];
        loop {
            match AsyncReadExt::read(&mut stdin, &mut buf).await {
                Ok(0) => {
                    // EOF on stdin -> request quit and end the task.
                    let _ = ui_tx.send(UiInput::Quit).await;
                    break;
                }
                Ok(n) => {
                    let _ = ui_tx.send(UiInput::Bytes(buf[..n].to_vec())).await;
                }
                Err(_) => break,
            }
        }
    });
    app.stdin_task = Some(handle);
}

//
// Native UNIX IPC client
//

// Convert nix::Errno to io::Error.
fn errno2io(errno: Errno) -> io::Error {
    io::Error::from_raw_os_error(errno as i32)
}

// Fully async connect to an abstract UNIX socket using a non-blocking socket.
async fn connect_abstract_unix_async(name: &str) -> io::Result<UnixStream> {
    // Create non-blocking UNIX stream socket.
    let fd = socket(
        AddressFamily::Unix,
        SockType::Stream,
        SockFlag::SOCK_CLOEXEC | SockFlag::SOCK_NONBLOCK,
        None,
    )
    .map_err(errno2io)?;

    // Build abstract address (@name => leading NUL + name bytes).
    let addr = UnixAddr::new_abstract(name.as_bytes()).map_err(errno2io)?;

    // Initiate non-blocking connect.
    match connect(fd.as_raw_fd(), &addr) {
        Ok(()) => {}
        Err(Errno::EAGAIN | Errno::EINPROGRESS) => {}
        Err(e) => return Err(errno2io(e)),
    }

    // Wait until the socket is writable => connect complete or failed.
    let stream = UnixStream::from_std(fd.into())?;
    stream.writable().await?;

    // Check SO_ERROR to determine connect status.
    let err = getsockopt(&stream, SocketError).map_err(errno2io)?;
    if err != 0 {
        return Err(io::Error::from_raw_os_error(err));
    }

    Ok(stream)
}

async fn start_ipc_client(
    syd_ipc_env: &str,
    ui_tx: mpsc::Sender<UiInput>,
    ipc_rx_tx: mpsc::Sender<Vec<u8>>,
) -> io::Result<(mpsc::Sender<Vec<u8>>, JoinHandle<()>)> {
    // Syd expects '@name' for abstract UNIX socket.
    let abstract_name = syd_ipc_env
        .strip_prefix('@')
        .unwrap_or(syd_ipc_env)
        .to_string();

    // Show where we're going (readable line).
    let _ = ipc_rx_tx
        .send(format!("Connecting to IPC socket @{abstract_name} (interactive)...\n").into_bytes())
        .await;

    // Outgoing line channel (used by IPC Insert mode).
    let (tx, mut rx) = mpsc::channel::<Vec<u8>>(CHAN_CAPACITY);

    // Non-blocking async connect (single try)
    let stream = match connect_abstract_unix_async(&abstract_name).await {
        Ok(s) => s,
        Err(e) => {
            let _ = ipc_rx_tx
                .send(format!("IPC connect error: {e}\n").into_bytes())
                .await;
            return Err(e);
        }
    };

    let (mut r, mut w) = stream.into_split();
    let _ = ipc_rx_tx
        .send(b"IPC connected; switching to interactive mode...\n".to_vec())
        .await;

    // Send "prompt i" and probe version immediately.
    if let Err(e) = AsyncWriteExt::write_all(&mut w, b"prompt i\nversion\n").await {
        let _ = ipc_rx_tx
            .send(b"Failed write prompt; not retrying. Use :re[connect]\n".to_vec())
            .await;
        return Err(e);
    }

    // Reader + writer task (single session, no auto-retry)
    let ui_tx = ui_tx.clone();
    let ipc_rx_tx = ipc_rx_tx.clone();
    let task = tokio::spawn(async move {
        // Reader task.
        let ui_tx2 = ui_tx.clone();
        tokio::spawn(async move {
            let mut buf = vec![0u8; IO_READ_CHUNK];
            loop {
                match AsyncReadExt::read(&mut r, &mut buf).await {
                    Ok(0) => {
                        let _ = ui_tx2.send(UiInput::IpcExit(Some(0), None)).await;
                        break;
                    }
                    Ok(n) => {
                        let clean = clean_bytes_for_plain(&buf[..n]);
                        let _ = ipc_rx_tx.send(clean).await;
                    }
                    Err(_) => {
                        let _ = ui_tx2.send(UiInput::IpcExit(None, None)).await;
                        break;
                    }
                }
            }
        });

        // Writer loop: Forward user lines to IPC.
        loop {
            match rx.recv().await {
                Some(msg) => {
                    if AsyncWriteExt::write_all(&mut w, &msg).await.is_err() {
                        // Can't write: notify and exit.
                        let _ = ui_tx.send(UiInput::IpcExit(None, None)).await;
                        break;
                    }
                }
                None => return, // Channel closed -> Exit task.
            }
        }
    });

    Ok((tx, task))
}

//
// Scrolling / Search helpers
//

fn handle_scrolling_keys(app: &mut App, data: &[u8]) -> bool {
    let s = String::from_utf8_lossy(data);
    let lines = app.area.height.saturating_sub(2).max(1);

    // Horizontal first (left/right).
    if s.contains("[C") {
        match app.active_tab {
            Tab::Log => app.hscroll_log = app.hscroll_log.saturating_add(1),
            Tab::Api => app.hscroll_api = app.hscroll_api.saturating_add(1),
            Tab::Sys => app.hscroll_sys = app.hscroll_sys.saturating_add(1),
            Tab::Msg => app.hscroll_msg = app.hscroll_msg.saturating_add(1),
            Tab::Cmd => app.hscroll_cmd = app.hscroll_cmd.saturating_add(1),
            _ => {}
        }
        return true;
    } else if s.contains("[D") {
        match app.active_tab {
            Tab::Log => app.hscroll_log = app.hscroll_log.saturating_sub(1),
            Tab::Api => app.hscroll_api = app.hscroll_api.saturating_sub(1),
            Tab::Sys => app.hscroll_sys = app.hscroll_sys.saturating_sub(1),
            Tab::Msg => app.hscroll_msg = app.hscroll_msg.saturating_sub(1),
            Tab::Cmd => app.hscroll_cmd = app.hscroll_cmd.saturating_sub(1),
            _ => {}
        }
        return true;
    }

    // Vertical + Paging.
    if s.contains("[A") {
        match app.active_tab {
            Tab::Log => app.buf_log.scroll_up(1),
            Tab::Api => app.buf_api.scroll_up(1),
            Tab::Sys => app.buf_sys.scroll_up(1),
            Tab::Msg => app.buf_msg.scroll_up(1),
            Tab::Ipc => app.buf_ipc.scroll_up(1),
            Tab::Help => app.buf_msg.scroll_up(1),
            Tab::Cmd => app.buf_cmd.scroll_up(1),
        }
        true
    } else if s.contains("[B") {
        match app.active_tab {
            Tab::Log => app.buf_log.scroll_down(1),
            Tab::Api => app.buf_api.scroll_down(1),
            Tab::Sys => app.buf_sys.scroll_down(1),
            Tab::Msg => app.buf_msg.scroll_down(1),
            Tab::Ipc => app.buf_ipc.scroll_down(1),
            Tab::Help => app.buf_msg.scroll_down(1),
            Tab::Cmd => app.buf_cmd.scroll_down(1),
        }
        true
    } else if s.contains("[5~") {
        match app.active_tab {
            Tab::Log => app.buf_log.scroll_up(lines),
            Tab::Api => app.buf_api.scroll_up(lines),
            Tab::Sys => app.buf_sys.scroll_up(lines),
            Tab::Msg => app.buf_msg.scroll_up(lines),
            Tab::Ipc => app.buf_ipc.scroll_up(lines),
            Tab::Help => app.buf_msg.scroll_up(lines),
            Tab::Cmd => app.buf_cmd.scroll_up(lines),
        }
        true
    } else if s.contains("[6~") {
        match app.active_tab {
            Tab::Log => app.buf_log.scroll_down(lines),
            Tab::Api => app.buf_api.scroll_down(lines),
            Tab::Sys => app.buf_sys.scroll_down(lines),
            Tab::Msg => app.buf_msg.scroll_down(lines),
            Tab::Ipc => app.buf_ipc.scroll_down(lines),
            Tab::Help => app.buf_msg.scroll_down(lines),
            Tab::Cmd => app.buf_cmd.scroll_down(lines),
        }
        true
    } else if s.contains("[H") || s.contains("[1~") {
        match app.active_tab {
            Tab::Log => app.buf_log.scroll_to_top(),
            Tab::Api => app.buf_api.scroll_to_top(),
            Tab::Sys => app.buf_sys.scroll_to_top(),
            Tab::Msg => app.buf_msg.scroll_to_top(),
            Tab::Ipc => {
                let total = app.buf_ipc.lines_count() as u16;
                app.buf_ipc.scroll = total.saturating_sub(1);
            }
            Tab::Help => {
                let total = app.buf_msg.lines_count() as u16;
                app.buf_msg.scroll = total.saturating_sub(1);
            }
            Tab::Cmd => app.buf_cmd.scroll_to_top(),
        }
        true
    } else if s.contains("[F") || s.contains("[4~") {
        match app.active_tab {
            Tab::Log => app.buf_log.scroll_to_bottom(),
            Tab::Api => app.buf_api.scroll_to_bottom(),
            Tab::Sys => app.buf_sys.scroll_to_bottom(),
            Tab::Msg => app.buf_msg.scroll_to_bottom(),
            Tab::Ipc => app.buf_ipc.scroll_to_bottom(),
            Tab::Help => app.buf_msg.scroll_to_bottom(),
            Tab::Cmd => app.buf_cmd.scroll_to_bottom(),
        }
        true
    } else {
        false
    }
}

fn scroll_line_into_view(buf: &mut TextBuffer, line: usize, _height: u16) {
    let total = buf.lines_count();
    if total == 0 {
        return;
    }
    let bottom_line = line as i64;
    let total_i = total as i64;
    // Compute scroll such that bottom shows `bottom_line`.
    let scroll_from_bottom = (total_i - 1 - bottom_line).max(0) as u16;
    // But ensure we have room for height.
    let max_scroll = total.saturating_sub(1);
    buf.scroll = min(scroll_from_bottom, max_scroll as u16);
}

fn percentage_right(buf: &TextBuffer, area_h: u16) -> u16 {
    let (_top, bottom) = buf.visible_bounds(area_h);
    let total = buf.lines_count().max(1);
    ((bottom + 1) * 100 / total) as u16
}

fn get_active_buf_and_height(app: &App) -> (&TextBuffer, u16) {
    let h = app.area.height.saturating_sub(3);
    match app.active_tab {
        Tab::Log => (&app.buf_log, h),
        Tab::Api => (&app.buf_api, h),
        Tab::Sys => (&app.buf_sys, h),
        Tab::Msg => (&app.buf_msg, h),
        _ => (&app.buf_log, h),
    }
}
fn get_active_buf_mut(app: &mut App) -> &mut TextBuffer {
    match app.active_tab {
        Tab::Log => &mut app.buf_log,
        Tab::Api => &mut app.buf_api,
        Tab::Sys => &mut app.buf_sys,
        Tab::Msg => &mut app.buf_msg,
        _ => &mut app.buf_log,
    }
}
fn get_search_state_mut(app: &mut App, tab: Tab) -> &mut SearchState {
    match tab {
        Tab::Log => &mut app.search_log,
        Tab::Api => &mut app.search_api,
        Tab::Sys => &mut app.search_sys,
        Tab::Msg => &mut app.search_msg,
        _ => &mut app.search_log,
    }
}

//
// Ex-commands (NORMAL :...)
//

async fn ex_execute(app: &mut App, cmdline: &str, ui_tx: mpsc::Sender<UiInput>) -> bool {
    let mut parts = cmdline.split_whitespace();
    let Some(cmd) = parts.next() else {
        return false;
    };
    match cmd {
        "q" | "qa" | "quit" | "wq" | "x" | "xa" => {
            if app.cmd_dead {
                return true;
            }
            app.set_error_status("ERROR: Syd is still running, add ! to terminate and quit.");
            false
        }
        "q!" | "qa!" | "quit!" | "wq!" | "x!" | "xa!" => {
            if let Some(ref pfd) = app.child_pfd {
                let _ = pidfd_send_signal(pfd, Signal::SIGKILL);
            }
            true
        }
        "cont" | "continue" => {
            if let Some(ref pfd) = app.child_pfd {
                let _ = pidfd_send_signal(pfd, Signal::SIGCONT);
            }
            false
        }
        "kill" => {
            if let Some(ref pfd) = app.child_pfd {
                let _ = pidfd_send_signal(pfd, Signal::SIGKILL);
            }
            false
        }
        "stop" => {
            if let Some(ref pfd) = app.child_pfd {
                let _ = pidfd_send_signal(pfd, Signal::SIGSTOP);
            }
            false
        }
        "n" | "next" => {
            app.cycle_next();
            false
        }
        "prev" => {
            app.cycle_prev();
            false
        }
        "tab" => {
            if let Some(arg) = parts.next() {
                if let Ok(n) = arg.parse::<u8>() {
                    if let Some(tab) = Tab::from_index(n) {
                        app.switch_to(tab);
                    }
                }
            }
            false
        }
        "redr" | "redraw" | "redraw!" => {
            app.force_redraw();
            false
        }
        "ve" | "version" => {
            if let Some(v) = &app.syd_version {
                let first = v.lines().next().unwrap_or(v);
                app.set_status(format!("Syd version: {first}"));
            } else {
                app.set_status("Syd version: (unknown)");
            }
            false
        }
        "w" | "write" | "w!" | "write!" => {
            let overwrite = cmd.ends_with('!');
            if !app.active_tab.is_content() {
                app.set_error_status("ERROR: Nothing to write (not a content tab)!");
                return false;
            }
            match parts.next() {
                None => {
                    app.set_error_status("ERROR: No file name!");
                }
                Some(p) => {
                    let content = get_active_buf_mut(app).to_owned_string();
                    let bytes_len = content.len();
                    match save_to_file_async(p, content, overwrite).await {
                        Ok(()) => app.set_status(format!("\"{p}\" {bytes_len} bytes written.")),
                        Err(e) => {
                            app.set_error_status(format!("ERROR: Failed to write: {e}!"));
                        }
                    }
                }
            }
            false
        }
        "se" | "set" => {
            if let Some(arg) = parts.next() {
                let on = arg == "nu" || arg == "number";
                let off = arg == "nonu" || arg == "nonumber";
                if on || off {
                    let v = on;
                    match app.active_tab {
                        Tab::Log => app.num_log = v,
                        Tab::Api => app.num_api = v,
                        Tab::Sys => app.num_sys = v,
                        Tab::Msg => app.num_msg = v,
                        Tab::Ipc => app.num_ipc = v,
                        _ => {}
                    }
                }
            }
            false
        }
        "re" | "recon" | "reconn" | "reconnect" => {
            let _ = ui_tx.send(UiInput::Reconnect(false)).await;
            false
        }
        "re!" | "recon!" | "reconn!" | "reconnect!" => {
            let _ = ui_tx.send(UiInput::Reconnect(true)).await;
            false
        }
        "rc" => return run_external(app, "rc", ui_tx).await,
        "sh" => return run_external(app, "sh", ui_tx).await,
        s if s.starts_with('!') => {
            let cmd = s[1..].trim();
            let mut run_rest = parts.collect::<Vec<_>>();
            if !cmd.is_empty() {
                run_rest.insert(0, cmd);
            }
            let run_rest = run_rest.join(" ");
            if !run_rest.is_empty() {
                return run_external(app, &run_rest, ui_tx).await;
            }
            false
        }
        "e" | "edit" => {
            let file = parts.next().map(|s| s.to_string());
            return run_editor(app, file).await;
        }
        _ => {
            app.set_error_status(format!("Unknown command: :{cmdline}"));
            false
        }
    }
}

//
// Termios helpers for suspend/resume
//

fn set_tty_cooked(saved: &Termios) {
    let _ = tcsetattr(io::stdin(), SetArg::TCSANOW, saved);
    let _ = tcsetattr(io::stdout(), SetArg::TCSANOW, saved);
}

fn set_tty_raw_from_saved(saved: &Termios) {
    let mut raw = saved.clone();
    cfmakeraw(&mut raw);
    let _ = tcsetattr(io::stdin(), SetArg::TCSANOW, &raw);
    let _ = tcsetattr(io::stdout(), SetArg::TCSANOW, &raw);
}

// Suspend TUI, stop stdin-reader to avoid SIGTTIN, run cmd on real TTY, resume.
async fn run_external(app: &mut App, cmdline: &str, _ui_tx: mpsc::Sender<UiInput>) -> bool {
    // Suspend TUI.
    suspend_tui(app).await;

    // Run synchronously.
    let status = std::process::Command::new("/bin/sh")
        .arg("-c")
        .arg(cmdline)
        .status();

    // Restore TUI.
    restore_tui(app).await;

    match status {
        Ok(st) => {
            let msg = if let Some(c) = st.code() {
                format!(":! {cmdline} exited with code {c}.")
            } else if let Some(sig) = st.signal() {
                format!(":! {cmdline} killed by signal {sig}.")
            } else {
                format!(":! {cmdline} exited.")
            };
            app.push_msg_tui(msg);
        }
        Err(e) => {
            app.push_msg_tui(format!(":! {cmdline} failed: {e}"));
        }
    }
    false
}

// Run $EDITOR [file] with full TTY.
async fn run_editor(app: &mut App, file: Option<String>) -> bool {
    // Suspend TUI.
    suspend_tui(app).await;

    // Run synchronously.
    let editor = env::var_os("EDITOR")
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| OsString::from("vi"));
    let mut cmd = std::process::Command::new(&editor);
    if let Some(f) = &file {
        cmd.arg(f);
    }
    let status = cmd.status();

    // Restore TUI.
    restore_tui(app).await;

    match status {
        Ok(st) => {
            let msg = if let Some(c) = st.code() {
                if let Some(f) = file {
                    format!(":edit {f} exited with code {c}.")
                } else {
                    format!(":edit exited with code {c}.")
                }
            } else if let Some(sig) = st.signal() {
                if let Some(f) = file {
                    format!(":edit {f} killed by signal {sig}.")
                } else {
                    format!(":edit killed by signal {sig}.")
                }
            } else if let Some(f) = file {
                format!(":edit {f} exited.")
            } else {
                ":edit exited.".to_string()
            };
            app.push_msg_tui(msg);
        }
        Err(e) => {
            let msg = if let Some(f) = file {
                format!(":edit {f} failed: {e}")
            } else {
                format!(":edit failed: {e}")
            };
            app.push_msg_tui(msg);
        }
    }

    false
}

// Restore TUI.
async fn restore_tui(app: &mut App) {
    if !app.suspend {
        // TUI not suspended.
        return;
    }

    // Re-enter alt-screen, hide cursor and restore RAW.
    let _ = io::Write::write_all(app.terminal.backend_mut(), b"\x1b[?1049h\x1b[?25l");
    let _ = app.terminal.backend_mut().flush();

    // Switch terminal back to RAW so keystrokes are delivered immediately.
    set_tty_raw_from_saved(&app.saved_termios);

    // Re-apply winsize because external may have resized.
    if let Ok(sz) = app.terminal.size() {
        let area = ratatui::layout::Rect::new(0, 0, sz.width, sz.height);
        app.area = area;
        set_pty_winsize(&app.pty_master_cmd, area);
        if let Some(ref fd) = app.pty_master_ipc {
            set_pty_winsize(fd, area);
        }
    }

    // Redraw, refresh title and respawn stdin reader.
    app.force_redraw();
    app.update_osc_title_for_tab();
    spawn_stdin_reader(app);

    // Restore done, set suspend off.
    app.suspend = false;
}

// Suspend TUI.
async fn suspend_tui(app: &mut App) {
    if app.suspend {
        // TUI already suspended.
        return;
    }

    // Stop stdin reader so we don't read while backgrounded.
    if let Some(h) = app.stdin_task.take() {
        h.abort();
        let _ = h.await;
    }

    // Leave alt-screen and show cursor.
    let _ = io::Write::write_all(app.terminal.backend_mut(), b"\x1b[?1049l\x1b[?25h\x1b[0m");
    let _ = app.terminal.backend_mut().flush();

    // Restore cooked while external program runs.
    set_tty_cooked(&app.saved_termios);

    // Make SIGCONT handler aware of the ^Z.
    app.suspend = true;
}

//
// Utilities
//

fn num_digits(n: usize) -> usize {
    let mut x = n.max(1);
    let mut d = 0;
    while x > 0 {
        d += 1;
        x /= 10;
    }
    d
}

fn shell_escape(s: &OsString) -> Cow<'_, str> {
    let raw = s.to_string_lossy();
    if raw.is_empty() {
        Cow::Borrowed("''")
    } else if raw
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || "-_./:@".contains(c))
    {
        Cow::Owned(raw.into_owned())
    } else {
        let mut out = String::new();
        out.push('\'');
        for ch in raw.chars() {
            if ch == '\'' {
                out.push_str("'\\''");
            } else {
                out.push(ch);
            }
        }
        out.push('\'');
        Cow::Owned(out)
    }
}

// Extract `"msg":"..."` and `"tip":"..."` from log chunks.
fn feed_msg_from_log_chunk(app: &mut App, chunk: &[u8]) {
    app.log_accum.push_str(&String::from_utf8_lossy(chunk));
    while let Some(pos) = app.log_accum.find('\n') {
        let mut line = app.log_accum.drain(..=pos).collect::<String>();
        if line.ends_with('\n') {
            line.pop();
            if line.ends_with('\r') {
                line.pop();
            }
        }
        if let Some(msg) = extract_json_string_field(&line, "msg") {
            app.push_msg_syd(msg);
        }
        if let Some(tip) = extract_json_string_field(&line, "tip") {
            app.push_msg_tip(tip);
        }
    }
}

fn extract_json_string_field(line: &str, key: &str) -> Option<String> {
    let needle = format!("\"{key}\"");
    let mut i = 0usize;
    while let Some(p) = line[i..].find(&needle) {
        let idx = i + p + needle.len();
        let rest = &line[idx..];
        let mut j = 0usize;
        while j < rest.len() && rest.as_bytes()[j].is_ascii_whitespace() {
            j += 1;
        }
        if j < rest.len() && rest.as_bytes()[j] == b':' {
            j += 1;
            while j < rest.len() && rest.as_bytes()[j].is_ascii_whitespace() {
                j += 1;
            }
            if j < rest.len() && rest.as_bytes()[j] == b'"' {
                j += 1;
                let bytes = rest.as_bytes();
                let mut val = String::new();
                while j < rest.len() {
                    let b = bytes[j];
                    if b == b'\\' && j + 1 < rest.len() {
                        let nb = bytes[j + 1];
                        match nb {
                            b'"' => val.push('"'),
                            b'\\' => val.push('\\'),
                            b'n' => val.push('\n'),
                            b'r' => val.push('\r'),
                            b't' => val.push('\t'),
                            _ => {}
                        }
                        j += 2;
                    } else if b == b'"' {
                        break;
                    } else {
                        val.push(b as char);
                        j += 1;
                    }
                }
                return Some(val);
            }
        }
        i = idx;
    }
    None
}

fn make_abstract_socket_name() -> String {
    let mut bytes = [0u8; RAND_HEX_LEN / 2];
    #[expect(clippy::disallowed_methods)]
    rng::fillrandom(&mut bytes).expect("getrandom");
    let hex = HEXLOWER.encode(&bytes);
    format!("@{PKG_NAME}-{hex}")
}
