tui: fix wrapping in trust_directory (#5007)
Refactor trust_directory to use ColumnRenderable & friends, thus correcting wrapping behavior at small widths. Also introduce RowRenderable with fixed-width rows. - fixed wrapping in trust_directory - changed selector cursor to match other list item selections - allow y/n to work as well as 1/2 - fixed key_hint to be standard before: <img width="661" height="550" alt="Screenshot 2025-10-09 at 9 50 36 AM" src="https://github.com/user-attachments/assets/e01627aa-bee4-4e25-8eca-5575c43f05bf" /> after: <img width="661" height="550" alt="Screenshot 2025-10-09 at 9 51 31 AM" src="https://github.com/user-attachments/assets/cb816cbd-7609-4c83-b62f-b4dba392d79a" />
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/onboarding/trust_directory.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
> You are running Codex in /workspace/project
|
||||
|
||||
Since this folder is version controlled, you may wish to allow Codex
|
||||
to work in this folder without asking for approval.
|
||||
|
||||
› 1. Yes, allow Codex to work in this folder without asking for
|
||||
approval
|
||||
2. No, ask me to approve edits and commands
|
||||
|
||||
Press enter to continue
|
||||
@@ -7,19 +7,25 @@ use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Widget;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Styled as _;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
use crate::key_hint;
|
||||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||
use crate::render::Insets;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableExt as _;
|
||||
use crate::render::renderable::RowRenderable;
|
||||
|
||||
use super::onboarding_screen::StepState;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub(crate) struct TrustDirectoryWidget {
|
||||
pub codex_home: PathBuf,
|
||||
@@ -38,76 +44,106 @@ pub enum TrustDirectorySelection {
|
||||
|
||||
impl WidgetRef for &TrustDirectoryWidget {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut lines: Vec<Line> = vec![
|
||||
Line::from(vec![
|
||||
"> ".into(),
|
||||
"You are running Codex in ".bold(),
|
||||
self.cwd.to_string_lossy().to_string().into(),
|
||||
]),
|
||||
"".into(),
|
||||
];
|
||||
let mut column = ColumnRenderable::new();
|
||||
|
||||
if self.is_git_repo {
|
||||
lines.push(
|
||||
" Since this folder is version controlled, you may wish to allow Codex".into(),
|
||||
);
|
||||
lines.push(" to work in this folder without asking for approval.".into());
|
||||
column.push(Line::from(vec![
|
||||
"> ".into(),
|
||||
"You are running Codex in ".bold(),
|
||||
self.cwd.to_string_lossy().to_string().into(),
|
||||
]));
|
||||
column.push("");
|
||||
|
||||
let guidance = if self.is_git_repo {
|
||||
"Since this folder is version controlled, you may wish to allow Codex to work in this folder without asking for approval."
|
||||
} else {
|
||||
lines.push(
|
||||
" Since this folder is not version controlled, we recommend requiring".into(),
|
||||
);
|
||||
lines.push(" approval of all edits and commands.".into());
|
||||
}
|
||||
lines.push("".into());
|
||||
"Since this folder is not version controlled, we recommend requiring approval of all edits and commands."
|
||||
};
|
||||
|
||||
let create_option =
|
||||
|idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> {
|
||||
let is_selected = self.highlighted == option;
|
||||
if is_selected {
|
||||
Line::from(format!("> {}. {text}", idx + 1)).cyan()
|
||||
} else {
|
||||
Line::from(format!(" {}. {}", idx + 1, text))
|
||||
}
|
||||
};
|
||||
column.push(
|
||||
Paragraph::new(guidance.to_string())
|
||||
.wrap(Wrap { trim: true })
|
||||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||||
);
|
||||
column.push("");
|
||||
|
||||
let mut options: Vec<(&str, TrustDirectorySelection)> = Vec::new();
|
||||
if self.is_git_repo {
|
||||
lines.push(create_option(
|
||||
0,
|
||||
TrustDirectorySelection::Trust,
|
||||
options.push((
|
||||
"Yes, allow Codex to work in this folder without asking for approval",
|
||||
TrustDirectorySelection::Trust,
|
||||
));
|
||||
lines.push(create_option(
|
||||
1,
|
||||
TrustDirectorySelection::DontTrust,
|
||||
options.push((
|
||||
"No, ask me to approve edits and commands",
|
||||
TrustDirectorySelection::DontTrust,
|
||||
));
|
||||
} else {
|
||||
lines.push(create_option(
|
||||
0,
|
||||
TrustDirectorySelection::Trust,
|
||||
options.push((
|
||||
"Allow Codex to work in this folder without asking for approval",
|
||||
TrustDirectorySelection::Trust,
|
||||
));
|
||||
lines.push(create_option(
|
||||
1,
|
||||
TrustDirectorySelection::DontTrust,
|
||||
options.push((
|
||||
"Require approval of edits and commands",
|
||||
TrustDirectorySelection::DontTrust,
|
||||
));
|
||||
}
|
||||
lines.push("".into());
|
||||
if let Some(error) = &self.error {
|
||||
lines.push(Line::from(format!(" {error}")).fg(Color::Red));
|
||||
lines.push("".into());
|
||||
}
|
||||
// AE: Following styles.md, this should probably be Cyan because it's a user input tip.
|
||||
// But leaving this for a future cleanup.
|
||||
lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
|
||||
|
||||
Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(area, buf);
|
||||
for (idx, (text, selection)) in options.iter().enumerate() {
|
||||
column.push(new_option_row(
|
||||
idx,
|
||||
text.to_string(),
|
||||
self.highlighted == *selection,
|
||||
));
|
||||
}
|
||||
|
||||
column.push("");
|
||||
|
||||
if let Some(error) = &self.error {
|
||||
column.push(
|
||||
Paragraph::new(error.to_string())
|
||||
.red()
|
||||
.wrap(Wrap { trim: true })
|
||||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||||
);
|
||||
column.push("");
|
||||
}
|
||||
|
||||
column.push(
|
||||
Line::from(vec![
|
||||
"Press ".dim(),
|
||||
key_hint::plain(KeyCode::Enter).into(),
|
||||
" to continue".dim(),
|
||||
])
|
||||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||||
);
|
||||
|
||||
column.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_option_row(index: usize, label: String, is_selected: bool) -> Box<dyn Renderable> {
|
||||
let prefix = if is_selected {
|
||||
format!("› {}. ", index + 1)
|
||||
} else {
|
||||
format!(" {}. ", index + 1)
|
||||
};
|
||||
|
||||
let mut style = Style::default();
|
||||
if is_selected {
|
||||
style = style.cyan();
|
||||
}
|
||||
|
||||
let mut row = RowRenderable::new();
|
||||
row.push(prefix.width() as u16, prefix.set_style(style));
|
||||
row.push(
|
||||
u16::MAX,
|
||||
Paragraph::new(label)
|
||||
.style(style)
|
||||
.wrap(Wrap { trim: false }),
|
||||
);
|
||||
|
||||
row.into()
|
||||
}
|
||||
|
||||
impl KeyboardHandler for TrustDirectoryWidget {
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if key_event.kind == KeyEventKind::Release {
|
||||
@@ -121,8 +157,8 @@ impl KeyboardHandler for TrustDirectoryWidget {
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.highlighted = TrustDirectorySelection::DontTrust;
|
||||
}
|
||||
KeyCode::Char('1') => self.handle_trust(),
|
||||
KeyCode::Char('2') => self.handle_dont_trust(),
|
||||
KeyCode::Char('1') | KeyCode::Char('y') => self.handle_trust(),
|
||||
KeyCode::Char('2') | KeyCode::Char('n') => self.handle_dont_trust(),
|
||||
KeyCode::Enter => match self.highlighted {
|
||||
TrustDirectorySelection::Trust => self.handle_trust(),
|
||||
TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
|
||||
@@ -161,12 +197,16 @@ impl TrustDirectoryWidget {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_backend::VT100Backend;
|
||||
|
||||
use super::*;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::Terminal;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
@@ -191,4 +231,23 @@ mod tests {
|
||||
widget.handle_key_event(press);
|
||||
assert_eq!(widget.selection, Some(TrustDirectorySelection::DontTrust));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_snapshot_for_git_repo() {
|
||||
let widget = TrustDirectoryWidget {
|
||||
codex_home: PathBuf::from("."),
|
||||
cwd: PathBuf::from("/workspace/project"),
|
||||
is_git_repo: true,
|
||||
selection: None,
|
||||
highlighted: TrustDirectorySelection::Trust,
|
||||
error: None,
|
||||
};
|
||||
|
||||
let mut terminal = Terminal::new(VT100Backend::new(70, 14)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| (&widget).render_ref(f.area(), f.buffer_mut()))
|
||||
.expect("draw");
|
||||
|
||||
insta::assert_snapshot!(terminal.backend());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user