Add spacing before composer footer hints (#3469)
<img width="647" height="82" alt="image" src="https://github.com/user-attachments/assets/867eb5d9-3076-4018-846e-260a50408185" />
This commit is contained in:
@@ -95,6 +95,10 @@ enum ActivePopup {
|
|||||||
File(FileSearchPopup),
|
File(FileSearchPopup),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FOOTER_HINT_HEIGHT: u16 = 1;
|
||||||
|
const FOOTER_SPACING_HEIGHT: u16 = 1;
|
||||||
|
const FOOTER_HEIGHT_WITH_HINT: u16 = FOOTER_HINT_HEIGHT + FOOTER_SPACING_HEIGHT;
|
||||||
|
|
||||||
impl ChatComposer {
|
impl ChatComposer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
has_input_focus: bool,
|
has_input_focus: bool,
|
||||||
@@ -134,20 +138,20 @@ impl ChatComposer {
|
|||||||
pub fn desired_height(&self, width: u16) -> u16 {
|
pub fn desired_height(&self, width: u16) -> u16 {
|
||||||
self.textarea.desired_height(width - 1)
|
self.textarea.desired_height(width - 1)
|
||||||
+ match &self.active_popup {
|
+ match &self.active_popup {
|
||||||
ActivePopup::None => 1u16,
|
ActivePopup::None => FOOTER_HEIGHT_WITH_HINT,
|
||||||
ActivePopup::Command(c) => c.calculate_required_height(),
|
ActivePopup::Command(c) => c.calculate_required_height(),
|
||||||
ActivePopup::File(c) => c.calculate_required_height(),
|
ActivePopup::File(c) => c.calculate_required_height(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||||
let popup_height = match &self.active_popup {
|
let popup_constraint = match &self.active_popup {
|
||||||
ActivePopup::Command(popup) => popup.calculate_required_height(),
|
ActivePopup::Command(popup) => Constraint::Max(popup.calculate_required_height()),
|
||||||
ActivePopup::File(popup) => popup.calculate_required_height(),
|
ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()),
|
||||||
ActivePopup::None => 1,
|
ActivePopup::None => Constraint::Max(FOOTER_HEIGHT_WITH_HINT),
|
||||||
};
|
};
|
||||||
let [textarea_rect, _] =
|
let [textarea_rect, _] =
|
||||||
Layout::vertical([Constraint::Min(1), Constraint::Max(popup_height)]).areas(area);
|
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
|
||||||
let mut textarea_rect = textarea_rect;
|
let mut textarea_rect = textarea_rect;
|
||||||
textarea_rect.width = textarea_rect.width.saturating_sub(1);
|
textarea_rect.width = textarea_rect.width.saturating_sub(1);
|
||||||
textarea_rect.x += 1;
|
textarea_rect.x += 1;
|
||||||
@@ -1223,13 +1227,16 @@ impl ChatComposer {
|
|||||||
|
|
||||||
impl WidgetRef for ChatComposer {
|
impl WidgetRef for ChatComposer {
|
||||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
let popup_height = match &self.active_popup {
|
let (popup_constraint, hint_spacing) = match &self.active_popup {
|
||||||
ActivePopup::Command(popup) => popup.calculate_required_height(),
|
ActivePopup::Command(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
|
||||||
ActivePopup::File(popup) => popup.calculate_required_height(),
|
ActivePopup::File(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
|
||||||
ActivePopup::None => 1,
|
ActivePopup::None => (
|
||||||
|
Constraint::Length(FOOTER_HEIGHT_WITH_HINT),
|
||||||
|
FOOTER_SPACING_HEIGHT,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let [textarea_rect, popup_rect] =
|
let [textarea_rect, popup_rect] =
|
||||||
Layout::vertical([Constraint::Min(1), Constraint::Max(popup_height)]).areas(area);
|
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
|
||||||
match &self.active_popup {
|
match &self.active_popup {
|
||||||
ActivePopup::Command(popup) => {
|
ActivePopup::Command(popup) => {
|
||||||
popup.render_ref(popup_rect, buf);
|
popup.render_ref(popup_rect, buf);
|
||||||
@@ -1238,7 +1245,16 @@ impl WidgetRef for ChatComposer {
|
|||||||
popup.render_ref(popup_rect, buf);
|
popup.render_ref(popup_rect, buf);
|
||||||
}
|
}
|
||||||
ActivePopup::None => {
|
ActivePopup::None => {
|
||||||
let bottom_line_rect = popup_rect;
|
let hint_rect = if hint_spacing > 0 {
|
||||||
|
let [_, hint_rect] = Layout::vertical([
|
||||||
|
Constraint::Length(hint_spacing),
|
||||||
|
Constraint::Length(FOOTER_HINT_HEIGHT),
|
||||||
|
])
|
||||||
|
.areas(popup_rect);
|
||||||
|
hint_rect
|
||||||
|
} else {
|
||||||
|
popup_rect
|
||||||
|
};
|
||||||
let mut hint: Vec<Span<'static>> = if self.ctrl_c_quit_hint {
|
let mut hint: Vec<Span<'static>> = if self.ctrl_c_quit_hint {
|
||||||
let ctrl_c_followup = if self.is_task_running {
|
let ctrl_c_followup = if self.is_task_running {
|
||||||
" to interrupt"
|
" to interrupt"
|
||||||
@@ -1309,7 +1325,7 @@ impl WidgetRef for ChatComposer {
|
|||||||
|
|
||||||
Line::from(hint)
|
Line::from(hint)
|
||||||
.style(Style::default().dim())
|
.style(Style::default().dim())
|
||||||
.render_ref(bottom_line_rect, buf);
|
.render_ref(hint_rect, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let border_style = if self.has_focus {
|
let border_style = if self.has_focus {
|
||||||
@@ -1344,6 +1360,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use image::ImageBuffer;
|
use image::ImageBuffer;
|
||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
@@ -1356,6 +1373,60 @@ mod tests {
|
|||||||
use crate::bottom_pane::textarea::TextArea;
|
use crate::bottom_pane::textarea::TextArea;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn footer_hint_row_is_separated_from_composer() {
|
||||||
|
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||||
|
let sender = AppEventSender::new(tx);
|
||||||
|
let composer = ChatComposer::new(
|
||||||
|
true,
|
||||||
|
sender,
|
||||||
|
false,
|
||||||
|
"Ask Codex to do anything".to_string(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let area = Rect::new(0, 0, 40, 6);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
composer.render_ref(area, &mut buf);
|
||||||
|
|
||||||
|
let row_to_string = |y: u16| {
|
||||||
|
let mut row = String::new();
|
||||||
|
for x in 0..area.width {
|
||||||
|
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
|
||||||
|
}
|
||||||
|
row
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hint_row: Option<(u16, String)> = None;
|
||||||
|
for y in 0..area.height {
|
||||||
|
let row = row_to_string(y);
|
||||||
|
if row.contains(" send") {
|
||||||
|
hint_row = Some((y, row));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (hint_row_idx, hint_row_contents) =
|
||||||
|
hint_row.expect("expected footer hint row to be rendered");
|
||||||
|
assert_eq!(
|
||||||
|
hint_row_idx,
|
||||||
|
area.height - 1,
|
||||||
|
"hint row should occupy the bottom line: {hint_row_contents:?}",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
hint_row_idx > 0,
|
||||||
|
"expected a spacing row above the footer hints",
|
||||||
|
);
|
||||||
|
|
||||||
|
let spacing_row = row_to_string(hint_row_idx - 1);
|
||||||
|
assert_eq!(
|
||||||
|
spacing_row.trim(),
|
||||||
|
"",
|
||||||
|
"expected blank spacing row above hints but saw: {spacing_row:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_current_at_token_basic_cases() {
|
fn test_current_at_token_basic_cases() {
|
||||||
let test_cases = vec![
|
let test_cases = vec![
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
" "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
" "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
" "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
" "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ expression: terminal.backend()
|
|||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
"▌ "
|
||||||
"▌ "
|
" "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ source: tui/src/chatwidget/tests.rs
|
|||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
"▌ Ask Codex to do anything "
|
"▌ Ask Codex to do anything "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃"
|
" "
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ expression: visual
|
|||||||
Investigating rendering code (0s • Esc to interrupt)
|
Investigating rendering code (0s • Esc to interrupt)
|
||||||
|
|
||||||
▌Summarize recent commits
|
▌Summarize recent commits
|
||||||
|
|
||||||
⏎ send ⌃J newline ⌃T transcript ⌃C quit
|
⏎ send ⌃J newline ⌃T transcript ⌃C quit
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ expression: terminal.backend()
|
|||||||
" Analyzing (0s • Esc to interrupt) "
|
" Analyzing (0s • Esc to interrupt) "
|
||||||
" "
|
" "
|
||||||
"▌ Ask Codex to do anything "
|
"▌ Ask Codex to do anything "
|
||||||
|
" "
|
||||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||||
" "
|
" "
|
||||||
|
|||||||
Reference in New Issue
Block a user