Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895))
* when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748))
* index-out-of-bounds panic when unstaging lines near the end of a diff ([#2953](https://github.com/gitui-org/gitui/issues/2953))
* restore highlights on find tool ([#3004](https://github.com/gitui-org/gitui/issues/3004))

## [0.28.1] - 2026-03-21

Expand Down
64 changes: 55 additions & 9 deletions src/components/textinput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,34 @@ impl TextInputComponent {
self.embed = true;
}

///
pub const fn enabled(&mut self, enable: bool) {
/// Focus/unfocus the text input visually (style + cursor).
/// Used by popups that share the field with other selectable rows.
pub fn enabled(&mut self, enable: bool) {
self.selected = Some(enable);
self.apply_enabled_style();
}

fn is_enabled(&self) -> bool {
self.selected.unwrap_or(true)
}

fn apply_enabled_style(&mut self) {
let enabled = self.is_enabled();
let Some(ta) = self.textarea.as_mut() else {
return;
};

let style = self.theme.text(enabled, false);
ta.set_style(style);
ta.set_placeholder_style(style);
// Hide the block cursor when another control has focus.
// Default REVERSED cursor is what ratatui-textarea uses when focused.
ta.set_cursor_style(if enabled {
ratatui::style::Style::default()
.add_modifier(ratatui::style::Modifier::REVERSED)
} else {
ratatui::style::Style::default()
});
}

fn show_inner_textarea(&mut self) {
Expand All @@ -156,13 +181,6 @@ impl TextInputComponent {
text_area
.set_cursor_line_style(self.theme.text(true, false));
text_area.set_placeholder_text(self.default_msg.clone());
text_area.set_placeholder_style(
self.theme
.text(self.selected.unwrap_or_default(), false),
);
text_area.set_style(
self.theme.text(self.selected.unwrap_or(true), false),
);

if !self.embed {
text_area.set_block(
Expand All @@ -179,6 +197,8 @@ impl TextInputComponent {
}
text_area
});
// Apply focus style/cursor after the widget exists (and on rebuilds).
self.apply_enabled_style();
}

/// Set the `msg`.
Expand Down Expand Up @@ -887,4 +907,30 @@ mod tests {
assert_eq!(ta.cursor(), save_cursor);
}
}

#[test]
fn test_enabled_toggles_cursor_visibility() {
use ratatui::style::Modifier;

let env = Environment::test_env();
let mut comp = TextInputComponent::new(&env, "", "", false);
comp.show_inner_textarea();
comp.enabled(true);
assert!(comp
.textarea
.as_ref()
.unwrap()
.cursor_style()
.add_modifier
.contains(Modifier::REVERSED));

comp.enabled(false);
assert!(!comp
.textarea
.as_ref()
.unwrap()
.cursor_style()
.add_modifier
.contains(Modifier::REVERSED));
}
}
215 changes: 114 additions & 101 deletions src/popups/log_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ impl LogSearchPopupPopup {
self.mode = PopupMode::JumpCommitSha;
self.jump_commit_id = None;
self.find_text.set_default_msg("commit sha".into());
self.find_text.enabled(false);
// Field stays focused; invalid SHA is shown via block style.
self.find_text.enabled(true);
self.selection = Selection::EnterText;
}
}
Expand Down Expand Up @@ -164,106 +165,66 @@ impl LogSearchPopupPopup {
}
}

fn get_text_options(&self) -> Vec<Line<'_>> {
let x_summary =
if self.options.0.contains(SearchFields::MESSAGE_SUMMARY)
{
"X"
} else {
" "
};

let x_body =
if self.options.0.contains(SearchFields::MESSAGE_BODY) {
"X"
} else {
" "
};

let x_files =
if self.options.0.contains(SearchFields::FILENAMES) {
"X"
} else {
" "
};

let x_authors =
if self.options.0.contains(SearchFields::AUTHORS) {
"X"
} else {
" "
};

let x_opt_fuzzy =
if self.options.1.contains(SearchOptions::FUZZY_SEARCH) {
"X"
} else {
" "
};
const fn option_mark(on: bool) -> &'static str {
if on {
"X"
} else {
" "
}
}

let x_opt_casesensitive =
if self.options.1.contains(SearchOptions::CASE_SENSITIVE)
{
"X"
} else {
" "
};
fn option_line(
&self,
checked: bool,
label: &str,
selected: bool,
) -> Line<'_> {
Line::from(vec![Span::styled(
format!("[{}] {label}", Self::option_mark(checked)),
// enabled=true keeps normal fg; selected applies selection_bg
self.theme.text(true, selected),
)])
}

fn get_text_options(&self) -> Vec<Line<'_>> {
vec![
Line::from(vec![Span::styled(
format!("[{x_opt_fuzzy}] fuzzy search"),
self.theme.text(
matches!(self.selection, Selection::FuzzyOption),
false,
),
)]),
Line::from(vec![Span::styled(
format!("[{x_opt_casesensitive}] case sensitive"),
self.theme.text(
matches!(self.selection, Selection::CaseOption),
false,
),
)]),
Line::from(vec![Span::styled(
format!("[{x_summary}] summary"),
self.theme.text(
matches!(
self.selection,
Selection::SummarySearch
),
false,
),
)]),
Line::from(vec![Span::styled(
format!("[{x_body}] message body"),
self.theme.text(
matches!(
self.selection,
Selection::MessageBodySearch
),
false,
),
)]),
Line::from(vec![Span::styled(
format!("[{x_files}] committed files"),
self.theme.text(
matches!(
self.selection,
Selection::FilenameSearch
),
false,
),
)]),
Line::from(vec![Span::styled(
format!("[{x_authors}] authors"),
self.theme.text(
matches!(
self.selection,
Selection::AuthorsSearch
),
false,
self.option_line(
self.options.1.contains(SearchOptions::FUZZY_SEARCH),
"fuzzy search",
matches!(self.selection, Selection::FuzzyOption),
),
self.option_line(
self.options
.1
.contains(SearchOptions::CASE_SENSITIVE),
"case sensitive",
matches!(self.selection, Selection::CaseOption),
),
self.option_line(
self.options
.0
.contains(SearchFields::MESSAGE_SUMMARY),
"summary",
matches!(self.selection, Selection::SummarySearch),
),
self.option_line(
self.options.0.contains(SearchFields::MESSAGE_BODY),
"message body",
matches!(
self.selection,
Selection::MessageBodySearch
),
)]),
),
self.option_line(
self.options.0.contains(SearchFields::FILENAMES),
"committed files",
matches!(self.selection, Selection::FilenameSearch),
),
self.option_line(
self.options.0.contains(SearchFields::AUTHORS),
"authors",
matches!(self.selection, Selection::AuthorsSearch),
),
]
}

Expand Down Expand Up @@ -315,7 +276,7 @@ impl LogSearchPopupPopup {
}
}

const fn move_selection(&mut self, arg: bool) {
fn move_selection(&mut self, arg: bool) {
if arg {
//up
self.selection = match self.selection {
Expand Down Expand Up @@ -513,9 +474,6 @@ impl LogSearchPopupPopup {
self.execute_confirm();
} else if self.find_text.event(event)?.is_consumed() {
self.validate_commit_sha();
self.find_text.enabled(
!self.find_text.get_text().trim().is_empty(),
);
}
}

Expand Down Expand Up @@ -629,3 +587,58 @@ impl Component for LogSearchPopupPopup {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::app::Environment;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{backend::TestBackend, style::Color, Terminal};

fn key(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}

#[test]
fn search_option_selection_uses_selection_background() {
let env = Environment::test_env();
let mut popup = LogSearchPopupPopup::new(&env);
popup.open().unwrap();

// Move focus from the text field onto the first option.
popup.event(&key(KeyCode::Down)).unwrap();

let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
popup.draw(f, f.area()).unwrap();
})
.unwrap();

let buf = terminal.backend().buffer();
// Find the "fuzzy search" row and assert it has selection_bg.
let mut found_selected = false;
let area = *buf.area();
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
let cell = &buf[(x, y)];
if cell.symbol() == "f"
&& cell.fg == Color::White
&& cell.bg == Color::Blue
{
// Selection highlight: command_fg on selection_bg
found_selected = true;
break;
}
}
if found_selected {
break;
}
}
assert!(
found_selected,
"focused search option should use selection background"
);
}
}