use anyhow::Result;
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton,
MouseEventKind,
},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::backend::CrosstermBackend;
use std::io;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use crate::multiplexer::{create_backend, detect_backend};
use super::app::SidebarApp;
use super::client;
use super::daemon_ctrl::ensure_daemon_running;
use super::panes::shutdown_all_sidebars;
use super::ui::render_sidebar;
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), DisableMouseCapture, LeaveAlternateScreen);
}
}
enum AppEvent {
SnapshotReady,
Input(Event),
}
fn spawn_input_thread(tx: mpsc::Sender<AppEvent>) {
thread::spawn(move || {
while let Ok(ev) = event::read() {
if tx.send(AppEvent::Input(ev)).is_err() {
break;
}
}
});
}
pub fn run_sidebar() -> Result<()> {
let mux = create_backend(detect_backend());
if !mux.is_running().unwrap_or(false) {
tracing::info!("sidebar-run exiting: mux not running");
return Ok(());
}
let mut app = SidebarApp::new_client(mux)?;
let sock_path = ensure_daemon_running()?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let _guard = TerminalGuard;
let backend = CrosstermBackend::new(stdout);
let mut terminal = ratatui::Terminal::new(backend)?;
let (tx, rx) = mpsc::channel();
let snapshot_handle = {
let (wake_tx, wake_rx) = mpsc::sync_channel::<()>(1);
let event_tx = tx.clone();
thread::spawn(move || {
for () in wake_rx {
if event_tx.send(AppEvent::SnapshotReady).is_err() {
break;
}
}
});
client::connect(&sock_path, wake_tx)
};
spawn_input_thread(tx);
let mut needs_render = true;
let startup = std::time::Instant::now();
let startup_grace = Duration::from_secs(3);
loop {
if needs_render {
terminal.draw(|f| render_sidebar(f, &mut app))?;
needs_render = false;
}
let timeout = if app.host_window_active() {
Duration::from_millis(250)
} else {
Duration::from_secs(3600)
};
let first_event = match rx.recv_timeout(timeout) {
Ok(ev) => Some(ev),
Err(mpsc::RecvTimeoutError::Timeout) => {
if app.host_window_active() {
app.tick();
needs_render = true;
}
continue;
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
tracing::info!("sidebar-run exiting: event channel disconnected");
break;
}
};
if let Some(ev) = first_event {
process_event(
ev,
&mut app,
&snapshot_handle,
&startup,
startup_grace,
&mut needs_render,
);
}
while let Ok(ev) = rx.try_recv() {
process_event(
ev,
&mut app,
&snapshot_handle,
&startup,
startup_grace,
&mut needs_render,
);
}
if app.should_quit {
tracing::info!(
host_window = ?app.host_window_id(),
quit_reason = app.quit_reason.as_deref().unwrap_or("unknown"),
"sidebar-run quitting"
);
shutdown_all_sidebars();
break;
}
}
Ok(())
}
fn process_event(
event: AppEvent,
app: &mut SidebarApp,
snapshot_handle: &client::SnapshotHandle,
startup: &std::time::Instant,
startup_grace: Duration,
needs_render: &mut bool,
) {
match event {
AppEvent::SnapshotReady => {
if let Some(snapshot) = snapshot_handle.take() {
if startup.elapsed() > startup_grace
&& let Some(wid) = app.host_window_id()
&& snapshot.window_pane_counts.get(wid).copied().unwrap_or(2) <= 1
{
app.quit_reason = Some(format!("last-pane: window {} has <= 1 pane", wid));
app.should_quit = true;
}
app.apply_snapshot(snapshot);
*needs_render = true;
}
}
AppEvent::Input(Event::Key(key)) if key.kind == KeyEventKind::Press => {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), _)
| (KeyCode::Esc, _)
| (KeyCode::Char('c'), crossterm::event::KeyModifiers::CONTROL) => {
app.quit_reason = Some("user keypress".to_string());
app.should_quit = true;
}
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => app.next(),
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => app.previous(),
(KeyCode::Enter, _) => app.jump_to_selected(),
(KeyCode::Char('G'), _) => app.select_last(),
(KeyCode::Char('g'), _) => app.select_first(),
(KeyCode::Char('v'), _) => app.toggle_layout_mode(),
(KeyCode::Char('z'), _) => app.toggle_sleeping(),
_ => {}
}
*needs_render = true;
}
AppEvent::Input(Event::Mouse(mouse)) => {
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
if let Some(idx) = app.hit_test(mouse.column, mouse.row) {
app.select_index(idx);
app.jump_to_selected();
}
}
MouseEventKind::ScrollUp => {
app.scroll_up();
}
MouseEventKind::ScrollDown => {
app.scroll_down();
}
_ => {}
}
*needs_render = true;
}
AppEvent::Input(Event::Resize(_, _)) => {
*needs_render = true;
}
AppEvent::Input(_) => {}
}
}