main
  1mod app;
  2mod protocol;
  3
  4use app::App;
  5use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind};
  6use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
  7use crossterm::ExecutableCommand;
  8use protocol::{Event, Line};
  9use ratatui::layout::{Constraint, Layout};
 10use ratatui::widgets::{Block, Borders, Paragraph};
 11use ratatui::Terminal;
 12use std::fs::{File, OpenOptions};
 13use std::io::{self, BufRead, Write};
 14use std::sync::mpsc;
 15use std::sync::{Arc, Mutex};
 16use std::thread;
 17use std::time::Duration;
 18
 19fn write_reply(stdout: &Arc<Mutex<io::Stdout>>, line: Option<String>) {
 20    let payload = serde_json::to_string(&Line { line }).unwrap();
 21    let mut out = stdout.lock().unwrap();
 22    let _ = writeln!(out, "{payload}");
 23    let _ = out.flush();
 24}
 25
 26fn open_tty() -> io::Result<File> {
 27    OpenOptions::new().read(true).write(true).open("/dev/tty")
 28}
 29
 30fn main() -> io::Result<()> {
 31    let stdout_handle = Arc::new(Mutex::new(io::stdout()));
 32
 33    let (tx, rx) = mpsc::channel::<Event>();
 34    thread::spawn(move || {
 35        let stdin = io::stdin();
 36        for line in stdin.lock().lines() {
 37            let Ok(line) = line else { break };
 38            if line.trim().is_empty() {
 39                continue;
 40            }
 41            match serde_json::from_str::<Event>(&line) {
 42                Ok(event) => {
 43                    if tx.send(event).is_err() {
 44                        break;
 45                    }
 46                }
 47                Err(err) => eprintln!("elelem-ratatui: bad event: {err}"),
 48            }
 49        }
 50    });
 51
 52    let mut tty = open_tty()?;
 53    enable_raw_mode()?;
 54    tty.execute(EnterAlternateScreen)?;
 55
 56    let backend_tty = tty.try_clone()?;
 57    let terminal = Terminal::new(ratatui::backend::CrosstermBackend::new(backend_tty));
 58    let result = match terminal {
 59        Ok(mut terminal) => {
 60            let mut app = App::new();
 61            run(&mut terminal, &mut app, &rx, &stdout_handle)
 62        }
 63        Err(err) => Err(err),
 64    };
 65
 66    disable_raw_mode()?;
 67    tty.execute(LeaveAlternateScreen)?;
 68    result
 69}
 70
 71fn run(
 72    terminal: &mut Terminal<ratatui::backend::CrosstermBackend<File>>,
 73    app: &mut App,
 74    rx: &mpsc::Receiver<Event>,
 75    stdout_handle: &Arc<Mutex<io::Stdout>>,
 76) -> io::Result<()> {
 77    loop {
 78        while let Ok(event) = rx.try_recv() {
 79            app.apply(event);
 80        }
 81
 82        if app.should_quit {
 83            return Ok(());
 84        }
 85
 86        terminal.draw(|f| draw(f, app))?;
 87
 88        if event::poll(Duration::from_millis(50))? {
 89            if let TermEvent::Key(key) = event::read()? {
 90                if key.kind != KeyEventKind::Press {
 91                    continue;
 92                }
 93
 94                if app.pending_prompt.is_some() {
 95                    match key.code {
 96                        KeyCode::Enter => {
 97                            let line = std::mem::take(&mut app.input);
 98                            app.pending_prompt = None;
 99                            write_reply(stdout_handle, Some(line));
100                        }
101                        KeyCode::Char('d')
102                            if key
103                                .modifiers
104                                .contains(crossterm::event::KeyModifiers::CONTROL) =>
105                        {
106                            app.pending_prompt = None;
107                            app.should_quit = true;
108                            write_reply(stdout_handle, None);
109                        }
110                        KeyCode::Char(c) => app.input.push(c),
111                        KeyCode::Backspace => {
112                            app.input.pop();
113                        }
114                        _ => {}
115                    }
116                } else if key.code == KeyCode::Char('c')
117                    && key
118                        .modifiers
119                        .contains(crossterm::event::KeyModifiers::CONTROL)
120                {
121                    app.should_quit = true;
122                }
123            }
124        }
125    }
126}
127
128fn draw(frame: &mut ratatui::Frame, app: &App) {
129    let area = frame.area();
130    let chunks = Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).split(area);
131
132    let title = if app.waiting { "elelem (waiting…)" } else { "elelem" };
133    let log = Paragraph::new(app.render_rows())
134        .block(Block::default().borders(Borders::ALL).title(title))
135        .wrap(ratatui::widgets::Wrap { trim: false });
136    frame.render_widget(log, chunks[0]);
137
138    let prompt_title = app.pending_prompt.as_deref().unwrap_or("(no prompt pending)");
139    let input = Paragraph::new(app.input.as_str())
140        .block(Block::default().borders(Borders::ALL).title(prompt_title));
141    frame.render_widget(input, chunks[1]);
142}