sac-cli 0.1.0

Terminal-based AI coding agent — fork of NAC with extended backend support and context management
Documentation
use super::*;

pub(super) fn enable_keyboard_enhancements(terminal: &mut UiTerminal) -> bool {
    let supports = supports_keyboard_enhancement().unwrap_or(false);
    if !supports {
        return false;
    }

    crossterm::execute!(
        terminal.backend_mut(),
        PushKeyboardEnhancementFlags(
            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
                | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
                | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
                | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
        )
    )
    .is_ok()
}

pub(super) fn enable_bracketed_paste(terminal: &mut UiTerminal) -> bool {
    crossterm::execute!(terminal.backend_mut(), EnableBracketedPaste).is_ok()
}

pub(super) fn enable_mouse_capture(terminal: &mut UiTerminal) -> bool {
    crossterm::execute!(terminal.backend_mut(), EnableMouseCapture).is_ok()
}

pub(super) fn spawn_input_thread(
    running: Arc<AtomicBool>,
    input_tx: mpsc::UnboundedSender<CrosstermEvent>,
) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        while running.load(AtomicOrdering::SeqCst) {
            match event::poll(Duration::from_millis(100)) {
                Ok(true) => match event::read() {
                    Ok(event) => {
                        if input_tx.send(event).is_err() {
                            break;
                        }
                    }
                    Err(_) => break,
                },
                Ok(false) => {}
                Err(_) => break,
            }
        }
    })
}

pub(super) async fn persist_session_snapshot(
    snapshot: &mut SessionSnapshot,
    agent: &Agent,
    last_response_duration_ms: Option<u64>,
    previous_response_duration_ms: Option<u64>,
    response_durations_ms: Vec<Option<u64>>,
    timeline_json: Option<String>,
) -> Result<()> {
    let refreshed = sessions::refresh_snapshot(
        snapshot,
        agent.messages.clone(),
        last_response_duration_ms,
        previous_response_duration_ms,
        Some(response_durations_ms),
        timeline_json,
    );
    let snapshot_for_blocking = refreshed.clone();
    tokio::task::spawn_blocking(move || sessions::save_session(&snapshot_for_blocking)).await??;
    *snapshot = refreshed;
    Ok(())
}

pub(super) fn contains_point(area: Rect, column: u16, row: u16) -> bool {
    column >= area.x && column < area.right() && row >= area.y && row < area.bottom()
}

pub(super) fn copy_text_to_clipboard(
    clipboard: &mut Option<arboard::Clipboard>,
    text: &str,
) -> io::Result<()> {
    // Try native clipboard first
    if let Some(ref mut cb) = clipboard {
        if cb.set_text(text).is_ok() {
            return Ok(());
        }
    }
    // Fallback: OSC 52 (works over SSH, tmux, etc.)
    write_osc52(text)
}

fn write_osc52(text: &str) -> io::Result<()> {
    use base64::Engine;
    use std::io::Write;

    let encoded = base64::engine::general_purpose::STANDARD.encode(text.as_bytes());
    let in_tmux = std::env::var_os("TMUX").is_some();

    let sequence = if in_tmux {
        format!("\x1bPtmux;\x1b\x1b]52;c;{encoded}\x07\x1b\\")
    } else {
        format!("\x1b]52;c;{encoded}\x07")
    };

    let mut stdout = io::stdout().lock();
    stdout.write_all(sequence.as_bytes())?;
    stdout.flush()
}