History

feat: toggle align mode with a single key

Codex GPT-5 committed 3 months ago

Commit 28fa84d1 parent ba30a933 files · +68 −52Browse files

Unified · Split

3 files
  1. gix-tix/src/app.rs +8 −10
  2. gix-tix/src/lib.rs +2 −7
  3. gix-tix/src/ui.rs +58 −35
gix-tix/src/app.rs+8−10
@@ -81,8 +81,7 @@ pub(crate) enum Action { · 80 unmodified lines
81 ToggleMailmap,81 ToggleMailmap,
82 ToggleSpecialRefs,82 ToggleSpecialRefs,
83 ToggleHidden,83 ToggleHidden,
84 PinMetadata,84 ToggleAlign,
85 UnpinMetadata,
86 Cancel,85 Cancel,
87 Copy,86 Copy,
88 Quit,87 Quit,
@@ -112,7 +111,7 @@ pub(crate) struct App { · 23 unmodified lines
112 pub show_special_refs: bool,111 pub show_special_refs: bool,
113 pub has_hidden_filter: bool,112 pub has_hidden_filter: bool,
114 pub show_hidden: bool,113 pub show_hidden: bool,
115 pub pin_metadata: Option<bool>,114 pub align_metadata: bool,
116 pub horizontal_offset: usize,115 pub horizontal_offset: usize,
117 horizontal_page: usize,116 horizontal_page: usize,
118 horizontal_max: usize,117 horizontal_max: usize,
@@ -136,7 +135,7 @@ impl App { · 17 unmodified lines
136 show_special_refs: false,135 show_special_refs: false,
137 has_hidden_filter: false,136 has_hidden_filter: false,
138 show_hidden: false,137 show_hidden: false,
139 pin_metadata: None,138 align_metadata: true,
140 horizontal_offset: 0,139 horizontal_offset: 0,
141 horizontal_page: 1,140 horizontal_page: 1,
142 horizontal_max: 0,141 horizontal_max: 0,
@@ -232,8 +231,7 @@ impl App { · 89 unmodified lines
232 {231 {
233 return vec![Effect::Reload(!self.show_hidden)];232 return vec![Effect::Reload(!self.show_hidden)];
234 }233 }
235 Action::PinMetadata => self.pin_metadata = Some(true),234 Action::ToggleAlign => self.align_metadata = !self.align_metadata,
236 Action::UnpinMetadata => self.pin_metadata = Some(false),
237 Action::Cancel if self.state == State::Loading => {235 Action::Cancel if self.state == State::Loading => {
238 self.state = State::Cancelling;236 self.state = State::Cancelling;
239 return vec![Effect::Cancel];237 return vec![Effect::Cancel];
@@ -620,16 +618,16 @@ mod tests { · 380 unmodified lines
620 app.update(Action::ToggleTrailers);618 app.update(Action::ToggleTrailers);
621 app.update(Action::ToggleMailmap);619 app.update(Action::ToggleMailmap);
622 app.update(Action::ToggleSpecialRefs);620 app.update(Action::ToggleSpecialRefs);
623 app.update(Action::PinMetadata);621 app.update(Action::ToggleAlign);
624 622
625 assert!(!app.show_committer_date);623 assert!(!app.show_committer_date);
626 assert!(!app.show_author_name);624 assert!(!app.show_author_name);
627 assert!(!app.show_trailers);625 assert!(!app.show_trailers);
628 assert!(!app.use_mailmap);626 assert!(!app.use_mailmap);
629 assert!(app.show_special_refs);627 assert!(app.show_special_refs);
630 assert_eq!(app.pin_metadata, Some(true));628 assert!(!app.align_metadata);
631 app.update(Action::UnpinMetadata);629 app.update(Action::ToggleAlign);
632 assert_eq!(app.pin_metadata, Some(false));630 assert!(app.align_metadata);
633 }631 }
634 632
635 #[test]633 #[test]
gix-tix/src/lib.rs+2−7
@@ -331,8 +331,7 @@ fn action(key: KeyEvent) -> Option<Action> { · 330 unmodified lines
331 KeyCode::Char('m') => Some(Action::ToggleMailmap),331 KeyCode::Char('m') => Some(Action::ToggleMailmap),
332 KeyCode::Char('r') => Some(Action::ToggleSpecialRefs),332 KeyCode::Char('r') => Some(Action::ToggleSpecialRefs),
333 KeyCode::Char('v') => Some(Action::ToggleHidden),333 KeyCode::Char('v') => Some(Action::ToggleHidden),
334 KeyCode::Char('[') => Some(Action::PinMetadata),334 KeyCode::Char('[') => Some(Action::ToggleAlign),
335 KeyCode::Char(']') => Some(Action::UnpinMetadata),
336 KeyCode::Char('y') => Some(Action::Copy),335 KeyCode::Char('y') => Some(Action::Copy),
337 _ => None,336 _ => None,
338 }337 }
@@ -432,11 +431,7 @@ mod tests { · 93 unmodified lines
432 );431 );
433 assert_eq!(432 assert_eq!(
434 action(KeyEvent::new(KeyCode::Char('['), KeyModifiers::NONE)),433 action(KeyEvent::new(KeyCode::Char('['), KeyModifiers::NONE)),
435 Some(Action::PinMetadata)434 Some(Action::ToggleAlign)
436 );
437 assert_eq!(
438 action(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::NONE)),
439 Some(Action::UnpinMetadata)
440 );435 );
441 assert_eq!(436 assert_eq!(
442 action(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),437 action(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
gix-tix/src/ui.rs+58−35
@@ -30,13 +30,12 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 29 unmodified lines
30 .map(|row| row.lane.trim_end().chars().count().saturating_add(1))30 .map(|row| row.lane.trim_end().chars().count().saturating_add(1))
31 .max()31 .max()
32 .unwrap_or_default();32 .unwrap_or_default();
33 let pane_limit = ((body.width as usize) / 3)33 let align_limit = ((body.width as usize) / 3)
34 .saturating_sub(2)34 .saturating_sub(2)
35 .max(1)35 .max(1)
36 .min(content.width as usize);36 .min(content.width as usize);
37 let pane_width = max_lane_width.min(pane_limit);37 let align_width = max_lane_width.min(align_limit);
38 let graph_is_wide = max_lane_width > pane_limit;38 let align_metadata = app.align_metadata;
39 let pin_metadata = app.pin_metadata.unwrap_or(graph_is_wide);
40 let show_committer_date = app.show_committer_date;39 let show_committer_date = app.show_committer_date;
41 let show_author_name = app.show_author_name;40 let show_author_name = app.show_author_name;
42 let show_trailers = app.show_trailers;41 let show_trailers = app.show_trailers;
@@ -62,8 +61,19 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 19 unmodified lines
62 )61 )
63 })62 })
64 .collect();63 .collect();
65 let max_offset = if pin_metadata {64 let graph_max_offset = max_lane_width.saturating_sub(align_width);
66 max_lane_width.saturating_sub(pane_width)65 let metadata_max_offset = if align_metadata {
66 metadata
67 .iter()
68 .map(Line::width)
69 .max()
70 .unwrap_or_default()
71 .saturating_sub((content.width as usize).saturating_sub(align_width))
72 } else {
73 0
74 };
75 let max_offset = if align_metadata {
76 graph_max_offset.saturating_add(metadata_max_offset)
67 } else {77 } else {
68 visible_rows78 visible_rows
69 .iter()79 .iter()
@@ -74,7 +84,9 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 4 unmodified lines
74 .saturating_sub(content.width as usize)84 .saturating_sub(content.width as usize)
75 }85 }
76 .min(u16::MAX as usize);86 .min(u16::MAX as usize);
77 let horizontal_offset = app.horizontal_offset.min(max_offset) as u16;87 let horizontal_offset = app.horizontal_offset.min(max_offset);
88 let graph_offset = horizontal_offset.min(graph_max_offset);
89 let metadata_offset = horizontal_offset.saturating_sub(graph_max_offset);
78 90
79 let visible_rows = &app.rows[start..end];91 let visible_rows = &app.rows[start..end];
80 for (index, (row, metadata)) in visible_rows.iter().zip(metadata).enumerate() {92 for (index, (row, metadata)) in visible_rows.iter().zip(metadata).enumerate() {
@@ -92,41 +104,41 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 11 unmodified lines
92 );104 );
93 105
94 let row_area = Rect::new(content.x, y, content.width, 1);106 let row_area = Rect::new(content.x, y, content.width, 1);
95 if pin_metadata {107 if align_metadata {
96 frame.render_widget(108 frame.render_widget(
97 Paragraph::new(row.lane.as_str())109 Paragraph::new(row.lane.as_str())
98 .style(style)110 .style(style)
99 .scroll((0, horizontal_offset)),111 .scroll((0, graph_offset as u16)),
100 row_area,112 row_area,
101 );113 );
102 color_graph(frame, row_area, &row.lane, horizontal_offset as usize, selected);114 color_graph(frame, row_area, &row.lane, graph_offset, selected);
103 let pane = Rect::new(115 let aligned = Rect::new(
104 content.x.saturating_add(pane_width as u16),116 content.x.saturating_add(align_width as u16),
105 y,117 y,
106 content.width.saturating_sub(pane_width as u16),118 content.width.saturating_sub(align_width as u16),
107 1,119 1,
108 );120 );
109 frame.render_widget(Clear, pane);121 frame.render_widget(Clear, aligned);
110 frame.render_widget(Paragraph::new(metadata), pane);122 frame.render_widget(Paragraph::new(metadata).scroll((0, metadata_offset as u16)), aligned);
111 } else {123 } else {
112 let mut spans = Vec::with_capacity(metadata.spans.len() + 1);124 let mut spans = Vec::with_capacity(metadata.spans.len() + 1);
113 spans.push(Span::styled(row.lane.as_str(), style));125 spans.push(Span::styled(row.lane.as_str(), style));
114 spans.extend(metadata.spans);126 spans.extend(metadata.spans);
115 frame.render_widget(127 frame.render_widget(
116 Paragraph::new(Line::from(spans)).scroll((0, horizontal_offset)),128 Paragraph::new(Line::from(spans)).scroll((0, horizontal_offset as u16)),
117 row_area,129 row_area,
118 );130 );
119 color_graph(frame, row_area, &row.lane, horizontal_offset as usize, selected);131 color_graph(frame, row_area, &row.lane, horizontal_offset, selected);
120 }132 }
121 if selected && body.width > 0 {133 if selected && body.width > 0 {
122 let line_width = if pin_metadata {134 let line_width = if align_metadata {
123 pane_width.saturating_add(metadata_width)135 align_width.saturating_add(metadata_width.saturating_sub(metadata_offset))
124 } else {136 } else {
125 row.lane137 row.lane
126 .chars()138 .chars()
127 .count()139 .count()
128 .saturating_add(metadata_width)140 .saturating_add(metadata_width)
129 .saturating_sub(horizontal_offset as usize)141 .saturating_sub(horizontal_offset)
130 };142 };
131 let marker_x = content143 let marker_x = content
132 .x144 .x
@@ -145,9 +157,10 @@ pub(crate) fn draw(frame: &mut Frame<'_>, app: &mut App, decorations: &Decoratio · 12 unmodified lines
145 State::Cancelled => "cancelled",157 State::Cancelled => "cancelled",
146 };158 };
147 let mut footer_spans = vec![Span::raw(format!(159 let mut footer_spans = vec![Span::raw(format!(
148 "{} commits · {status} · ↑↓/jk move · h/l pan · [ pane · ] natural",160 "{} commits · {status} · ↑↓/jk move · h/l pan",
149 app.rows.len()161 app.rows.len()
150 ))];162 ))];
163 footer_spans.extend([Span::raw(" · "), toggle("[ align", app.align_metadata)]);
151 if app.has_hidden_filter {164 if app.has_hidden_filter {
152 footer_spans.extend([165 footer_spans.extend([
153 Span::raw(" · "),166 Span::raw(" · "),
@@ -533,7 +546,7 @@ mod tests { · 379 unmodified lines
533 546
534 terminal.draw(|frame| super::draw(frame, &mut app, &decorations, &mailmap))?;547 terminal.draw(|frame| super::draw(frame, &mut app, &decorations, &mailmap))?;
535 548
536 let footer_text = "1 commits · complete · ↑↓/jk move · h/l pan · [ pane · ] natural · d date · n name · m mailmap · t trailers · r refs · y copy · q quit";549 let footer_text = "1 commits · complete · ↑↓/jk move · h/l pan · [ align · d date · n name · m mailmap · t trailers · r refs · y copy · q quit";
537 let selected_line = "> ● 0101010 (HEAD) 1970-01-01 mapped author subject";550 let selected_line = "> ● 0101010 (HEAD) 1970-01-01 mapped author subject";
538 let mut expected = Buffer::with_lines([format!("{selected_line:<140}"), format!("{footer_text:<140}")]);551 let mut expected = Buffer::with_lines([format!("{selected_line:<140}"), format!("{footer_text:<140}")]);
539 for x in 0..11 {552 for x in 0..11 {
@@ -780,38 +793,36 @@ mod tests { · 240 unmodified lines
780 committer_time: gix::date::Time::default(),793 committer_time: gix::date::Time::default(),
781 author: author(b"author", b"author@example.com"),794 author: author(b"author", b"author@example.com"),
782 attributions: Box::default(),795 attributions: Box::default(),
783 title: "subject".into(),796 title: format!("{} subject-tail", "a".repeat(50)).into(),
784 }]);797 }]);
785 app.update(Action::Complete);798 app.update(Action::Complete);
786 app.rows[0].lane = format!("{}{}", "A".repeat(40), "B".repeat(40));799 app.rows[0].lane = format!("{}{}", "A".repeat(40), "B".repeat(40));
787 let mut terminal = Terminal::new(TestBackend::new(60, 2))?;800 let mut terminal = Terminal::new(TestBackend::new(60, 2))?;
788 801
789 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;802 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
790 let pane_column = rendered_row(&terminal)803 let aligned_column = rendered_row(&terminal)
791 .find("0101010")804 .find("0101010")
792 .expect("wide graphs automatically pin metadata");805 .expect("alignment keeps metadata visible beside wide graphs");
793 assert!(806 assert!(aligned_column < 60, "alignment keeps metadata within the viewport");
794 pane_column < 60,
795 "wide graphs automatically pin metadata within the viewport"
796 );
797 807
798 app.update(Action::ScrollRight);808 app.update(Action::ScrollRight);
799 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;809 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
800 assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "B");810 assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "B");
801 assert_eq!(811 assert_eq!(
802 rendered_row(&terminal).find("0101010"),812 rendered_row(&terminal).find("0101010"),
803 Some(pane_column),813 Some(aligned_column),
804 "horizontal graph scrolling leaves pinned metadata fixed"814 "horizontal graph scrolling leaves aligned metadata fixed"
805 );815 );
806 816
807 app.update(Action::ScrollLeft);817 app.update(Action::ScrollLeft);
808 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;818 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
809 app.update(Action::UnpinMetadata);819 app.update(Action::ToggleAlign);
810 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;820 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
811 assert!(821 assert!(
812 !rendered_row(&terminal).contains("0101010"),822 !rendered_row(&terminal).contains("0101010"),
813 "] restores natural post-graph placement"823 "[ restores natural post-graph placement"
814 );824 );
825 assert!(footer_is_dim(&terminal, "[ align"), "disabled alignment is dimmed");
815 826
816 app.update(Action::ScrollRight);827 app.update(Action::ScrollRight);
817 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;828 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
@@ -820,13 +831,25 @@ mod tests { · 2 unmodified lines
820 "l pages far enough right to reveal natural metadata"831 "l pages far enough right to reveal natural metadata"
821 );832 );
822 833
834 app.update(Action::ScrollLeft);
823 app.rows[0].lane = "● ".into();835 app.rows[0].lane = "● ".into();
824 app.update(Action::PinMetadata);836 app.update(Action::ToggleAlign);
825 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;837 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
826 assert_eq!(838 assert_eq!(
827 terminal.backend().buffer()[(4, 0)].symbol(),839 terminal.backend().buffer()[(4, 0)].symbol(),
828 "0",840 "0",
829 "the pane starts immediately after the widest visible lane"841 "aligned metadata starts immediately after the widest visible lane"
842 );
843 assert!(
844 !rendered_row(&terminal).contains("subject-tail"),
845 "long aligned metadata starts clipped"
846 );
847
848 app.update(Action::ScrollRight);
849 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
850 assert!(
851 rendered_row(&terminal).contains("subject-tail"),
852 "l reveals clipped aligned metadata after graph panning is exhausted"
830 );853 );
831 Ok(())854 Ok(())
832 }855 }