Compare commits

...

3 Commits

Author SHA1 Message Date
Amjith Ramanujam
0f0cc03291 Remove non-ascii chars. 2025-08-18 08:34:08 -07:00
Amjith Ramanujam
b7afed5042 Update README with local dev instructions. 2025-08-18 08:01:28 -07:00
amjith
c7e0d5645f feat(tui): add yank and undo shortcuts 2025-08-15 09:48:09 -07:00
3 changed files with 135 additions and 7 deletions

View File

@@ -17,6 +17,7 @@
- [Quickstart](#quickstart)
- [Installing and running Codex CLI](#installing-and-running-codex-cli)
- [Local Build & Run (from source)](#local-build--run-from-source)
- [Using Codex with your ChatGPT plan](#using-codex-with-your-chatgpt-plan)
- [Connecting on a "Headless" Machine](#connecting-on-a-headless-machine)
- [Authenticate locally and copy your credentials to the "headless" machine](#authenticate-locally-and-copy-your-credentials-to-the-headless-machine)
@@ -95,6 +96,28 @@ Each archive contains a single entry with the platform baked into the name (e.g.
</details>
### Local Build & Run (from source)
Build and run the Rust CLI locally from the `codex-rs` workspace.
Prerequisites
- Rust toolchain via `rustup` (toolchain pinned by `codex-rs/rust-toolchain.toml`)
- macOS: `brew install pkg-config openssl`
- Linux (Debian/Ubuntu): `sudo apt-get install -y build-essential pkg-config libssl-dev`
Build & Run
- Run the TUI (default):
```bash
cd codex-rs
cargo run --bin codex
```
Convenience (via `just`, inside `codex-rs`)
- `just codex` - run the CLI
- `just tui` - run the TUI subcommand
- `just fmt` - format code
- `just fix` - apply clippy fixes
### Using Codex with your ChatGPT plan
<p align="center">

View File

@@ -2,16 +2,27 @@
We provide Codex CLI as a standalone, native executable to ensure a zero-dependency install.
## Installing Codex
## Local Build & Run
Today, the easiest way to install Codex is via `npm`, though we plan to publish Codex to other package managers soon.
Build and run the Rust CLI locally from source.
```shell
npm i -g @openai/codex@native
codex
```
Prerequisites
- Rust toolchain via `rustup` (toolchain pinned by `rust-toolchain.toml`)
- macOS: `brew install pkg-config openssl`
- Linux (Debian/Ubuntu): `sudo apt-get install -y build-essential pkg-config libssl-dev`
You can also download a platform-specific release directly from our [GitHub Releases](https://github.com/openai/codex/releases).
Build & Run
- Run the TUI (default):
```bash
cd codex-rs
cargo run --bin codex
```
Convenience (via `just`, inside `codex-rs`)
- `just codex` — run the CLI
- `just tui` — run the TUI subcommand
- `just fmt` — format code
- `just fix` — apply clippy fixes
## What's new in the Rust CLI

View File

@@ -26,6 +26,8 @@ pub(crate) struct TextArea {
wrap_cache: RefCell<Option<WrapCache>>,
preferred_col: Option<usize>,
elements: Vec<TextElement>,
kill_buffer: String,
undo_stack: Vec<UndoState>,
}
#[derive(Debug, Clone)]
@@ -34,6 +36,13 @@ struct WrapCache {
lines: Vec<Range<usize>>,
}
#[derive(Debug, Clone)]
struct UndoState {
text: String,
cursor_pos: usize,
elements: Vec<TextElement>,
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct TextAreaState {
/// Index into wrapped lines of the first visible line.
@@ -48,6 +57,8 @@ impl TextArea {
wrap_cache: RefCell::new(None),
preferred_col: None,
elements: Vec::new(),
kill_buffer: String::new(),
undo_stack: Vec::new(),
}
}
@@ -57,6 +68,8 @@ impl TextArea {
self.wrap_cache.replace(None);
self.preferred_col = None;
self.elements.clear();
self.kill_buffer.clear();
self.undo_stack.clear();
}
pub fn text(&self) -> &str {
@@ -68,6 +81,7 @@ impl TextArea {
}
pub fn insert_str_at(&mut self, pos: usize, text: &str) {
self.push_undo_state();
let pos = self.clamp_pos_for_insertion(pos);
self.text.insert_str(pos, text);
self.wrap_cache.replace(None);
@@ -84,6 +98,7 @@ impl TextArea {
}
fn replace_range_raw(&mut self, range: std::ops::Range<usize>, text: &str) {
self.push_undo_state();
assert!(range.start <= range.end);
let start = range.start.clamp(0, self.text.len());
let end = range.end.clamp(0, self.text.len());
@@ -116,6 +131,14 @@ impl TextArea {
self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);
}
fn push_undo_state(&mut self) {
self.undo_stack.push(UndoState {
text: self.text.clone(),
cursor_pos: self.cursor_pos,
elements: self.elements.clone(),
});
}
pub fn cursor(&self) -> usize {
self.cursor_pos
}
@@ -279,6 +302,20 @@ impl TextArea {
} => {
self.kill_to_end_of_line();
}
KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::CONTROL,
..
} => {
self.yank();
}
KeyEvent {
code: KeyCode::Char('_' | '\u{1f}'),
modifiers: KeyModifiers::CONTROL,
..
} => {
self.undo();
}
// Cursor movement
KeyEvent {
@@ -411,17 +448,25 @@ impl TextArea {
pub fn delete_backward_word(&mut self) {
let start = self.beginning_of_previous_word();
let removed = self.text[start..self.cursor_pos].to_string();
self.replace_range(start..self.cursor_pos, "");
if !removed.is_empty() {
self.kill_buffer = removed;
}
}
pub fn kill_to_end_of_line(&mut self) {
let eol = self.end_of_current_line();
if self.cursor_pos == eol {
if eol < self.text.len() {
let removed = self.text[self.cursor_pos..eol + 1].to_string();
self.replace_range(self.cursor_pos..eol + 1, "");
self.kill_buffer = removed;
}
} else {
let removed = self.text[self.cursor_pos..eol].to_string();
self.replace_range(self.cursor_pos..eol, "");
self.kill_buffer = removed;
}
}
@@ -429,10 +474,31 @@ impl TextArea {
let bol = self.beginning_of_current_line();
if self.cursor_pos == bol {
if bol > 0 {
let removed = self.text[bol - 1..bol].to_string();
self.replace_range(bol - 1..bol, "");
self.kill_buffer = removed;
}
} else {
let removed = self.text[bol..self.cursor_pos].to_string();
self.replace_range(bol..self.cursor_pos, "");
self.kill_buffer = removed;
}
}
pub fn yank(&mut self) {
if !self.kill_buffer.is_empty() {
let ins = self.kill_buffer.clone();
self.insert_str(&ins);
}
}
pub fn undo(&mut self) {
if let Some(state) = self.undo_stack.pop() {
self.text = state.text;
self.cursor_pos = state.cursor_pos.min(self.text.len());
self.elements = state.elements;
self.wrap_cache.replace(None);
self.preferred_col = None;
}
}
@@ -1098,6 +1164,34 @@ mod tests {
assert_eq!(t.cursor(), 3);
}
#[test]
fn yank_restores_last_kill() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
let mut t = ta_with("hello world");
t.set_cursor(5);
t.kill_to_end_of_line();
t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL));
assert_eq!(t.text(), "hello world");
assert_eq!(t.cursor(), 11);
}
#[test]
fn undo_reverts_last_change() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
let mut t = ta_with("hello");
t.set_cursor(5);
t.insert_str("!");
t.input(KeyEvent::new(KeyCode::Char('_'), KeyModifiers::CONTROL));
assert_eq!(t.text(), "hello");
assert_eq!(t.cursor(), 5);
}
#[test]
fn cursor_left_and_right_handle_graphemes() {
let mut t = ta_with("a👍b");