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
151 changes: 119 additions & 32 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ratatui::{
crossterm::event::{KeyCode, KeyEvent},
layout::{Constraint, Layout, Rect},
style::{Modifier, Style, Stylize},
text::Line,
text::{Line, Span},
widgets::{Block, Borders, Padding, Paragraph},
DefaultTerminal, Frame,
};
Expand All @@ -32,8 +32,17 @@ use crate::{
enum StatusLine {
#[default]
None,
Input(String, Option<u16>, Option<String>),
Input {
message: String,
cursor_position: u16,
metadata: String,
},
Transient(String),
SearchResult {
message: String,
options: String,
matched: bool,
},
NotificationInfo(String),
NotificationSuccess(String),
NotificationWarn(String),
Expand Down Expand Up @@ -159,10 +168,11 @@ impl App<'_> {
match self.ec.recv() {
AppEvent::Key(key) => {
match self.app_status.status_line {
StatusLine::None | StatusLine::Input(_, _, _) => {
StatusLine::None | StatusLine::Input { .. } => {
// do nothing
}
StatusLine::Transient(_)
| StatusLine::SearchResult { .. }
| StatusLine::NotificationInfo(_)
| StatusLine::NotificationSuccess(_)
| StatusLine::NotificationWarn(_) => {
Expand Down Expand Up @@ -197,7 +207,7 @@ impl App<'_> {
self.app_status.numeric_prefix.clear();
}
None => {
if let StatusLine::Input(_, _, _) = self.app_status.status_line {
if let StatusLine::Input { .. } = self.app_status.status_line {
// In input mode, pass all key events to the view
// fixme: currently, the only thing that processes key_event is searching the list,
// so this probably works, but it's not the right process...
Expand Down Expand Up @@ -274,12 +284,23 @@ impl App<'_> {
AppEvent::ClearStatusLine => {
self.clear_status_line();
}
AppEvent::UpdateStatusInput(msg, cursor_pos, msg_r) => {
self.update_status_input(msg, cursor_pos, msg_r);
AppEvent::UpdateStatusInput {
message,
cursor_position,
metadata,
} => {
self.update_status_input(message, cursor_position, metadata);
}
AppEvent::UpdateStatusTransient(msg) => {
self.update_status_transient(msg);
}
AppEvent::UpdateSearchResult {
message,
options,
matched,
} => {
self.update_search_result(message, options, matched);
}
AppEvent::NotifyInfo(msg) => {
self.info_notification(msg);
}
Expand Down Expand Up @@ -349,23 +370,15 @@ impl App<'_> {
.fg(self.ctx.color_theme.status_input_transient_fg)
}
}
StatusLine::Input(msg, _, transient_msg) => {
let msg_w = console::measure_text_width(msg.as_str());
if let Some(t_msg) = transient_msg {
let t_msg_w = console::measure_text_width(t_msg.as_str());
let pad_w =
(area.width as usize).saturating_sub(msg_w + t_msg_w + 2 /* pad */);
Line::from(vec![
msg.as_str().fg(self.ctx.color_theme.status_input_fg),
" ".repeat(pad_w).into(),
t_msg
.as_str()
.fg(self.ctx.color_theme.status_input_transient_fg),
])
} else {
Line::raw(msg).fg(self.ctx.color_theme.status_input_fg)
}
}
StatusLine::Input {
message, metadata, ..
} => status_line_with_metadata(
message,
metadata,
Style::default().fg(self.ctx.color_theme.status_input_fg),
Style::default().fg(self.ctx.color_theme.status_input_transient_fg),
area.width,
),
StatusLine::Transient(msg) => {
let msg_w = console::measure_text_width(msg.as_str());
let pad_w = (area.width as usize).saturating_sub(msg_w + 2 /* pad */);
Expand All @@ -375,6 +388,26 @@ impl App<'_> {
.fg(self.ctx.color_theme.status_input_transient_fg),
])
}
StatusLine::SearchResult {
message,
options,
matched,
} => {
let message_style = if *matched {
Style::default().fg(self.ctx.color_theme.status_info_fg)
} else {
Style::default()
.fg(self.ctx.color_theme.status_warn_fg)
.add_modifier(Modifier::BOLD)
};
status_line_with_metadata(
message,
options,
message_style,
Style::default().fg(self.ctx.color_theme.status_input_transient_fg),
area.width,
)
}
StatusLine::NotificationInfo(msg) => {
Line::raw(msg).fg(self.ctx.color_theme.status_info_fg)
}
Expand All @@ -396,8 +429,11 @@ impl App<'_> {
);
f.render_widget(paragraph, area);

if let StatusLine::Input(_, Some(cursor_pos), _) = &self.app_status.status_line {
let (x, y) = (area.x + cursor_pos + 1, area.y + 1);
if let StatusLine::Input {
cursor_position, ..
} = &self.app_status.status_line
{
let (x, y) = (area.x + cursor_position + 1, area.y + 1);
match &self.ctx.ui_config.common.cursor_type {
CursorType::Native => {
f.set_cursor_position((x, y));
Expand Down Expand Up @@ -689,19 +725,26 @@ impl App<'_> {
self.app_status.status_line = StatusLine::None;
}

fn update_status_input(
&mut self,
msg: String,
cursor_pos: Option<u16>,
transient_msg: Option<String>,
) {
self.app_status.status_line = StatusLine::Input(msg, cursor_pos, transient_msg);
fn update_status_input(&mut self, message: String, cursor_position: u16, metadata: String) {
self.app_status.status_line = StatusLine::Input {
message,
cursor_position,
metadata,
};
}

fn update_status_transient(&mut self, msg: String) {
self.app_status.status_line = StatusLine::Transient(msg);
}

fn update_search_result(&mut self, message: String, options: String, matched: bool) {
self.app_status.status_line = StatusLine::SearchResult {
message,
options,
matched,
};
}

fn info_notification(&mut self, msg: String) {
self.app_status.status_line = StatusLine::NotificationInfo(msg);
}
Expand Down Expand Up @@ -731,6 +774,30 @@ impl App<'_> {
}
}

fn status_line_with_metadata(
message: &str,
metadata: &str,
message_style: Style,
metadata_style: Style,
area_width: u16,
) -> Line<'static> {
let content_width = area_width.saturating_sub(2) as usize;
let message_width = console::measure_text_width(message);
let metadata_width = console::measure_text_width(metadata);
let min_gap_width = 2;

if message_width + min_gap_width + metadata_width > content_width {
return Line::from(Span::styled(message.to_owned(), message_style));
}

let pad_width = content_width - message_width - metadata_width;
Line::from(vec![
Span::styled(message.to_owned(), message_style),
Span::raw(" ".repeat(pad_width)),
Span::styled(metadata.to_owned(), metadata_style),
])
}

fn selected_commit_details(
repository: &Repository,
commit_list_state: &CommitListState,
Expand Down Expand Up @@ -862,4 +929,24 @@ mod tests {
let actual = process_numeric_prefix(numeric_prefix, user_event, dummy_key_event);
assert_eq!(actual, expected);
}

#[test]
fn test_status_line_with_metadata_right_aligns_metadata() {
let line =
status_line_with_metadata("left", "[meta]", Style::default(), Style::default(), 14);

assert_eq!(line.spans.len(), 3);
assert_eq!(line.spans[0].content.as_ref(), "left");
assert_eq!(line.spans[1].content.as_ref(), " ");
assert_eq!(line.spans[2].content.as_ref(), "[meta]");
}

#[test]
fn test_status_line_with_metadata_hides_metadata_when_area_is_too_narrow() {
let line =
status_line_with_metadata("left", "[meta]", Style::default(), Style::default(), 13);

assert_eq!(line.spans.len(), 1);
assert_eq!(line.spans[0].content.as_ref(), "left");
}
}
18 changes: 16 additions & 2 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,27 @@ pub enum AppEvent {
SelectNewerCommit,
SelectOlderCommit,
SelectParentCommit,
CopyToClipboard { name: String, value: String },
CopyToClipboard {
name: String,
value: String,
},
Refresh(RefreshViewContext),
ClearStatusLine,
UpdateStatusInput(String, Option<u16>, Option<String>),
UpdateStatusInput {
message: String,
cursor_position: u16,
metadata: String,
},
UpdateStatusTransient(String),
UpdateSearchResult {
message: String,
options: String,
matched: bool,
},
#[expect(dead_code)]
NotifyInfo(String),
NotifySuccess(String),
#[expect(dead_code)]
NotifyWarn(String),
NotifyError(String),
}
Expand Down
57 changes: 35 additions & 22 deletions src/view/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ impl<'a> ListView<'a> {
self.clear_search_query();
}
UserEvent::IgnoreCaseToggle => {
let message = self.as_mut_list_state().toggle_ignore_case();
self.update_search_status(Some(message));
self.as_mut_list_state().toggle_ignore_case();
self.update_search_status();
}
UserEvent::FuzzyToggle => {
let message = self.as_mut_list_state().toggle_fuzzy();
self.update_search_status(Some(message));
self.as_mut_list_state().toggle_fuzzy();
self.update_search_status();
}
_ => {
self.as_mut_list_state().handle_search_input(key);
self.update_search_status(None);
self.update_search_status();
}
}
return;
Expand Down Expand Up @@ -131,15 +131,15 @@ impl<'a> ListView<'a> {
}
UserEvent::Search => {
self.as_mut_list_state().start_search();
self.update_search_status(None);
self.update_search_status();
}
UserEvent::IgnoreCaseToggle => {
let message = self.as_mut_list_state().toggle_ignore_case();
self.tx.send(AppEvent::UpdateStatusTransient(message));
self.as_mut_list_state().toggle_ignore_case();
self.update_search_options_message();
}
UserEvent::FuzzyToggle => {
let message = self.as_mut_list_state().toggle_fuzzy();
self.tx.send(AppEvent::UpdateStatusTransient(message));
self.as_mut_list_state().toggle_fuzzy();
self.update_search_options_message();
}
UserEvent::UserCommand(n) => {
self.tx.send(AppEvent::OpenUserCommand(n));
Expand Down Expand Up @@ -215,31 +215,44 @@ impl<'a> ListView<'a> {
self.as_list_state().graph_image_ids_sorted()
}

fn update_search_status(&self, transient_message: Option<String>) {
fn update_search_status(&self) {
if let SearchState::Searching { .. } = self.as_list_state().search_state() {
let list_state = self.as_list_state();
if let Some(query) = list_state.search_query_string() {
let cursor_pos = list_state.search_query_cursor_position();
self.tx.send(AppEvent::UpdateStatusInput(
query,
Some(cursor_pos),
transient_message,
));
let cursor_position = list_state.search_query_cursor_position();
let options = list_state.search_options().status_string();
self.tx.send(AppEvent::UpdateStatusInput {
message: query,
cursor_position,
metadata: options,
});
}
}
}

fn update_search_options_message(&self) {
if let SearchState::Applied { .. } = self.as_list_state().search_state() {
self.update_matched_message();
} else {
let options = self.as_list_state().search_options().status_string();
self.tx.send(AppEvent::UpdateStatusTransient(format!(
"Search: {options}"
)));
}
}

fn clear_search_query(&self) {
self.tx.send(AppEvent::ClearStatusLine);
}

fn update_matched_message(&self) {
if let Some((msg, matched)) = self.as_list_state().matched_query_string() {
if matched {
self.tx.send(AppEvent::NotifyInfo(msg));
} else {
self.tx.send(AppEvent::NotifyWarn(msg));
}
let options = self.as_list_state().search_options().status_string();
self.tx.send(AppEvent::UpdateSearchResult {
message: msg,
options,
matched,
});
} else {
self.tx.send(AppEvent::ClearStatusLine);
}
Expand Down
Loading