mirror of
https://github.com/openai/codex.git
synced 2026-04-28 18:32:04 +03:00
215 lines
6.9 KiB
Markdown
215 lines
6.9 KiB
Markdown
**DOs**
|
||
|
||
- **Keep app.rs lean:** Drive the event loop and delegate event-specific handling to modules.
|
||
```
|
||
// app.rs
|
||
match event {
|
||
TuiEvent::AttachImage { path, width, height, format_label } => {
|
||
self.chat_widget.attach_image(path, width, height, format_label);
|
||
}
|
||
TuiEvent::Key(key) => self.chat_widget.handle_key_event(key),
|
||
TuiEvent::Paste(s) => self.chat_widget.handle_paste(s),
|
||
TuiEvent::Draw => self.redraw()?,
|
||
}
|
||
```
|
||
|
||
- **Generate rich events in the TUI layer:** Map platform input to semantic TuiEvents before app.rs sees them.
|
||
```
|
||
// tui.rs
|
||
match crossterm_event {
|
||
Event::Key(k) if is_ctrl_or_cmd_v(&k) => {
|
||
if let Ok((path, info)) = paste_image_to_temp_png() {
|
||
yield TuiEvent::AttachImage { path, width: info.width, height: info.height, format_label: info.encoded_format.label() };
|
||
} else {
|
||
yield TuiEvent::Key(k);
|
||
}
|
||
}
|
||
Event::Paste(s) => yield TuiEvent::Paste(s),
|
||
Event::Key(k) => yield TuiEvent::Key(k),
|
||
Event::Resize(_, _) => yield TuiEvent::Draw,
|
||
_ => {}
|
||
}
|
||
```
|
||
|
||
- **Handle cross-platform shortcuts:** Don’t assume macOS Cmd maps to Control; accept Control, Super, or Meta for V.
|
||
```
|
||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||
|
||
fn is_ctrl_or_cmd_v(k: &KeyEvent) -> bool {
|
||
k.kind == KeyEventKind::Press
|
||
&& matches!(k.code, KeyCode::Char('v'))
|
||
&& (k.modifiers.contains(KeyModifiers::CONTROL)
|
||
|| k.modifiers.contains(KeyModifiers::SUPER)
|
||
|| k.modifiers.contains(KeyModifiers::META))
|
||
}
|
||
```
|
||
|
||
- **Model image formats with an enum, not strings:** Add a label() to present user-friendly text.
|
||
```
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
enum EncodedImageFormat { Png }
|
||
|
||
impl EncodedImageFormat {
|
||
fn label(self) -> &'static str {
|
||
match self { EncodedImageFormat::Png => "PNG" }
|
||
}
|
||
}
|
||
```
|
||
|
||
- **Write clipboard images via tempfile and persist safely:**
|
||
```
|
||
use tempfile::NamedTempFile;
|
||
|
||
let (png_bytes, info) = paste_image_as_png()?;
|
||
let tmp = NamedTempFile::new()?; // unique, race-free
|
||
std::fs::write(tmp.path(), &png_bytes)?;
|
||
let (_file, path) = tmp.keep()?; // persist and get PathBuf
|
||
```
|
||
|
||
- **Prefer small structs over tuples for clarity:** Make stored state self-documenting.
|
||
```
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
struct AttachedImage {
|
||
placeholder: String,
|
||
path: PathBuf,
|
||
}
|
||
```
|
||
|
||
- **Make image placeholders atomic UI elements and keep mappings in sync:**
|
||
```
|
||
// Insert atomic element so a single Backspace removes it.
|
||
self.textarea.insert_element(&placeholder);
|
||
self.attached_images.push(AttachedImage { placeholder, path });
|
||
|
||
// On edit, keep only as many AttachedImage entries as visible placeholders.
|
||
let visible = text_after.matches(&img.placeholder).count();
|
||
```
|
||
|
||
- **Strip placeholders before submit; drain images separately:**
|
||
```
|
||
// composer.rs (on Enter)
|
||
for img in &self.attached_images {
|
||
text = text.replace(&img.placeholder, "");
|
||
}
|
||
text = text.trim().to_string();
|
||
|
||
// chatwidget.rs
|
||
let images = self.bottom_pane.take_recent_submission_images();
|
||
self.submit_user_message(UserMessage { text, image_paths: images });
|
||
```
|
||
|
||
- **Inline variables in logs and formats:** Use modern capture in tracing/format! calls.
|
||
```
|
||
tracing::info!("attach_image path={path:?} width={width} height={height} format={format_label}");
|
||
let msg = format!("added {count} images from {source}");
|
||
```
|
||
|
||
- **Use local imports and concise types; move imports to the module top:**
|
||
```
|
||
use std::path::PathBuf; // at top of file
|
||
// ...
|
||
fn attach_image(&mut self, path: PathBuf, width: u32, height: u32, format_label: &str) { /* ... */ }
|
||
```
|
||
|
||
- **Test user-visible behaviors thoroughly:** Attach + submit, deletion at both ends, and duplicates.
|
||
```
|
||
#[test]
|
||
fn deleting_one_of_duplicate_image_placeholders_removes_matching_entry() {
|
||
composer.attach_image(p1.clone(), 10, 5, "PNG");
|
||
composer.handle_paste(" ".into());
|
||
composer.attach_image(p2.clone(), 10, 5, "PNG");
|
||
composer.textarea.set_cursor(end_of_first_placeholder);
|
||
composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
|
||
assert_eq!(1, composer.textarea.text().matches(&ph).count());
|
||
assert_eq!(vec![p2], composer.take_recent_submission_images());
|
||
}
|
||
```
|
||
|
||
- **Treat @file-selected images as images (with fallback):**
|
||
```
|
||
if is_image_path(&sel_path) {
|
||
if let Ok((w, h)) = image::image_dimensions(&sel_path) {
|
||
self.attach_image(PathBuf::from(&sel_path), w, h, "PNG"); // or "JPEG"
|
||
self.textarea.insert_str(" ");
|
||
} else {
|
||
self.insert_selected_path(&sel_path); // fallback
|
||
}
|
||
} else {
|
||
self.insert_selected_path(&sel_path);
|
||
}
|
||
```
|
||
|
||
|
||
**DON’Ts**
|
||
|
||
- **Don’t put event logic in app.rs:** Keep it for orchestration; push specifics into TUI/chat modules.
|
||
```
|
||
// ❌ Heavy logic in app.rs
|
||
if is_ctrl_or_cmd_v(&key) { do_clipboard_io_here(); }
|
||
|
||
// ✅ Delegate
|
||
TuiEvent::AttachImage { .. } => self.chat_widget.attach_image(...)
|
||
```
|
||
|
||
- **Don’t assume Cmd==Control on macOS:** Accept SUPER/META too; fall back gracefully.
|
||
```
|
||
// ❌ Only CONTROL
|
||
k.modifiers.contains(KeyModifiers::CONTROL)
|
||
|
||
// ✅ Cross-platform
|
||
k.modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER | KeyModifiers::META)
|
||
```
|
||
|
||
- **Don’t write temp files with ad-hoc names in temp_dir:** Avoid collisions and TOCTOU races.
|
||
```
|
||
// ❌
|
||
let mut p = std::env::temp_dir(); p.push("clipboard.png"); std::fs::write(p, &bytes)?;
|
||
|
||
// ✅
|
||
let tmp = tempfile::NamedTempFile::new()?;
|
||
```
|
||
|
||
- **Don’t use ad-hoc strings for image format:** Prefer a typed enum.
|
||
```
|
||
// ❌
|
||
struct PastedImageInfo { encoded_format_label: &'static str }
|
||
|
||
// ✅
|
||
struct PastedImageInfo { encoded_format: EncodedImageFormat }
|
||
```
|
||
|
||
- **Don’t return meaningless booleans or keep dead code:** Remove unused return values and placeholders.
|
||
```
|
||
// ❌
|
||
pub fn attach_image(&mut self, path: PathBuf, w: u32, h: u32, fmt: &str) -> bool { /* always true */ }
|
||
|
||
// ✅
|
||
pub fn attach_image(&mut self, path: PathBuf, w: u32, h: u32, fmt: &str) { /* ... */ }
|
||
```
|
||
|
||
- **Don’t fully qualify common std types everywhere or import inside tests:** Keep files tidy and readable.
|
||
```
|
||
// ❌
|
||
fn f(p: std::path::PathBuf) {}
|
||
|
||
// ✅
|
||
use std::path::PathBuf;
|
||
fn f(p: PathBuf) {}
|
||
```
|
||
|
||
- **Don’t let placeholders and image state drift:** Always update mappings when text changes.
|
||
```
|
||
// ❌ Forgetting to drop mapping when placeholder deleted.
|
||
|
||
// ✅ Remove mapping when matching placeholder instance is removed.
|
||
self.attached_images.remove(idx);
|
||
```
|
||
|
||
- **Don’t log with positional/format-args noise:** Use inline captures for clarity and consistency.
|
||
```
|
||
// ❌
|
||
tracing::info!("path: {:?}, width: {}, height: {}", path, width, height);
|
||
|
||
// ✅
|
||
tracing::info!("path={path:?} width={width} height={height}");
|
||
``` |