1mod inner {
2 use anyhow::{Context, Result};
3 use ratatui::{
4 layout::{Alignment, Rect},
5 style::Modifier,
6 text::{Line, Span},
7 widgets::Widget,
8 Frame,
9 };
10
11 use crate::event::ActionMap;
12 use crate::modes::{Content, Display, FilterKind, Preview, Search, Selectable, Text, TextKind};
13 use crate::{
14 app::{Status, Tab},
15 config::MENU_STYLES,
16 };
17 use crate::{
18 common::{
19 PathShortener, UtfWidth, ACTION_LOG_PATH, HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE,
20 LAZYGIT, LOG_FIRST_SENTENCE, LOG_SECOND_SENTENCE, NCDU,
21 },
22 modes::SAME_WINDOW_TOKEN,
23 };
24
25 #[derive(Clone, Copy, Debug)]
27 enum Align {
28 Left,
29 Right,
30 }
31
32 #[derive(Clone, Debug)]
37 pub struct ClickableString {
38 text: String,
39 action: ActionMap,
40 width: u16,
41 left: u16,
42 right: u16,
43 }
44
45 impl ClickableString {
46 fn new(text: String, align: Align, action: ActionMap, col: u16) -> Self {
51 let width = text.utf_width_u16();
52 let (left, right) = match align {
53 Align::Left => (col, col + width),
54 Align::Right => (col.saturating_sub(width + 3), col.saturating_sub(3)),
55 };
56 Self {
57 text,
58 action,
59 width,
60 left,
61 right,
62 }
63 }
64
65 pub fn text(&self) -> &str {
67 self.text.as_str()
68 }
69
70 pub fn width(&self) -> u16 {
71 self.width
72 }
73 }
74
75 trait ToLine<'a> {
76 fn left_to_line(&'a self, effect_reverse: bool) -> Line<'a>;
77 fn right_to_line(&'a self, effect_reverse: bool) -> Line<'a>;
78 }
79
80 impl<'a> ToLine<'a> for &Vec<ClickableString> {
81 fn left_to_line(&'a self, effect_reverse: bool) -> Line<'a> {
82 let left: Vec<_> = std::iter::zip(
83 self.iter(),
84 MENU_STYLES
85 .get()
86 .expect("Menu colors should be set")
87 .palette()
88 .iter()
89 .cycle(),
90 )
91 .map(|(elem, style)| {
92 let mut style = *style;
93 if effect_reverse {
94 style.add_modifier |= Modifier::REVERSED;
95 }
96 Span::styled(elem.text(), style)
97 })
98 .collect();
99 Line::from(left).alignment(Alignment::Left)
100 }
101
102 fn right_to_line(&'a self, effect_reverse: bool) -> Line<'a> {
103 let left: Vec<_> = std::iter::zip(
104 self.iter(),
105 MENU_STYLES
106 .get()
107 .expect("Menu colors should be set")
108 .palette()
109 .iter()
110 .rev()
111 .cycle(),
112 )
113 .map(|(elem, style)| {
114 let mut style = *style;
115 if effect_reverse {
116 style.add_modifier |= Modifier::REVERSED;
117 }
118 Span::styled(elem.text(), style)
119 })
120 .collect();
121 Line::from(left).alignment(Alignment::Right)
122 }
123 }
124
125 pub trait ClickableLine {
127 fn left(&self) -> &Vec<ClickableString>;
129 fn right(&self) -> &Vec<ClickableString>;
130 fn action(&self, col: u16, is_right: bool) -> &ActionMap {
132 let offset = self.offset(is_right);
133 let col = col - offset;
134 for clickable in self.left().iter().chain(self.right().iter()) {
135 if clickable.left <= col && col < clickable.right {
136 return &clickable.action;
137 }
138 }
139
140 crate::log_info!("no action found");
141 &ActionMap::Nothing
142 }
143 fn full_width(&self) -> u16;
145 fn offset(&self, is_right: bool) -> u16 {
149 if is_right {
150 self.full_width() / 2 + 2
151 } else {
152 1
153 }
154 }
155
156 fn draw_left(&self, f: &mut Frame, rect: Rect, effect_reverse: bool) {
158 self.left()
159 .left_to_line(effect_reverse)
160 .render(rect, f.buffer_mut());
161 }
162
163 fn draw_right(&self, f: &mut Frame, rect: Rect, effect_reverse: bool) {
165 self.right()
166 .right_to_line(effect_reverse)
167 .render(rect, f.buffer_mut());
168 }
169 }
170
171 pub struct Header {
173 left: Vec<ClickableString>,
174 right: Vec<ClickableString>,
175 full_width: u16,
176 }
177
178 impl Header {
179 pub fn new(status: &Status, tab: &Tab) -> Result<Self> {
181 let full_width = status.internal_settings.term_size().0;
182 let canvas_width = status.canvas_width()?;
183 let left = Self::make_left(tab, canvas_width)?;
184 let right = Self::make_right(tab, canvas_width)?;
185
186 Ok(Self {
187 left,
188 right,
189 full_width,
190 })
191 }
192
193 fn make_left(tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
194 let mut left = 0;
195 let shorten_path = Self::elem_shorten_path(tab, left)?;
196 left += shorten_path.width();
197
198 let filename = Self::elem_filename(tab, width, left)?;
199
200 Ok(vec![shorten_path, filename])
201 }
202
203 fn make_right(tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
204 let mut right = width;
205 let mut right_elems = vec![];
206
207 if !tab.search.is_empty() {
208 let search = Self::elem_search(&tab.search, right);
209 right -= search.width();
210 right_elems.push(search)
211 }
212
213 let filter_kind = &tab.settings.filter;
214 if !matches!(filter_kind, FilterKind::All) {
215 right_elems.push(Self::elem_filter(filter_kind, right))
216 }
217
218 Ok(right_elems)
219 }
220
221 fn elem_shorten_path(tab: &Tab, left: u16) -> Result<ClickableString> {
222 Ok(ClickableString::new(
223 format!(
224 " {}",
225 PathShortener::path(&tab.directory.path)
226 .context("Couldn't parse path")?
227 .shorten()
228 ),
229 Align::Left,
230 ActionMap::Cd,
231 left,
232 ))
233 }
234
235 fn elem_filename(tab: &Tab, width: u16, left: u16) -> Result<ClickableString> {
236 let text = match tab.display_mode {
237 Display::Tree => Self::elem_tree_filename(tab, width)?,
238 _ => Self::elem_directory_filename(tab),
239 };
240 Ok(ClickableString::new(
241 text,
242 Align::Left,
243 ActionMap::Rename,
244 left,
245 ))
246 }
247
248 fn elem_tree_filename(tab: &Tab, width: u16) -> Result<String> {
249 Ok(format!(
250 "{sep}{rel}",
251 rel = PathShortener::path(tab.tree.selected_path_relative_to_root()?)
252 .context("Couldn't parse path")?
253 .with_size(width as usize / 2)
254 .shorten(),
255 sep = if tab.tree.root_path() == std::path::Path::new("/") {
256 ""
257 } else {
258 "/"
259 }
260 ))
261 }
262
263 fn elem_directory_filename(tab: &Tab) -> String {
264 if tab.directory.is_dotdot_selected() {
265 "".to_owned()
266 } else if let Some(fileinfo) = tab.directory.selected() {
267 fileinfo.filename_without_dot_dotdot()
268 } else {
269 "".to_owned()
270 }
271 }
272
273 fn elem_search(search: &Search, right: u16) -> ClickableString {
274 ClickableString::new(search.to_string(), Align::Right, ActionMap::Search, right)
275 }
276
277 fn elem_filter(filter: &FilterKind, right: u16) -> ClickableString {
278 ClickableString::new(format!(" {filter}"), Align::Right, ActionMap::Filter, right)
279 }
280 }
281
282 static EMPTY_VEC: Vec<ClickableString> = vec![];
283
284 impl ClickableLine for Header {
285 fn left(&self) -> &Vec<ClickableString> {
286 &self.left
287 }
288 fn right(&self) -> &Vec<ClickableString> {
289 &self.right
290 }
291 fn full_width(&self) -> u16 {
292 self.full_width
293 }
294 }
295
296 pub struct Footer {
298 left: Vec<ClickableString>,
299 full_width: u16,
300 }
301
302 impl ClickableLine for Footer {
303 fn left(&self) -> &Vec<ClickableString> {
304 &self.left
305 }
306 fn right(&self) -> &Vec<ClickableString> {
307 &EMPTY_VEC
308 }
309
310 fn full_width(&self) -> u16 {
311 self.full_width
312 }
313 }
314
315 impl Footer {
316 fn footer_actions() -> [ActionMap; 6] {
317 [
318 ActionMap::Nothing, ActionMap::Custom(SAME_WINDOW_TOKEN.to_owned() + " " + NCDU),
320 ActionMap::Sort,
321 ActionMap::Custom(SAME_WINDOW_TOKEN.to_owned() + " " + LAZYGIT),
322 ActionMap::DisplayFlagged,
323 ActionMap::Sort,
324 ]
325 }
326
327 pub fn new(status: &Status, tab: &Tab) -> Result<Self> {
329 let full_width = status.internal_settings.term_size().0;
330 let canvas_width = status.canvas_width()?;
331 let left = Self::make_elems(status, tab, canvas_width)?;
332 Ok(Self { left, full_width })
333 }
334
335 fn make_elems(status: &Status, tab: &Tab, width: u16) -> Result<Vec<ClickableString>> {
336 let disk_space = status.disk_spaces_of_selected();
337 let raw_strings = Self::make_raw_strings(status, tab, disk_space)?;
338 let padded_strings = Self::make_padded_strings(&raw_strings, width);
339 let mut left = 0;
340 let mut elems = vec![];
341 for (index, string) in padded_strings.iter().enumerate() {
342 let elem = ClickableString::new(
343 string.to_owned(),
344 Align::Left,
345 Self::footer_actions()[index].to_owned(),
346 left,
347 );
348 left += elem.width();
349 elems.push(elem)
350 }
351 Ok(elems)
352 }
353
354 fn make_raw_strings(status: &Status, tab: &Tab, disk_space: String) -> Result<Vec<String>> {
355 Ok(vec![
356 Self::string_first_row_position(tab)?,
357 Self::string_used_space(tab),
358 Self::string_disk_space(&disk_space),
359 Self::string_git_string(tab)?,
360 Self::string_first_row_flags(status),
361 Self::string_sort_kind(tab),
362 ])
363 }
364
365 fn make_padded_strings(raw_strings: &[String], total_width: u16) -> Vec<String> {
367 let total_width = total_width as usize;
368 let used_width = raw_strings.iter().map(|s| s.utf_width()).sum();
369 let available_width = total_width.saturating_sub(used_width);
370 let margin_width = available_width / (2 * raw_strings.len());
371 let margin = " ".repeat(margin_width);
372 let mut padded_strings: Vec<String> = raw_strings
373 .iter()
374 .map(|content| format!("{margin}{content}{margin}"))
375 .collect();
376 let rest = total_width
377 .saturating_sub(padded_strings.iter().map(|s| s.utf_width()).sum::<usize>());
378 padded_strings[raw_strings.len().saturating_sub(1)].push_str(&" ".repeat(rest));
379 padded_strings
380 }
381
382 fn string_first_row_position(tab: &Tab) -> Result<String> {
383 let len: u16;
384 let index: u16;
385 if tab.display_mode.is_tree() {
386 index = tab.tree.selected_node().context("no node")?.index() as u16 + 1;
387 len = tab.tree.len() as u16;
388 } else {
389 index = tab.directory.index as u16 + 1;
390 len = tab.directory.len() as u16;
391 }
392 Ok(format!(" {index} / {len} "))
393 }
394
395 fn string_used_space(tab: &Tab) -> String {
396 if tab.visual {
397 "VISUAL".to_owned()
398 } else {
399 format!(" {} ", tab.directory.used_space())
400 }
401 }
402
403 fn string_disk_space(disk_space: &str) -> String {
404 format!(" Avail: {disk_space} ")
405 }
406
407 fn string_git_string(tab: &Tab) -> Result<String> {
408 Ok(format!(" {} ", tab.directory.git_string()?))
409 }
410
411 fn string_sort_kind(tab: &Tab) -> String {
412 format!(" {} ", &tab.settings.sort_kind)
413 }
414
415 fn string_first_row_flags(status: &Status) -> String {
416 let nb_flagged = status.menu.flagged.len();
417 let flag_string = if nb_flagged > 1 { "flags" } else { "flag" };
418 format!(" {nb_flagged} {flag_string} ",)
419 }
420 }
421
422 pub struct PreviewHeader {
425 left: Vec<ClickableString>,
426 right: Vec<ClickableString>,
427 full_width: u16,
428 }
429
430 impl ClickableLine for PreviewHeader {
431 fn left(&self) -> &Vec<ClickableString> {
432 &self.left
433 }
434 fn right(&self) -> &Vec<ClickableString> {
435 &self.right
436 }
437 fn full_width(&self) -> u16 {
438 self.full_width
439 }
440 }
441
442 impl PreviewHeader {
443 pub fn into_default_preview(status: &Status, tab: &Tab, width: u16) -> Self {
444 Self {
445 left: Self::default_preview(status, tab, width),
446 right: vec![],
447 full_width: width,
448 }
449 }
450
451 pub fn new(status: &Status, tab: &Tab, width: u16) -> Self {
452 Self {
453 left: Self::pair_to_clickable(&Self::strings_left(status, tab), width),
454 right: Self::pair_to_clickable(&Self::strings_right(tab), width),
455 full_width: width,
456 }
457 }
458
459 fn pair_to_clickable(pairs: &[(String, Align)], width: u16) -> Vec<ClickableString> {
460 let mut left = 0;
461 let mut right = width;
462 let mut elems = vec![];
463 for (text, align) in pairs.iter() {
464 let pos = if let Align::Left = align { left } else { right };
465 let elem = ClickableString::new(
466 text.to_owned(),
467 align.to_owned(),
468 ActionMap::Nothing,
469 pos,
470 );
471 match align {
472 Align::Left => {
473 left += elem.width();
474 }
475 Align::Right => {
476 right -= elem.width();
477 }
478 }
479 elems.push(elem)
480 }
481 elems
482 }
483
484 fn strings_left(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
485 match &tab.preview {
486 Preview::Text(text_content) => match text_content.kind {
487 TextKind::CommandStdout => Self::make_colored_text(text_content),
488 TextKind::Help => Self::make_help(),
489 TextKind::Log => Self::make_log(),
490 _ => Self::make_default_preview(status, tab),
491 },
492 _ => Self::make_default_preview(status, tab),
493 }
494 }
495
496 fn strings_right(tab: &Tab) -> Vec<(String, Align)> {
497 let index = match &tab.preview {
498 Preview::Empty => 0,
499 Preview::Image(image) => image.index + 1,
500 _ => tab.window.bottom,
501 };
502 vec![(
503 format!(" {index} / {len} ", len = tab.preview.len()),
504 Align::Right,
505 )]
506 }
507
508 fn make_help() -> Vec<(String, Align)> {
509 vec![
510 (HELP_FIRST_SENTENCE.to_owned(), Align::Left),
511 (
512 format!(" Version: {v} ", v = std::env!("CARGO_PKG_VERSION")),
513 Align::Left,
514 ),
515 (HELP_SECOND_SENTENCE.to_owned(), Align::Right),
516 ]
517 }
518
519 fn make_log() -> Vec<(String, Align)> {
520 vec![
521 (LOG_FIRST_SENTENCE.to_owned(), Align::Left),
522 (ACTION_LOG_PATH.to_owned(), Align::Left),
523 (LOG_SECOND_SENTENCE.to_owned(), Align::Right),
524 ]
525 }
526
527 fn make_colored_text(colored_text: &Text) -> Vec<(String, Align)> {
528 vec![
529 (" Command output: ".to_owned(), Align::Left),
530 (
531 format!(" {command} ", command = colored_text.title),
532 Align::Right,
533 ),
534 ]
535 }
536
537 fn pick_previewed_fileinfo(status: &Status) -> String {
538 if status.session.dual() && status.session.preview() {
539 status.tabs[1].preview.filepath()
540 } else {
541 status.current_tab().preview.filepath()
542 }
543 }
544
545 fn make_default_preview(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
546 vec![
547 (
548 format!(" Preview as {kind} ", kind = tab.preview.kind_display()),
549 Align::Left,
550 ),
551 (
552 format!(
553 " {filepath} ",
554 filepath = Self::pick_previewed_fileinfo(status)
555 ),
556 Align::Left,
557 ),
558 ]
559 }
560
561 pub fn default_preview(status: &Status, tab: &Tab, width: u16) -> Vec<ClickableString> {
563 Self::pair_to_clickable(&Self::make_default_preview(status, tab), width)
564 }
565 }
566}
567
568pub use inner::{ClickableLine, ClickableString, Footer, Header, PreviewHeader};