Files
llmx/codex-rs/tui/src/app.rs
jcoens-openai 87cf120873 Workspace lints and disallow unwrap (#855)
Sets submodules to use workspace lints. Added denying unwrap as a
workspace level lint, which found a couple of cases where we could have
propagated errors. Also manually labeled ones that were fine by my eye.
2025-05-08 09:46:18 -07:00

216 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::app_event::AppEvent;
use crate::chatwidget::ChatWidget;
use crate::git_warning_screen::GitWarningOutcome;
use crate::git_warning_screen::GitWarningScreen;
use crate::scroll_event_helper::ScrollEventHelper;
use crate::tui;
use codex_core::config::Config;
use codex_core::protocol::Event;
use codex_core::protocol::Op;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::MouseEvent;
use crossterm::event::MouseEventKind;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
use std::sync::mpsc::channel;
/// Toplevel application state which fullscreen view is currently active.
enum AppState {
/// The main chat UI is visible.
Chat,
/// The startup warning that recommends running codex inside a Git repo.
GitWarning { screen: GitWarningScreen },
}
pub(crate) struct App<'a> {
app_event_tx: Sender<AppEvent>,
app_event_rx: Receiver<AppEvent>,
chat_widget: ChatWidget<'a>,
app_state: AppState,
}
impl App<'_> {
pub(crate) fn new(
config: Config,
initial_prompt: Option<String>,
show_git_warning: bool,
initial_images: Vec<std::path::PathBuf>,
) -> Self {
let (app_event_tx, app_event_rx) = channel();
let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone());
// Spawn a dedicated thread for reading the crossterm event loop and
// re-publishing the events as AppEvents, as appropriate.
{
let app_event_tx = app_event_tx.clone();
std::thread::spawn(move || {
while let Ok(event) = crossterm::event::read() {
let app_event = match event {
crossterm::event::Event::Key(key_event) => AppEvent::KeyEvent(key_event),
crossterm::event::Event::Resize(_, _) => AppEvent::Redraw,
crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
}) => {
scroll_event_helper.scroll_up();
continue;
}
crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
}) => {
scroll_event_helper.scroll_down();
continue;
}
_ => {
continue;
}
};
if let Err(e) = app_event_tx.send(app_event) {
tracing::error!("failed to send event: {e}");
}
}
});
}
let chat_widget = ChatWidget::new(
config,
app_event_tx.clone(),
initial_prompt.clone(),
initial_images,
);
let app_state = if show_git_warning {
AppState::GitWarning {
screen: GitWarningScreen::new(),
}
} else {
AppState::Chat
};
Self {
app_event_tx,
app_event_rx,
chat_widget,
app_state,
}
}
/// Clone of the internal event sender so external tasks (e.g. log bridge)
/// can inject `AppEvent`s.
pub fn event_sender(&self) -> Sender<AppEvent> {
self.app_event_tx.clone()
}
pub(crate) fn run(&mut self, terminal: &mut tui::Tui) -> Result<()> {
// Insert an event to trigger the first render.
let app_event_tx = self.app_event_tx.clone();
app_event_tx.send(AppEvent::Redraw)?;
while let Ok(event) = self.app_event_rx.recv() {
match event {
AppEvent::Redraw => {
self.draw_next_frame(terminal)?;
}
AppEvent::KeyEvent(key_event) => {
match key_event {
KeyEvent {
code: KeyCode::Char('c'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
..
} => {
self.chat_widget.submit_op(Op::Interrupt);
}
KeyEvent {
code: KeyCode::Char('d'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
..
} => {
self.app_event_tx.send(AppEvent::ExitRequest)?;
}
_ => {
self.dispatch_key_event(key_event);
}
};
}
AppEvent::Scroll(scroll_delta) => {
self.dispatch_scroll_event(scroll_delta);
}
AppEvent::CodexEvent(event) => {
self.dispatch_codex_event(event);
}
AppEvent::ExitRequest => {
break;
}
AppEvent::CodexOp(op) => {
if matches!(self.app_state, AppState::Chat) {
self.chat_widget.submit_op(op);
}
}
AppEvent::LatestLog(line) => {
if matches!(self.app_state, AppState::Chat) {
let _ = self.chat_widget.update_latest_log(line);
}
}
}
}
terminal.clear()?;
Ok(())
}
fn draw_next_frame(&mut self, terminal: &mut tui::Tui) -> Result<()> {
match &mut self.app_state {
AppState::Chat => {
terminal.draw(|frame| frame.render_widget_ref(&self.chat_widget, frame.area()))?;
}
AppState::GitWarning { screen } => {
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
}
}
Ok(())
}
/// Dispatch a KeyEvent to the current view and let it decide what to do
/// with it.
fn dispatch_key_event(&mut self, key_event: KeyEvent) {
match &mut self.app_state {
AppState::Chat => {
if let Err(e) = self.chat_widget.handle_key_event(key_event) {
tracing::error!("SendError: {e}");
}
}
AppState::GitWarning { screen } => match screen.handle_key_event(key_event) {
GitWarningOutcome::Continue => {
self.app_state = AppState::Chat;
let _ = self.app_event_tx.send(AppEvent::Redraw);
}
GitWarningOutcome::Quit => {
let _ = self.app_event_tx.send(AppEvent::ExitRequest);
}
GitWarningOutcome::None => {
// do nothing
}
},
}
}
fn dispatch_scroll_event(&mut self, scroll_delta: i32) {
if matches!(self.app_state, AppState::Chat) {
if let Err(e) = self.chat_widget.handle_scroll_delta(scroll_delta) {
tracing::error!("SendError: {e}");
}
}
}
fn dispatch_codex_event(&mut self, event: Event) {
if matches!(self.app_state, AppState::Chat) {
if let Err(e) = self.chat_widget.handle_codex_event(event) {
tracing::error!("SendError: {e}");
}
}
}
}