Detect kitty terminals (#1748)
We want to detect kitty terminals so we can preferentially upgrade their UX without degrading older terminals.
This commit is contained in:
@@ -62,6 +62,8 @@ unicode-segmentation = "1.12.0"
|
|||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
uuid = "1"
|
uuid = "1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.43.1"
|
insta = "1.43.1"
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crossterm::SynchronizedUpdate;
|
|||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use crossterm::event::KeyEventKind;
|
use crossterm::event::KeyEventKind;
|
||||||
|
use crossterm::terminal::supports_keyboard_enhancement;
|
||||||
use ratatui::layout::Offset;
|
use ratatui::layout::Offset;
|
||||||
use ratatui::prelude::Backend;
|
use ratatui::prelude::Backend;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
@@ -61,6 +62,8 @@ pub(crate) struct App<'a> {
|
|||||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
||||||
/// after dismissing the Git-repo warning.
|
/// after dismissing the Git-repo warning.
|
||||||
chat_args: Option<ChatWidgetArgs>,
|
chat_args: Option<ChatWidgetArgs>,
|
||||||
|
|
||||||
|
enhanced_keys_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||||
@@ -70,6 +73,7 @@ struct ChatWidgetArgs {
|
|||||||
config: Config,
|
config: Config,
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
initial_images: Vec<PathBuf>,
|
initial_images: Vec<PathBuf>,
|
||||||
|
enhanced_keys_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App<'_> {
|
impl App<'_> {
|
||||||
@@ -83,6 +87,8 @@ impl App<'_> {
|
|||||||
let app_event_tx = AppEventSender::new(app_event_tx);
|
let app_event_tx = AppEventSender::new(app_event_tx);
|
||||||
let pending_redraw = Arc::new(AtomicBool::new(false));
|
let pending_redraw = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
let enhanced_keys_supported = supports_keyboard_enhancement().unwrap_or(false);
|
||||||
|
|
||||||
// Spawn a dedicated thread for reading the crossterm event loop and
|
// Spawn a dedicated thread for reading the crossterm event loop and
|
||||||
// re-publishing the events as AppEvents, as appropriate.
|
// re-publishing the events as AppEvents, as appropriate.
|
||||||
{
|
{
|
||||||
@@ -135,6 +141,7 @@ impl App<'_> {
|
|||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
initial_images,
|
initial_images,
|
||||||
|
enhanced_keys_supported,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -143,6 +150,7 @@ impl App<'_> {
|
|||||||
app_event_tx.clone(),
|
app_event_tx.clone(),
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
initial_images,
|
initial_images,
|
||||||
|
enhanced_keys_supported,
|
||||||
);
|
);
|
||||||
(
|
(
|
||||||
AppState::Chat {
|
AppState::Chat {
|
||||||
@@ -162,6 +170,7 @@ impl App<'_> {
|
|||||||
file_search,
|
file_search,
|
||||||
pending_redraw,
|
pending_redraw,
|
||||||
chat_args,
|
chat_args,
|
||||||
|
enhanced_keys_supported,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +293,7 @@ impl App<'_> {
|
|||||||
self.app_event_tx.clone(),
|
self.app_event_tx.clone(),
|
||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
self.enhanced_keys_supported,
|
||||||
));
|
));
|
||||||
self.app_state = AppState::Chat { widget: new_widget };
|
self.app_state = AppState::Chat { widget: new_widget };
|
||||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||||
@@ -400,6 +410,7 @@ impl App<'_> {
|
|||||||
AppState::Chat { widget } => widget.desired_height(size.width),
|
AppState::Chat { widget } => widget.desired_height(size.width),
|
||||||
AppState::GitWarning { .. } => 10,
|
AppState::GitWarning { .. } => 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut area = terminal.viewport_area;
|
let mut area = terminal.viewport_area;
|
||||||
area.height = desired_height.min(size.height);
|
area.height = desired_height.min(size.height);
|
||||||
area.width = size.width;
|
area.width = size.width;
|
||||||
@@ -451,6 +462,7 @@ impl App<'_> {
|
|||||||
self.app_event_tx.clone(),
|
self.app_event_tx.clone(),
|
||||||
args.initial_prompt,
|
args.initial_prompt,
|
||||||
args.initial_images,
|
args.initial_images,
|
||||||
|
args.enhanced_keys_supported,
|
||||||
));
|
));
|
||||||
self.app_state = AppState::Chat { widget };
|
self.app_state = AppState::Chat { widget };
|
||||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ mod tests {
|
|||||||
let mut pane = BottomPane::new(super::super::BottomPaneParams {
|
let mut pane = BottomPane::new(super::super::BottomPaneParams {
|
||||||
app_event_tx: AppEventSender::new(tx_raw2),
|
app_event_tx: AppEventSender::new(tx_raw2),
|
||||||
has_input_focus: true,
|
has_input_focus: true,
|
||||||
|
enhanced_keys_supported: false,
|
||||||
});
|
});
|
||||||
assert_eq!(CancellationEvent::Handled, view.on_ctrl_c(&mut pane));
|
assert_eq!(CancellationEvent::Handled, view.on_ctrl_c(&mut pane));
|
||||||
assert!(view.queue.is_empty());
|
assert!(view.queue.is_empty());
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub(crate) struct ChatComposer<'a> {
|
|||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
history: ChatComposerHistory,
|
history: ChatComposerHistory,
|
||||||
ctrl_c_quit_hint: bool,
|
ctrl_c_quit_hint: bool,
|
||||||
|
use_shift_enter_hint: bool,
|
||||||
dismissed_file_popup_token: Option<String>,
|
dismissed_file_popup_token: Option<String>,
|
||||||
current_file_query: Option<String>,
|
current_file_query: Option<String>,
|
||||||
pending_pastes: Vec<(String, String)>,
|
pending_pastes: Vec<(String, String)>,
|
||||||
@@ -54,17 +55,24 @@ enum ActivePopup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChatComposer<'_> {
|
impl ChatComposer<'_> {
|
||||||
pub fn new(has_input_focus: bool, app_event_tx: AppEventSender) -> Self {
|
pub fn new(
|
||||||
|
has_input_focus: bool,
|
||||||
|
app_event_tx: AppEventSender,
|
||||||
|
enhanced_keys_supported: bool,
|
||||||
|
) -> Self {
|
||||||
let mut textarea = TextArea::default();
|
let mut textarea = TextArea::default();
|
||||||
textarea.set_placeholder_text(BASE_PLACEHOLDER_TEXT);
|
textarea.set_placeholder_text(BASE_PLACEHOLDER_TEXT);
|
||||||
textarea.set_cursor_line_style(ratatui::style::Style::default());
|
textarea.set_cursor_line_style(ratatui::style::Style::default());
|
||||||
|
|
||||||
|
let use_shift_enter_hint = enhanced_keys_supported;
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
textarea,
|
textarea,
|
||||||
active_popup: ActivePopup::None,
|
active_popup: ActivePopup::None,
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
history: ChatComposerHistory::new(),
|
history: ChatComposerHistory::new(),
|
||||||
ctrl_c_quit_hint: false,
|
ctrl_c_quit_hint: false,
|
||||||
|
use_shift_enter_hint,
|
||||||
dismissed_file_popup_token: None,
|
dismissed_file_popup_token: None,
|
||||||
current_file_query: None,
|
current_file_query: None,
|
||||||
pending_pastes: Vec::new(),
|
pending_pastes: Vec::new(),
|
||||||
@@ -712,11 +720,16 @@ impl WidgetRef for &ChatComposer<'_> {
|
|||||||
Span::from(" to quit"),
|
Span::from(" to quit"),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
|
let newline_hint_key = if self.use_shift_enter_hint {
|
||||||
|
"Shift+⏎"
|
||||||
|
} else {
|
||||||
|
"Ctrl+J"
|
||||||
|
};
|
||||||
vec![
|
vec![
|
||||||
Span::from(" "),
|
Span::from(" "),
|
||||||
"⏎".set_style(key_hint_style),
|
"⏎".set_style(key_hint_style),
|
||||||
Span::from(" send "),
|
Span::from(" send "),
|
||||||
"Shift+⏎".set_style(key_hint_style),
|
newline_hint_key.set_style(key_hint_style),
|
||||||
Span::from(" newline "),
|
Span::from(" newline "),
|
||||||
"Ctrl+C".set_style(key_hint_style),
|
"Ctrl+C".set_style(key_hint_style),
|
||||||
Span::from(" quit"),
|
Span::from(" quit"),
|
||||||
@@ -890,7 +903,7 @@ mod tests {
|
|||||||
|
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
let sender = AppEventSender::new(tx);
|
let sender = AppEventSender::new(tx);
|
||||||
let mut composer = ChatComposer::new(true, sender);
|
let mut composer = ChatComposer::new(true, sender, false);
|
||||||
|
|
||||||
let needs_redraw = composer.handle_paste("hello".to_string());
|
let needs_redraw = composer.handle_paste("hello".to_string());
|
||||||
assert!(needs_redraw);
|
assert!(needs_redraw);
|
||||||
@@ -913,7 +926,7 @@ mod tests {
|
|||||||
|
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
let sender = AppEventSender::new(tx);
|
let sender = AppEventSender::new(tx);
|
||||||
let mut composer = ChatComposer::new(true, sender);
|
let mut composer = ChatComposer::new(true, sender, false);
|
||||||
|
|
||||||
let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10);
|
let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10);
|
||||||
let needs_redraw = composer.handle_paste(large.clone());
|
let needs_redraw = composer.handle_paste(large.clone());
|
||||||
@@ -942,7 +955,7 @@ mod tests {
|
|||||||
let large = "y".repeat(LARGE_PASTE_CHAR_THRESHOLD + 1);
|
let large = "y".repeat(LARGE_PASTE_CHAR_THRESHOLD + 1);
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
let sender = AppEventSender::new(tx);
|
let sender = AppEventSender::new(tx);
|
||||||
let mut composer = ChatComposer::new(true, sender);
|
let mut composer = ChatComposer::new(true, sender, false);
|
||||||
|
|
||||||
composer.handle_paste(large);
|
composer.handle_paste(large);
|
||||||
assert_eq!(composer.pending_pastes.len(), 1);
|
assert_eq!(composer.pending_pastes.len(), 1);
|
||||||
@@ -978,7 +991,7 @@ mod tests {
|
|||||||
|
|
||||||
for (name, input) in test_cases {
|
for (name, input) in test_cases {
|
||||||
// Create a fresh composer for each test case
|
// Create a fresh composer for each test case
|
||||||
let mut composer = ChatComposer::new(true, sender.clone());
|
let mut composer = ChatComposer::new(true, sender.clone(), false);
|
||||||
|
|
||||||
if let Some(text) = input {
|
if let Some(text) = input {
|
||||||
composer.handle_paste(text);
|
composer.handle_paste(text);
|
||||||
@@ -1015,7 +1028,7 @@ mod tests {
|
|||||||
|
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
let sender = AppEventSender::new(tx);
|
let sender = AppEventSender::new(tx);
|
||||||
let mut composer = ChatComposer::new(true, sender);
|
let mut composer = ChatComposer::new(true, sender, false);
|
||||||
|
|
||||||
// Define test cases: (paste content, is_large)
|
// Define test cases: (paste content, is_large)
|
||||||
let test_cases = [
|
let test_cases = [
|
||||||
@@ -1088,7 +1101,7 @@ mod tests {
|
|||||||
|
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
let sender = AppEventSender::new(tx);
|
let sender = AppEventSender::new(tx);
|
||||||
let mut composer = ChatComposer::new(true, sender);
|
let mut composer = ChatComposer::new(true, sender, false);
|
||||||
|
|
||||||
// Define test cases: (content, is_large)
|
// Define test cases: (content, is_large)
|
||||||
let test_cases = [
|
let test_cases = [
|
||||||
@@ -1161,7 +1174,7 @@ mod tests {
|
|||||||
|
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
let sender = AppEventSender::new(tx);
|
let sender = AppEventSender::new(tx);
|
||||||
let mut composer = ChatComposer::new(true, sender);
|
let mut composer = ChatComposer::new(true, sender, false);
|
||||||
|
|
||||||
// Define test cases: (cursor_position_from_end, expected_pending_count)
|
// Define test cases: (cursor_position_from_end, expected_pending_count)
|
||||||
let test_cases = [
|
let test_cases = [
|
||||||
|
|||||||
@@ -50,12 +50,18 @@ pub(crate) struct BottomPane<'a> {
|
|||||||
pub(crate) struct BottomPaneParams {
|
pub(crate) struct BottomPaneParams {
|
||||||
pub(crate) app_event_tx: AppEventSender,
|
pub(crate) app_event_tx: AppEventSender,
|
||||||
pub(crate) has_input_focus: bool,
|
pub(crate) has_input_focus: bool,
|
||||||
|
pub(crate) enhanced_keys_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BottomPane<'_> {
|
impl BottomPane<'_> {
|
||||||
pub fn new(params: BottomPaneParams) -> Self {
|
pub fn new(params: BottomPaneParams) -> Self {
|
||||||
|
let enhanced_keys_supported = params.enhanced_keys_supported;
|
||||||
Self {
|
Self {
|
||||||
composer: ChatComposer::new(params.has_input_focus, params.app_event_tx.clone()),
|
composer: ChatComposer::new(
|
||||||
|
params.has_input_focus,
|
||||||
|
params.app_event_tx.clone(),
|
||||||
|
enhanced_keys_supported,
|
||||||
|
),
|
||||||
active_view: None,
|
active_view: None,
|
||||||
app_event_tx: params.app_event_tx,
|
app_event_tx: params.app_event_tx,
|
||||||
has_input_focus: params.has_input_focus,
|
has_input_focus: params.has_input_focus,
|
||||||
@@ -298,6 +304,7 @@ mod tests {
|
|||||||
let mut pane = BottomPane::new(BottomPaneParams {
|
let mut pane = BottomPane::new(BottomPaneParams {
|
||||||
app_event_tx: tx,
|
app_event_tx: tx,
|
||||||
has_input_focus: true,
|
has_input_focus: true,
|
||||||
|
enhanced_keys_supported: false,
|
||||||
});
|
});
|
||||||
pane.push_approval_request(exec_request());
|
pane.push_approval_request(exec_request());
|
||||||
assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c());
|
assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c());
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
" ⏎ send Shift+⏎ newline Ctrl+C quit "
|
" ⏎ send Ctrl+J newline Ctrl+C quit "
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
" ⏎ send Shift+⏎ newline Ctrl+C quit "
|
" ⏎ send Ctrl+J newline Ctrl+C quit "
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
" ⏎ send Shift+⏎ newline Ctrl+C quit "
|
" ⏎ send Ctrl+J newline Ctrl+C quit "
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
" ⏎ send Shift+⏎ newline Ctrl+C quit "
|
" ⏎ send Ctrl+J newline Ctrl+C quit "
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
" ⏎ send Shift+⏎ newline Ctrl+C quit "
|
" ⏎ send Ctrl+J newline Ctrl+C quit "
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ impl ChatWidget<'_> {
|
|||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
initial_images: Vec<PathBuf>,
|
initial_images: Vec<PathBuf>,
|
||||||
|
enhanced_keys_supported: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
|
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
|
||||||
|
|
||||||
@@ -140,6 +141,7 @@ impl ChatWidget<'_> {
|
|||||||
bottom_pane: BottomPane::new(BottomPaneParams {
|
bottom_pane: BottomPane::new(BottomPaneParams {
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
has_input_focus: true,
|
has_input_focus: true,
|
||||||
|
enhanced_keys_supported,
|
||||||
}),
|
}),
|
||||||
config,
|
config,
|
||||||
initial_user_message: create_initial_user_message(
|
initial_user_message: create_initial_user_message(
|
||||||
|
|||||||
Reference in New Issue
Block a user