Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cortex-tui-components/src/dropdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl DropdownState {

/// Select the next item.
pub fn select_next(&mut self) {
if self.items.is_empty() {
if self.items.is_empty() || self.max_visible == 0 {
return;
}
self.selected = (self.selected + 1) % self.items.len();
Expand All @@ -108,7 +108,7 @@ impl DropdownState {

/// Select the previous item.
pub fn select_prev(&mut self) {
if self.items.is_empty() {
if self.items.is_empty() || self.max_visible == 0 {
return;
}
self.selected = if self.selected == 0 {
Expand Down
6 changes: 5 additions & 1 deletion src/cortex-tui-components/src/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,16 @@ impl ScrollState {
///
/// Adjusts offset if necessary to make the item visible.
pub fn ensure_visible(&mut self, index: usize) {
// Guard against zero visible items to prevent underflow
if self.visible == 0 {
return;
}
if index < self.offset {
// Item is above visible area - scroll up
self.offset = index;
} else if index >= self.offset + self.visible {
// Item is below visible area - scroll down
self.offset = index.saturating_sub(self.visible - 1);
self.offset = index.saturating_sub(self.visible.saturating_sub(1));
}
self.clamp_offset();
}
Expand Down
8 changes: 6 additions & 2 deletions src/cortex-tui-components/src/selection_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ impl SelectionList {
&& let Some(reason) = &item.disabled_reason
{
let reason_str = format!(" {}", reason);
let reason_x = x + width - reason_str.len() as u16 - 1;
let reason_x = x.saturating_add(width.saturating_sub(reason_str.len() as u16 + 1));
if reason_x > col + 2 {
buf.set_string(
reason_x,
Expand Down Expand Up @@ -651,7 +651,11 @@ impl SelectionList {

buf.set_string(x + 2, area.y, &display_text, text_style);

let cursor_x = x + 2 + self.search_query.len() as u16;
// Use character count for cursor position, and account for truncation
let query_char_count = self.search_query.chars().count();
let display_char_count = display_text.chars().count();
let cursor_offset = display_char_count.min(query_char_count) as u16;
let cursor_x = x + 2 + cursor_offset;
if cursor_x < area.right().saturating_sub(1) {
buf[(cursor_x, area.y)].set_bg(self.colors.accent);
buf[(cursor_x, area.y)].set_fg(self.colors.void);
Expand Down
5 changes: 3 additions & 2 deletions src/cortex-tui/src/cards/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,9 @@ impl CardView for CommandsCard {

fn desired_height(&self, max_height: u16, _width: u16) -> u16 {
// Base height for list items + search bar + some padding
let command_count = self.commands.len() as u16;
let content_height = command_count + 2; // +2 for search bar and padding
// Use saturating conversion to prevent overflow when count > u16::MAX
let command_count = u16::try_from(self.commands.len()).unwrap_or(u16::MAX);
let content_height = command_count.saturating_add(2); // +2 for search bar and padding

// Clamp between min 5 and max 14, respecting max_height
content_height.clamp(5, 14).min(max_height)
Expand Down
5 changes: 3 additions & 2 deletions src/cortex-tui/src/cards/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ impl CardView for ModelsCard {

fn desired_height(&self, max_height: u16, _width: u16) -> u16 {
// Base height for list items + search bar + some padding
let model_count = self.models.len() as u16;
let content_height = model_count + 2; // +2 for search bar and padding
// Use saturating conversion to prevent overflow when count > u16::MAX
let model_count = u16::try_from(self.models.len()).unwrap_or(u16::MAX);
let content_height = model_count.saturating_add(2); // +2 for search bar and padding

// Clamp between min 5 and max 12, respecting max_height
content_height.clamp(5, 12).min(max_height)
Expand Down
4 changes: 3 additions & 1 deletion src/cortex-tui/src/cards/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ impl CardView for SessionsCard {

fn desired_height(&self, max_height: u16, _width: u16) -> u16 {
// Base height: sessions + header + search bar + padding
let content_height = self.sessions.len() as u16 + 3;
// Use saturating conversion to prevent overflow when count > u16::MAX
let session_count = u16::try_from(self.sessions.len()).unwrap_or(u16::MAX);
let content_height = session_count.saturating_add(3);
let min_height = 5;
let max_desired = 15;
content_height
Expand Down
8 changes: 7 additions & 1 deletion src/cortex-tui/src/interactive/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,13 @@ impl<'a> InteractiveWidget<'a> {
let hints_height = 1;
let border_height = 2;

(items_count as u16) + header_height + search_height + hints_height + border_height
// Use saturating conversion to prevent overflow when items_count exceeds u16::MAX
let items_height = u16::try_from(items_count).unwrap_or(u16::MAX);
items_height
.saturating_add(header_height)
.saturating_add(search_height)
.saturating_add(hints_height)
.saturating_add(border_height)
}
}

Expand Down
19 changes: 14 additions & 5 deletions src/cortex-tui/src/widgets/autocomplete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ impl<'a> AutocompletePopup<'a> {
let item_count = self.state.visible_items().len() as u16;
let height = item_count * ITEM_HEIGHT + 2; // +2 for borders

// Calculate width based on content
// Calculate width based on visible/filtered items only (not all items)
// This prevents the popup from being too wide when the filtered list is smaller
let content_width = self
.state
.items
.visible_items()
.iter()
.map(|item| {
let icon_width = if item.icon != '\0' { 2 } else { 0 };
Expand Down Expand Up @@ -204,11 +205,19 @@ impl Widget for AutocompletePopup<'_> {

let (width, height) = self.calculate_dimensions();

// Position the popup above the input area
// We assume `area` is positioned where the popup should appear
// Position the popup above the input area if there's room, otherwise below
// This prevents the popup from going off-screen at the top
let y = if area.y >= height {
// Enough room above - position popup above the input
area.y.saturating_sub(height)
} else {
// Not enough room above - position popup below the input
area.bottom()
};

let popup_area = Rect {
x: area.x,
y: area.y.saturating_sub(height),
y,
width: width.min(area.width),
height,
};
Expand Down
4 changes: 3 additions & 1 deletion src/cortex-tui/src/widgets/help_browser/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ impl<'a> HelpBrowser<'a> {

/// Renders the content pane.
fn render_content(&self, area: Rect, buf: &mut Buffer) {
let section = self.state.current_section();
let Some(section) = self.state.current_section() else {
return;
};
let mut y = area.y;
let scroll = self.state.content_scroll;
let mut line_idx = 0;
Expand Down
9 changes: 7 additions & 2 deletions src/cortex-tui/src/widgets/help_browser/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,13 @@ impl HelpBrowserState {
}

/// Returns the currently selected section.
pub fn current_section(&self) -> &HelpSection {
&self.sections[self.selected_section]
///
/// Returns `None` if the sections vector is empty.
pub fn current_section(&self) -> Option<&HelpSection> {
if self.sections.is_empty() {
return None;
}
self.sections.get(self.selected_section)
}

/// Handles character input for search.
Expand Down
22 changes: 19 additions & 3 deletions src/cortex-tui/src/widgets/help_browser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ mod tests {
#[test]
fn test_help_browser_state_with_topic() {
let state = HelpBrowserState::new().with_topic(Some("keyboard"));
assert_eq!(state.current_section().id, "keyboard");
assert_eq!(
state.current_section().expect("should have section").id,
"keyboard"
);
}

#[test]
Expand Down Expand Up @@ -220,10 +223,16 @@ mod tests {
#[test]
fn test_current_section() {
let mut state = HelpBrowserState::new();
assert_eq!(state.current_section().id, "getting-started");
assert_eq!(
state.current_section().expect("should have section").id,
"getting-started"
);

state.select_next();
assert_eq!(state.current_section().id, "keyboard");
assert_eq!(
state.current_section().expect("should have section").id,
"keyboard"
);
}

#[test]
Expand All @@ -232,4 +241,11 @@ mod tests {
assert!(!state.sections.is_empty());
assert_eq!(state.selected_section, 0);
}

#[test]
fn test_current_section_empty_sections() {
let mut state = HelpBrowserState::new();
state.sections.clear();
assert!(state.current_section().is_none());
}
}
11 changes: 7 additions & 4 deletions src/cortex-tui/src/widgets/mention_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ impl<'a> MentionPopup<'a> {
let item_count = self.state.visible_results().len() as u16;
let height = (item_count + 2).min(MAX_HEIGHT + 2); // +2 for borders

// Calculate width based on content
// Calculate width based on content (use chars().count() for Unicode support)
let content_width = self
.state
.results()
.iter()
.map(|p| p.to_string_lossy().len())
.map(|p| p.to_string_lossy().chars().count())
.max()
.unwrap_or(20) as u16;

Expand Down Expand Up @@ -195,8 +195,11 @@ impl Widget for MentionPopup<'_> {

let (width, height) = self.calculate_dimensions(area);

// Position the popup
let popup_area = if self.above {
// Position the popup - check if it fits above, otherwise render below
let fits_above = area.y >= height;
let render_above = self.above && fits_above;

let popup_area = if render_above {
Rect::new(area.x, area.y.saturating_sub(height), width, height)
} else {
Rect::new(
Expand Down
19 changes: 11 additions & 8 deletions src/cortex-tui/src/widgets/scrollable_dropdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,12 @@ impl<'a> ScrollableDropdown<'a> {

/// Returns visible items slice.
fn visible_items(&self) -> &[DropdownItem] {
let start = self.scroll_offset;
if self.max_visible == 0 || self.items.is_empty() {
return &[];
}
let start = self.scroll_offset.min(self.items.len());
let end = (start + self.max_visible).min(self.items.len());
&self.items[start..end]
self.items.get(start..end).unwrap_or(&[])
}

/// Renders a single item.
Expand Down Expand Up @@ -459,7 +462,7 @@ pub fn calculate_scroll_offset(
max_visible: usize,
total_items: usize,
) -> usize {
if total_items <= max_visible {
if max_visible == 0 || total_items <= max_visible {
return 0;
}

Expand All @@ -468,7 +471,7 @@ pub fn calculate_scroll_offset(
selected
} else if selected >= current_offset + max_visible {
// Selected item is below visible area - scroll down
selected.saturating_sub(max_visible - 1)
selected.saturating_sub(max_visible.saturating_sub(1))
} else {
// Selected item is visible - no change needed
current_offset
Expand All @@ -482,7 +485,7 @@ pub fn select_prev(
max_visible: usize,
total_items: usize,
) -> (usize, usize) {
if total_items == 0 {
if total_items == 0 || max_visible == 0 {
return (0, 0);
}

Expand Down Expand Up @@ -511,7 +514,7 @@ pub fn select_next(
max_visible: usize,
total_items: usize,
) -> (usize, usize) {
if total_items == 0 {
if total_items == 0 || max_visible == 0 {
return (0, 0);
}

Expand All @@ -521,8 +524,8 @@ pub fn select_next(
// Wrapped to start
0
} else if new_selected >= scroll_offset + max_visible {
// Need to scroll down
new_selected.saturating_sub(max_visible - 1)
// Need to scroll down - use saturating_sub to prevent underflow
new_selected.saturating_sub(max_visible.saturating_sub(1))
} else {
scroll_offset
};
Expand Down