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(tab)?,
359 Self::string_used_space(tab),
360 Self::string_disk_space(&disk_space),
361 Self::string_git_string(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(tab: &Tab) -> Result<String> {
385 let len: u16;
386 let index: u16;
387 if tab.display_mode.is_tree() {
388 index = tab.tree.selected_node().context("no node")?.index() as u16 + 1;
389 len = tab.tree.len() as u16;
390 } else {
391 index = tab.directory.index as u16 + 1;
392 len = tab.directory.len() as u16;
393 }
394 Ok(format!(" {index} / {len} "))
395 }
396
397 fn string_used_space(tab: &Tab) -> String {
398 if tab.visual {
399 "VISUAL".to_owned()
400 } else {
401 format!(" {} ", tab.directory.used_space())
402 }
403 }
404
405 fn string_disk_space(disk_space: &str) -> String {
406 format!(" Avail: {disk_space} ")
407 }
408
409 fn string_git_string(tab: &Tab) -> Result<String> {
410 Ok(format!(" {} ", tab.directory.git_string()?))
411 }
412
413 fn string_sort_kind(tab: &Tab) -> String {
414 format!(" {} ", &tab.settings.sort_kind)
415 }
416
417 fn string_first_row_flags(status: &Status) -> String {
418 let nb_flagged = status.menu.flagged.len();
419 let flag_string = if nb_flagged > 1 { "flags" } else { "flag" };
420 format!(" {nb_flagged} {flag_string} ",)
421 }
422 }
423
424 pub struct PreviewHeader {
427 left: Vec<ClickableString>,
428 right: Vec<ClickableString>,
429 full_width: u16,
430 }
431
432 impl ClickableLine for PreviewHeader {
433 fn left(&self) -> &Vec<ClickableString> {
434 &self.left
435 }
436 fn right(&self) -> &Vec<ClickableString> {
437 &self.right
438 }
439 fn full_width(&self) -> u16 {
440 self.full_width
441 }
442 }
443
444 impl PreviewHeader {
445 pub fn into_default_preview(status: &Status, tab: &Tab, width: u16) -> Self {
446 Self {
447 left: Self::default_preview(status, tab, width),
448 right: vec![],
449 full_width: width,
450 }
451 }
452
453 pub fn new(status: &Status, tab: &Tab, width: u16) -> Self {
454 Self {
455 left: Self::pair_to_clickable(&Self::strings_left(status, tab), width),
456 right: Self::pair_to_clickable(&Self::strings_right(tab), width),
457 full_width: width,
458 }
459 }
460
461 fn pair_to_clickable(pairs: &[(String, Align)], width: u16) -> Vec<ClickableString> {
462 let mut left = 0;
463 let mut right = width;
464 let mut elems = vec![];
465 for (text, align) in pairs.iter() {
466 let pos = if let Align::Left = align { left } else { right };
467 let elem = ClickableString::new(
468 text.to_owned(),
469 align.to_owned(),
470 ActionMap::Nothing,
471 pos,
472 );
473 match align {
474 Align::Left => {
475 left += elem.width();
476 }
477 Align::Right => {
478 right -= elem.width();
479 }
480 }
481 elems.push(elem)
482 }
483 elems
484 }
485
486 fn strings_left(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
487 match &tab.preview {
488 Preview::Text(text_content) => match text_content.kind {
489 TextKind::CommandStdout => Self::make_colored_text(text_content),
490 TextKind::Help => Self::make_help(),
491 TextKind::Log => Self::make_log(),
492 _ => Self::make_default_preview(status, tab),
493 },
494 _ => Self::make_default_preview(status, tab),
495 }
496 }
497
498 fn strings_right(tab: &Tab) -> Vec<(String, Align)> {
499 let index = match &tab.preview {
500 Preview::Empty => 0,
501 Preview::Image(image) => image.index + 1,
502 _ => tab.window.bottom,
503 };
504 vec![(
505 format!(" {index} / {len} ", len = tab.preview.len()),
506 Align::Right,
507 )]
508 }
509
510 fn make_help() -> Vec<(String, Align)> {
511 vec![
512 (HELP_FIRST_SENTENCE.to_owned(), Align::Left),
513 (
514 format!(" Version: {v} ", v = std::env!("CARGO_PKG_VERSION")),
515 Align::Left,
516 ),
517 (HELP_SECOND_SENTENCE.to_owned(), Align::Right),
518 ]
519 }
520
521 fn make_log() -> Vec<(String, Align)> {
522 vec![
523 (LOG_FIRST_SENTENCE.to_owned(), Align::Left),
524 (ACTION_LOG_PATH.to_owned(), Align::Left),
525 (LOG_SECOND_SENTENCE.to_owned(), Align::Right),
526 ]
527 }
528
529 fn make_colored_text(colored_text: &Text) -> Vec<(String, Align)> {
530 vec![
531 (" Command output: ".to_owned(), Align::Left),
532 (
533 format!(" {command} ", command = colored_text.title),
534 Align::Right,
535 ),
536 ]
537 }
538
539 fn pick_previewed_fileinfo(status: &Status) -> Arc<Path> {
540 if status.session.dual() && status.session.preview() {
541 status.tabs[1].preview.filepath()
542 } else {
543 status.current_tab().preview.filepath()
544 }
545 }
546
547 fn make_default_preview(status: &Status, tab: &Tab) -> Vec<(String, Align)> {
548 vec![
549 (
550 format!(" Preview as {kind} ", kind = tab.preview.kind_display()),
551 Align::Left,
552 ),
553 (
554 format!(
555 " {filepath} ",
556 filepath = Self::pick_previewed_fileinfo(status).display()
557 ),
558 Align::Left,
559 ),
560 ]
561 }
562
563 pub fn default_preview(status: &Status, tab: &Tab, width: u16) -> Vec<ClickableString> {
565 Self::pair_to_clickable(&Self::make_default_preview(status, tab), width)
566 }
567 }
568}
569
570pub use inner::{ClickableLine, ClickableString, Footer, Header, PreviewHeader};