feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`: Today, Codex CLI is written in TypeScript and requires Node.js 22+ to run it. For a number of users, this runtime requirement inhibits adoption: they would be better served by a standalone executable. As maintainers, we want Codex to run efficiently in a wide range of environments with minimal overhead. We also want to take advantage of operating system-specific APIs to provide better sandboxing, where possible. To that end, we are moving forward with a Rust implementation of Codex CLI contained in this folder, which has the following benefits: - The CLI compiles to small, standalone, platform-specific binaries. - Can make direct, native calls to [seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and [landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in order to support sandboxing on Linux. - No runtime garbage collection, resulting in lower memory consumption and better, more predictable performance. Currently, the Rust implementation is materially behind the TypeScript implementation in functionality, so continue to use the TypeScript implmentation for the time being. We will publish native executables via GitHub Releases as soon as we feel the Rust version is usable.
This commit is contained in:
194
codex-rs/tui/src/app.rs
Normal file
194
codex-rs/tui/src/app.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::chatwidget::ChatWidget;
|
||||
use crate::git_warning_screen::GitWarningOutcome;
|
||||
use crate::git_warning_screen::GitWarningScreen;
|
||||
use crate::tui;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
/// Top‑level application state – which full‑screen view is currently active.
|
||||
enum AppState {
|
||||
/// The main chat UI is visible.
|
||||
Chat,
|
||||
/// The start‑up 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(
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
initial_prompt: Option<String>,
|
||||
show_git_warning: bool,
|
||||
initial_images: Vec<std::path::PathBuf>,
|
||||
model: Option<String>,
|
||||
) -> Self {
|
||||
let (app_event_tx, app_event_rx) = channel();
|
||||
|
||||
// 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::FocusGained
|
||||
| crossterm::event::Event::FocusLost
|
||||
| crossterm::event::Event::Mouse(_)
|
||||
| crossterm::event::Event::Paste(_) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(e) = app_event_tx.send(app_event) {
|
||||
tracing::error!("failed to send event: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let chat_widget = ChatWidget::new(
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
app_event_tx.clone(),
|
||||
initial_prompt.clone(),
|
||||
initial_images,
|
||||
model,
|
||||
);
|
||||
|
||||
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).unwrap();
|
||||
|
||||
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).unwrap();
|
||||
}
|
||||
_ => {
|
||||
self.dispatch_key_event(key_event);
|
||||
}
|
||||
};
|
||||
}
|
||||
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_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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user