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