Files
codex/prs/bolinfest/study/PR-1695-study.md
2025-09-02 15:17:45 -07:00

6.9 KiB
Raw Blame History

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: Dont 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);
    }
    

DONTs

  • Dont 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(...)
    
  • Dont 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)
    
  • Dont 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()?;
    
  • Dont use ad-hoc strings for image format: Prefer a typed enum.

    // ❌
    struct PastedImageInfo { encoded_format_label: &'static str }
    
    // ✅
    struct PastedImageInfo { encoded_format: EncodedImageFormat }
    
  • Dont 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) { /* ... */ }
    
  • Dont 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) {}
    
  • Dont 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);
    
  • Dont 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}");