mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
add ability to disable input temporarily in the TUI. (#8876)
We will disable input while the elevated sandbox setup is running.
This commit is contained in:
@@ -110,6 +110,9 @@ pub(crate) struct ChatComposer {
|
||||
attached_images: Vec<AttachedImage>,
|
||||
placeholder_text: String,
|
||||
is_task_running: bool,
|
||||
/// When false, the composer is temporarily read-only (e.g. during sandbox setup).
|
||||
input_enabled: bool,
|
||||
input_disabled_placeholder: Option<String>,
|
||||
// Non-bracketed paste burst tracker.
|
||||
paste_burst: PasteBurst,
|
||||
// When true, disables paste-burst logic and inserts characters immediately.
|
||||
@@ -160,6 +163,8 @@ impl ChatComposer {
|
||||
attached_images: Vec::new(),
|
||||
placeholder_text,
|
||||
is_task_running: false,
|
||||
input_enabled: true,
|
||||
input_disabled_placeholder: None,
|
||||
paste_burst: PasteBurst::default(),
|
||||
disable_paste_burst: false,
|
||||
custom_prompts: Vec::new(),
|
||||
@@ -488,6 +493,10 @@ impl ChatComposer {
|
||||
|
||||
/// Handle a key event coming from the main UI.
|
||||
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
|
||||
if !self.input_enabled {
|
||||
return (InputResult::None, false);
|
||||
}
|
||||
|
||||
let result = match &mut self.active_popup {
|
||||
ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event),
|
||||
ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event),
|
||||
@@ -1877,6 +1886,17 @@ impl ChatComposer {
|
||||
self.has_focus = has_focus;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_input_enabled(&mut self, enabled: bool, placeholder: Option<String>) {
|
||||
self.input_enabled = enabled;
|
||||
self.input_disabled_placeholder = if enabled { None } else { placeholder };
|
||||
|
||||
// Avoid leaving interactive popups open while input is blocked.
|
||||
if !enabled && !matches!(self.active_popup, ActivePopup::None) {
|
||||
self.active_popup = ActivePopup::None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_task_running(&mut self, running: bool) {
|
||||
self.is_task_running = running;
|
||||
}
|
||||
@@ -1902,6 +1922,10 @@ impl ChatComposer {
|
||||
|
||||
impl Renderable for ChatComposer {
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
if !self.input_enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let [_, textarea_rect, _] = self.layout_areas(area);
|
||||
let state = *self.textarea_state.borrow();
|
||||
self.textarea.cursor_pos_with_state(textarea_rect, state)
|
||||
@@ -1980,10 +2004,15 @@ impl Renderable for ChatComposer {
|
||||
let style = user_message_style();
|
||||
Block::default().style(style).render_ref(composer_rect, buf);
|
||||
if !textarea_rect.is_empty() {
|
||||
let prompt = if self.input_enabled {
|
||||
"›".bold()
|
||||
} else {
|
||||
"›".dim()
|
||||
};
|
||||
buf.set_span(
|
||||
textarea_rect.x - LIVE_PREFIX_COLS,
|
||||
textarea_rect.y,
|
||||
&"›".bold(),
|
||||
&prompt,
|
||||
textarea_rect.width,
|
||||
);
|
||||
}
|
||||
@@ -1991,7 +2020,15 @@ impl Renderable for ChatComposer {
|
||||
let mut state = self.textarea_state.borrow_mut();
|
||||
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
|
||||
if self.textarea.text().is_empty() {
|
||||
let placeholder = Span::from(self.placeholder_text.as_str()).dim();
|
||||
let text = if self.input_enabled {
|
||||
self.placeholder_text.as_str().to_string()
|
||||
} else {
|
||||
self.input_disabled_placeholder
|
||||
.as_deref()
|
||||
.unwrap_or("Input disabled.")
|
||||
.to_string()
|
||||
};
|
||||
let placeholder = Span::from(text).dim().italic();
|
||||
Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
|
||||
}
|
||||
}
|
||||
@@ -4389,4 +4426,38 @@ mod tests {
|
||||
);
|
||||
assert_eq!(composer.attached_images.len(), 1);
|
||||
}
|
||||
#[test]
|
||||
fn input_disabled_ignores_keypresses_and_hides_cursor() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
composer.set_text_content("hello".to_string());
|
||||
composer.set_input_enabled(false, Some("Input disabled for test.".to_string()));
|
||||
|
||||
let (result, needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(result, InputResult::None);
|
||||
assert!(!needs_redraw);
|
||||
assert_eq!(composer.current_text(), "hello");
|
||||
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 40,
|
||||
height: 5,
|
||||
};
|
||||
assert_eq!(composer.cursor_pos(area), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,16 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_composer_input_enabled(
|
||||
&mut self,
|
||||
enabled: bool,
|
||||
placeholder: Option<String>,
|
||||
) {
|
||||
self.composer.set_input_enabled(enabled, placeholder);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn clear_composer_for_ctrl_c(&mut self) {
|
||||
self.composer.clear_for_ctrl_c();
|
||||
self.request_redraw();
|
||||
|
||||
@@ -113,6 +113,9 @@ pub(crate) struct ChatComposer {
|
||||
attached_images: Vec<AttachedImage>,
|
||||
placeholder_text: String,
|
||||
is_task_running: bool,
|
||||
/// When false, the composer is temporarily read-only (e.g. during sandbox setup).
|
||||
input_enabled: bool,
|
||||
input_disabled_placeholder: Option<String>,
|
||||
// Non-bracketed paste burst tracker.
|
||||
paste_burst: PasteBurst,
|
||||
// When true, disables paste-burst logic and inserts characters immediately.
|
||||
@@ -168,6 +171,8 @@ impl ChatComposer {
|
||||
attached_images: Vec::new(),
|
||||
placeholder_text,
|
||||
is_task_running: false,
|
||||
input_enabled: true,
|
||||
input_disabled_placeholder: None,
|
||||
paste_burst: PasteBurst::default(),
|
||||
disable_paste_burst: false,
|
||||
custom_prompts: Vec::new(),
|
||||
@@ -405,6 +410,10 @@ impl ChatComposer {
|
||||
|
||||
/// Handle a key event coming from the main UI.
|
||||
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
|
||||
if !self.input_enabled {
|
||||
return (InputResult::None, false);
|
||||
}
|
||||
|
||||
let result = match &mut self.active_popup {
|
||||
ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event),
|
||||
ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event),
|
||||
@@ -1819,6 +1828,17 @@ impl ChatComposer {
|
||||
self.has_focus = has_focus;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_input_enabled(&mut self, enabled: bool, placeholder: Option<String>) {
|
||||
self.input_enabled = enabled;
|
||||
self.input_disabled_placeholder = if enabled { None } else { placeholder };
|
||||
|
||||
// Avoid leaving interactive popups open while input is blocked.
|
||||
if !enabled && !matches!(self.active_popup, ActivePopup::None) {
|
||||
self.active_popup = ActivePopup::None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_task_running(&mut self, running: bool) {
|
||||
self.is_task_running = running;
|
||||
}
|
||||
@@ -1844,6 +1864,10 @@ impl ChatComposer {
|
||||
|
||||
impl Renderable for ChatComposer {
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
if !self.input_enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let [_, textarea_rect, _] = self.layout_areas(area);
|
||||
let state = *self.textarea_state.borrow();
|
||||
self.textarea.cursor_pos_with_state(textarea_rect, state)
|
||||
@@ -1922,10 +1946,15 @@ impl Renderable for ChatComposer {
|
||||
let style = user_message_style();
|
||||
Block::default().style(style).render_ref(composer_rect, buf);
|
||||
if !textarea_rect.is_empty() {
|
||||
let prompt = if self.input_enabled {
|
||||
"›".bold()
|
||||
} else {
|
||||
"›".dim()
|
||||
};
|
||||
buf.set_span(
|
||||
textarea_rect.x - LIVE_PREFIX_COLS,
|
||||
textarea_rect.y,
|
||||
&"›".bold(),
|
||||
&prompt,
|
||||
textarea_rect.width,
|
||||
);
|
||||
}
|
||||
@@ -1933,7 +1962,15 @@ impl Renderable for ChatComposer {
|
||||
let mut state = self.textarea_state.borrow_mut();
|
||||
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
|
||||
if self.textarea.text().is_empty() {
|
||||
let placeholder = Span::from(self.placeholder_text.as_str()).dim();
|
||||
let text = if self.input_enabled {
|
||||
self.placeholder_text.as_str().to_string()
|
||||
} else {
|
||||
self.input_disabled_placeholder
|
||||
.as_deref()
|
||||
.unwrap_or("Input disabled.")
|
||||
.to_string()
|
||||
};
|
||||
let placeholder = Span::from(text).dim().italic();
|
||||
Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
|
||||
}
|
||||
}
|
||||
@@ -4108,4 +4145,38 @@ mod tests {
|
||||
"'/zzz' should not activate slash popup because it is not a prefix of any built-in command"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn input_disabled_ignores_keypresses_and_hides_cursor() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
composer.set_text_content("hello".to_string());
|
||||
composer.set_input_enabled(false, Some("Input disabled for test.".to_string()));
|
||||
|
||||
let (result, needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(result, InputResult::None);
|
||||
assert!(!needs_redraw);
|
||||
assert_eq!(composer.current_text(), "hello");
|
||||
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 40,
|
||||
height: 5,
|
||||
};
|
||||
assert_eq!(composer.cursor_pos(area), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +256,16 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_composer_input_enabled(
|
||||
&mut self,
|
||||
enabled: bool,
|
||||
placeholder: Option<String>,
|
||||
) {
|
||||
self.composer.set_input_enabled(enabled, placeholder);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn clear_composer_for_ctrl_c(&mut self) {
|
||||
self.composer.clear_for_ctrl_c();
|
||||
self.request_redraw();
|
||||
|
||||
Reference in New Issue
Block a user