Skip to content

Commit 64c85f8

Browse files
committed
Manually wrap commit message
- Add focus and selection to commit message
1 parent 079f045 commit 64c85f8

File tree

6 files changed

+226
-25
lines changed

6 files changed

+226
-25
lines changed

Cargo.lock

+11-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ ron = "0.6"
3939
serde = "1.0"
4040
anyhow = "1.0.32"
4141
unicode-width = "0.1"
42+
textwrap = "0.12"
4243

4344
[target.'cfg(not(windows))'.dependencies]
4445
pprof = { version = "0.3", features = ["flamegraph"], optional = true }

src/components/commit_details/details.rs

+180-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::{
22
components::{
33
dialog_paragraph, utils::time_to_string, CommandBlocking,
4-
CommandInfo, Component, DrawableComponent,
4+
CommandInfo, Component, DrawableComponent, ScrollType,
55
},
6-
strings,
6+
keys,
7+
strings::{self, commands, order},
78
ui::style::SharedTheme,
89
};
910
use anyhow::Result;
@@ -13,7 +14,7 @@ use asyncgit::{
1314
};
1415
use crossterm::event::Event;
1516
use itertools::Itertools;
16-
use std::borrow::Cow;
17+
use std::{borrow::Cow, cell::Cell};
1718
use sync::CommitTags;
1819
use tui::{
1920
backend::Backend,
@@ -27,15 +28,21 @@ pub struct DetailsComponent {
2728
data: Option<CommitDetails>,
2829
tags: Vec<String>,
2930
theme: SharedTheme,
31+
focused: bool,
32+
current_size: Cell<(u16, u16)>,
33+
selection: usize,
3034
}
3135

3236
impl DetailsComponent {
3337
///
34-
pub const fn new(theme: SharedTheme) -> Self {
38+
pub const fn new(theme: SharedTheme, focused: bool) -> Self {
3539
Self {
3640
data: None,
3741
tags: Vec::new(),
3842
theme,
43+
focused,
44+
current_size: Cell::new((0, 0)),
45+
selection: 0,
3946
}
4047
}
4148

@@ -59,21 +66,76 @@ impl DetailsComponent {
5966
Ok(())
6067
}
6168

62-
fn get_text_message(&self) -> Vec<Text> {
69+
fn get_number_of_lines(&self, width: usize) -> Option<usize> {
6370
if let Some(ref data) = self.data {
6471
if let Some(ref message) = data.message {
65-
let mut res = vec![Text::Styled(
66-
Cow::from(message.subject.clone()),
67-
self.theme
68-
.text(true, false)
69-
.modifier(Modifier::BOLD),
70-
)];
72+
let wrapped_title =
73+
textwrap::wrap(&message.subject, width);
7174

7275
if let Some(ref body) = message.body {
73-
res.push(Text::Styled(
74-
Cow::from(body),
75-
self.theme.text(true, false),
76-
));
76+
let wrapped_message = textwrap::wrap(body, width);
77+
78+
return Some(
79+
wrapped_title.len() + wrapped_message.len()
80+
- 1,
81+
);
82+
}
83+
84+
return Some(wrapped_title.len());
85+
}
86+
}
87+
88+
None
89+
}
90+
91+
fn get_wrapped_text_message(&self, width: usize) -> Vec<Text> {
92+
if let Some(ref data) = self.data {
93+
if let Some(ref message) = data.message {
94+
let wrapped_title =
95+
textwrap::wrap(&message.subject, width);
96+
97+
let mut res: Vec<Text> = wrapped_title
98+
.iter()
99+
.enumerate()
100+
.map(|(i, line)| {
101+
let line_with_newline = format!("{}\n", line);
102+
103+
Text::Styled(
104+
line_with_newline.into(),
105+
self.theme
106+
.text(
107+
true,
108+
self.focused
109+
&& i == self.selection,
110+
)
111+
.modifier(Modifier::BOLD),
112+
)
113+
})
114+
.collect();
115+
116+
if let Some(ref body) = message.body {
117+
let wrapped_message = textwrap::wrap(body, width);
118+
119+
res.extend(
120+
wrapped_message.iter().enumerate().map(
121+
|(i, line)| {
122+
let line_with_newline =
123+
format!("{}\n", line);
124+
125+
Text::Styled(
126+
line_with_newline.into(),
127+
self.theme
128+
.text(
129+
true,
130+
self.focused
131+
&& i == self
132+
.selection,
133+
)
134+
.modifier(Modifier::BOLD),
135+
)
136+
},
137+
),
138+
);
77139
}
78140

79141
return res;
@@ -181,6 +243,39 @@ impl DetailsComponent {
181243
vec![]
182244
}
183245
}
246+
247+
fn move_selection(
248+
&mut self,
249+
move_type: ScrollType,
250+
) -> Result<bool> {
251+
if self.data.is_some() {
252+
let old = self.selection;
253+
let width = self.current_size.get().0 as usize;
254+
255+
if let Some(number_of_lines) =
256+
self.get_number_of_lines(width)
257+
{
258+
let max = number_of_lines.saturating_sub(1) as usize;
259+
260+
let new_selection = match move_type {
261+
ScrollType::Down => old.saturating_add(1),
262+
ScrollType::Up => old.saturating_sub(1),
263+
ScrollType::Home => 0,
264+
ScrollType::End => max,
265+
_ => old,
266+
};
267+
268+
if new_selection > max {
269+
return Ok(false);
270+
}
271+
272+
self.selection = new_selection;
273+
274+
return Ok(true);
275+
}
276+
}
277+
Ok(false)
278+
}
184279
}
185280

186281
impl DrawableComponent for DetailsComponent {
@@ -189,6 +284,15 @@ impl DrawableComponent for DetailsComponent {
189284
f: &mut Frame<B>,
190285
rect: Rect,
191286
) -> Result<()> {
287+
// We have to take the border into account which is one character on
288+
// each side.
289+
let border_width = 2;
290+
291+
self.current_size.set((
292+
rect.width.saturating_sub(border_width),
293+
rect.height.saturating_sub(border_width),
294+
));
295+
192296
let chunks = Layout::default()
193297
.direction(Direction::Vertical)
194298
.constraints(
@@ -206,14 +310,17 @@ impl DrawableComponent for DetailsComponent {
206310
chunks[0],
207311
);
208312

313+
let wrapped_lines = self.get_wrapped_text_message(
314+
self.current_size.get().0 as usize,
315+
);
316+
209317
f.render_widget(
210318
dialog_paragraph(
211319
strings::commit::DETAILS_MESSAGE_TITLE,
212-
self.get_text_message().iter(),
320+
wrapped_lines.iter(),
213321
&self.theme,
214-
false,
215-
)
216-
.wrap(true),
322+
self.focused,
323+
),
217324
chunks[1],
218325
);
219326

@@ -224,14 +331,65 @@ impl DrawableComponent for DetailsComponent {
224331
impl Component for DetailsComponent {
225332
fn commands(
226333
&self,
227-
_out: &mut Vec<CommandInfo>,
228-
_force_all: bool,
334+
out: &mut Vec<CommandInfo>,
335+
force_all: bool,
229336
) -> CommandBlocking {
230337
// visibility_blocking(self)
338+
339+
let width = self.current_size.get().0 as usize;
340+
let number_of_lines = self.get_number_of_lines(width);
341+
342+
out.push(
343+
CommandInfo::new(
344+
commands::NAVIGATE_COMMIT_MESSAGE,
345+
number_of_lines.is_some(),
346+
self.focused || force_all,
347+
)
348+
.order(order::NAV),
349+
);
350+
231351
CommandBlocking::PassingOn
232352
}
233353

234-
fn event(&mut self, _ev: Event) -> Result<bool> {
354+
fn event(&mut self, event: Event) -> Result<bool> {
355+
if self.focused {
356+
if let Event::Key(e) = event {
357+
return match e {
358+
keys::MOVE_UP => {
359+
self.move_selection(ScrollType::Up)
360+
}
361+
keys::MOVE_DOWN => {
362+
self.move_selection(ScrollType::Down)
363+
}
364+
keys::HOME | keys::SHIFT_UP => {
365+
self.move_selection(ScrollType::Home)
366+
}
367+
keys::END | keys::SHIFT_DOWN => {
368+
self.move_selection(ScrollType::End)
369+
}
370+
_ => Ok(false),
371+
};
372+
}
373+
}
374+
235375
Ok(false)
236376
}
377+
378+
fn focused(&self) -> bool {
379+
self.focused
380+
}
381+
382+
fn focus(&mut self, focus: bool) {
383+
if focus {
384+
let width = self.current_size.get().0 as usize;
385+
386+
if let Some(number_of_lines) =
387+
self.get_number_of_lines(width)
388+
{
389+
self.selection = number_of_lines.saturating_sub(1);
390+
}
391+
}
392+
393+
self.focused = focus;
394+
}
237395
}

src/components/commit_details/mod.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use super::{
55
Component, DrawableComponent, FileTreeComponent,
66
};
77
use crate::{
8-
accessors, queue::Queue, strings, ui::style::SharedTheme,
8+
accessors, keys, queue::Queue, strings, ui::style::SharedTheme,
99
};
1010
use anyhow::Result;
1111
use asyncgit::{
@@ -38,7 +38,7 @@ impl CommitDetailsComponent {
3838
theme: SharedTheme,
3939
) -> Self {
4040
Self {
41-
details: DetailsComponent::new(theme.clone()),
41+
details: DetailsComponent::new(theme.clone(), false),
4242
git_commit_files: AsyncCommitFiles::new(sender),
4343
file_tree: FileTreeComponent::new(
4444
"",
@@ -146,6 +146,28 @@ impl Component for CommitDetailsComponent {
146146
return Ok(true);
147147
}
148148

149+
if self.focused() {
150+
if let Event::Key(e) = ev {
151+
return match e {
152+
keys::FOCUS_BELOW if (self.details.focused()) => {
153+
self.details.focus(false);
154+
self.file_tree.focus(true);
155+
156+
return Ok(true);
157+
}
158+
keys::FOCUS_ABOVE
159+
if (self.file_tree.focused()) =>
160+
{
161+
self.file_tree.focus(false);
162+
self.details.focus(true);
163+
164+
return Ok(true);
165+
}
166+
_ => Ok(false),
167+
};
168+
}
169+
}
170+
149171
Ok(false)
150172
}
151173

@@ -164,6 +186,7 @@ impl Component for CommitDetailsComponent {
164186
self.details.focused() || self.file_tree.focused()
165187
}
166188
fn focus(&mut self, focus: bool) {
189+
self.details.focus(false);
167190
self.file_tree.focus(focus);
168191
self.file_tree.show_selection(true);
169192
}

src/keys.rs

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub const FOCUS_WORKDIR: KeyEvent = no_mod(KeyCode::Char('w'));
2727
pub const FOCUS_STAGE: KeyEvent = no_mod(KeyCode::Char('s'));
2828
pub const FOCUS_RIGHT: KeyEvent = no_mod(KeyCode::Right);
2929
pub const FOCUS_LEFT: KeyEvent = no_mod(KeyCode::Left);
30+
pub const FOCUS_ABOVE: KeyEvent = no_mod(KeyCode::Up);
31+
pub const FOCUS_BELOW: KeyEvent = no_mod(KeyCode::Down);
3032
pub const EXIT: KeyEvent =
3133
with_mod(KeyCode::Char('c'), KeyModifiers::CONTROL);
3234
pub const EXIT_POPUP: KeyEvent = no_mod(KeyCode::Esc);

src/strings.rs

+7
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ pub mod commands {
8686
CMD_GROUP_GENERAL,
8787
);
8888
///
89+
pub static NAVIGATE_COMMIT_MESSAGE: CommandText =
90+
CommandText::new(
91+
"Nav [\u{2191}\u{2193}]",
92+
"navigate commit message",
93+
CMD_GROUP_GENERAL,
94+
);
95+
///
8996
pub static NAVIGATE_TREE: CommandText = CommandText::new(
9097
"Nav [\u{2190}\u{2191}\u{2192}\u{2193}]",
9198
"navigate tree view",

0 commit comments

Comments
 (0)