Skip to content
Merged
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
79 changes: 63 additions & 16 deletions src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ pub struct App {
pub selected_rebase_branch_index: usize,
pub rebase_branch_list_state: ListState,
pub pending_branch_command: Option<StoredCommand>,
pub rebase_branch_search: TextArea<'static>,
// Delete mode chooser
pub archive_mode_index: usize,
// Restart task wizard
Expand Down Expand Up @@ -687,6 +688,7 @@ impl App {
selected_rebase_branch_index: 0,
rebase_branch_list_state: ListState::default(),
pending_branch_command: None,
rebase_branch_search: Self::create_plain_editor(),
archive_mode_index: 0,
restart_wizard: None,
restart_pending: false,
Expand Down Expand Up @@ -1492,6 +1494,7 @@ impl App {
self.rebase_branches = branches;
self.selected_rebase_branch_index = preselect_index;
self.rebase_branch_list_state.select(Some(preselect_index));
self.rebase_branch_search = Self::create_plain_editor();
self.view = View::RebaseBranchPicker;
}
Err(e) => {
Expand Down Expand Up @@ -2533,6 +2536,24 @@ impl App {
.collect()
}

/// Return indices into `self.rebase_branches` that match the current search query.
pub fn rebase_branch_filtered_indices(&self) -> Vec<usize> {
let query: String = self.rebase_branch_search.lines().join("").to_lowercase();
let terms: Vec<&str> = query.split_whitespace().collect();
if terms.is_empty() {
return (0..self.rebase_branches.len()).collect();
}
self.rebase_branches
.iter()
.enumerate()
.filter(|(_, branch)| {
let branch_lower = branch.to_lowercase();
terms.iter().all(|term| branch_lower.contains(term))
})
.map(|(i, _)| i)
.collect()
}

fn handle_archive_event(&mut self, event: Event) -> Result<bool> {
if let Event::Key(key) = event {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
Expand Down Expand Up @@ -3276,32 +3297,58 @@ impl App {
}

match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
KeyCode::Esc => {
self.view = View::Preview;
}
KeyCode::Char('j') | KeyCode::Down => {
if !self.rebase_branches.is_empty() {
self.selected_rebase_branch_index =
(self.selected_rebase_branch_index + 1) % self.rebase_branches.len();
self.rebase_branch_list_state.select(Some(self.selected_rebase_branch_index));
KeyCode::Up | KeyCode::Down => {
let filtered = self.rebase_branch_filtered_indices();
if !filtered.is_empty() {
if key.code == KeyCode::Up {
self.selected_rebase_branch_index =
self.selected_rebase_branch_index.saturating_sub(1);
} else {
self.selected_rebase_branch_index =
(self.selected_rebase_branch_index + 1).min(filtered.len() - 1);
}
}
}
KeyCode::Char('k') | KeyCode::Up => {
if !self.rebase_branches.is_empty() {
KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let filtered = self.rebase_branch_filtered_indices();
if !filtered.is_empty() {
self.selected_rebase_branch_index =
if self.selected_rebase_branch_index == 0 {
self.rebase_branches.len() - 1
} else {
self.selected_rebase_branch_index - 1
};
self.rebase_branch_list_state.select(Some(self.selected_rebase_branch_index));
(self.selected_rebase_branch_index + 1).min(filtered.len() - 1);
}
}
KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.selected_rebase_branch_index =
self.selected_rebase_branch_index.saturating_sub(1);
}
KeyCode::Enter => {
if let Some(branch) = self.rebase_branches.get(self.selected_rebase_branch_index).cloned() {
self.run_branch_command(&branch)?;
let filtered = self.rebase_branch_filtered_indices();
if let Some(&real_idx) = filtered.get(self.selected_rebase_branch_index) {
if let Some(branch) = self.rebase_branches.get(real_idx).cloned() {
self.run_branch_command(&branch)?;
}
}
}
KeyCode::Backspace => {
self.rebase_branch_search.input(Input {
key: Key::Backspace,
ctrl: false,
alt: false,
shift: false,
});
self.selected_rebase_branch_index = 0;
}
KeyCode::Char(c) => {
self.rebase_branch_search.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
self.selected_rebase_branch_index = 0;
}
_ => {}
}
}
Expand Down
112 changes: 78 additions & 34 deletions src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,19 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) {
Span::styled(" close", Style::default().fg(Color::DarkGray)),
]
}
View::RebaseBranchPicker | View::SessionPicker => {
View::RebaseBranchPicker => {
vec![
Span::styled("type", Style::default().fg(Color::LightCyan)),
Span::styled(" to filter ", Style::default().fg(Color::DarkGray)),
Span::styled("↑/↓", Style::default().fg(Color::LightCyan)),
Span::styled(" nav ", Style::default().fg(Color::DarkGray)),
Span::styled("Enter", Style::default().fg(Color::LightGreen)),
Span::styled(" select ", Style::default().fg(Color::DarkGray)),
Span::styled("Esc", Style::default().fg(Color::LightRed)),
Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
]
}
View::SessionPicker => {
vec![
Span::styled("j/k", Style::default().fg(Color::LightCyan)),
Span::styled(" nav ", Style::default().fg(Color::DarkGray)),
Expand Down Expand Up @@ -2141,32 +2153,24 @@ fn draw_rebase_branch_picker(f: &mut Frame, app: &mut App) {
.unwrap_or_else(|| "unknown".to_string());

// Dynamic title and labels based on the pending command
let (picker_title, header_label, list_title) = match app
let (picker_title, header_label) = match app
.pending_branch_command
.as_ref()
.map(|c| c.id.as_str())
{
Some("local-merge") => (
" Merge Branch Picker ",
"Merge task into: ",
" Select branch to merge into (Enter to select, Esc to cancel) ",
),
Some("rebase") => (
" Rebase Branch Picker ",
"Rebase task: ",
" Select branch to rebase onto (Enter to select, Esc to cancel) ",
),
_ => (
" Branch Picker ",
"Task: ",
" Select branch (Enter to select, Esc to cancel) ",
),
Some("local-merge") => (" Merge Branch Picker ", "Merge task into: "),
Some("rebase") => (" Rebase Branch Picker ", "Rebase task: "),
_ => (" Branch Picker ", "Task: "),
};

// Split into header and list
// Split into header, search input, and list
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(5)])
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(5),
])
.split(area);

// Header
Expand All @@ -2192,32 +2196,66 @@ fn draw_rebase_branch_picker(f: &mut Frame, app: &mut App) {
);
f.render_widget(header, chunks[0]);

// Branch list
let items: Vec<ListItem> = app
.rebase_branches
// Search input
let search_block = Block::default()
.title(" Filter ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::LightCyan));
let search_inner = search_block.inner(chunks[1]);
f.render_widget(search_block, chunks[1]);
f.render_widget(&app.rebase_branch_search, search_inner);

// Filtered results
let filtered = app.rebase_branch_filtered_indices();
let query: String = app.rebase_branch_search.lines().join("").to_lowercase();
let terms: Vec<&str> = query.split_whitespace().collect();

// Clamp selection
if !filtered.is_empty() && app.selected_rebase_branch_index >= filtered.len() {
app.selected_rebase_branch_index = filtered.len() - 1;
}

let items: Vec<ListItem> = filtered
.iter()
.enumerate()
.map(|(i, branch)| {
let style = if i == app.selected_rebase_branch_index {
.map(|(i, &real_idx)| {
let branch = &app.rebase_branches[real_idx];
let is_selected = i == app.selected_rebase_branch_index;

let prefix = if is_selected { "▸ " } else { " " };
let prefix_style = if is_selected {
Style::default()
.fg(Color::White)
.bg(Color::Rgb(40, 40, 60))
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
let prefix = if i == app.selected_rebase_branch_index {
"▸ "
} else {
" "
};
ListItem::new(Line::from(vec![
Span::styled(prefix, style),
Span::styled(branch, style),
]))

let mut spans: Vec<Span> = vec![Span::styled(prefix, prefix_style)];

// Branch name with match highlighting
for (seg, is_match) in highlight_segments(branch, &terms) {
let style = if is_match {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else if is_selected {
Style::default()
.fg(Color::White)
.bg(Color::Rgb(40, 40, 60))
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
spans.push(Span::styled(seg.to_string(), style));
}

ListItem::new(Line::from(spans))
})
.collect();

let list_title = format!(" Branches ({}) ", filtered.len());
let list = List::new(items)
.block(
Block::default()
Expand All @@ -2230,7 +2268,13 @@ fn draw_rebase_branch_picker(f: &mut Frame, app: &mut App) {
)
.highlight_style(Style::default());

f.render_stateful_widget(list, chunks[1], &mut app.rebase_branch_list_state);
app.rebase_branch_list_state.select(if filtered.is_empty() {
None
} else {
Some(app.selected_rebase_branch_index)
});

f.render_stateful_widget(list, chunks[2], &mut app.rebase_branch_list_state);
}

fn draw_session_picker(f: &mut Frame, app: &App) {
Expand Down