diff --git a/src/list.rs b/src/list.rs index f1a4ab0a..6dacf320 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,4 +1,6 @@ -use crate::theme::*; +use std::usize; + +use crate::{float::floating_window, theme::*}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ego_tree::{tree, NodeId}; use ratatui::{ @@ -25,6 +27,24 @@ pub struct CustomList { /// This is the state asociated with the list widget, used to display the selection in the /// widget list_state: ListState, + /// This stores the preview windows state. If it is None, it will not be displayed. + /// If it is Some, we show it with the content of the selected item + preview_window_state: Option, +} + +/// This struct stores the preview window state +struct PreviewWindowState { + /// The text inside the window + text: Vec, + /// The current line scroll + scroll: usize, +} + +impl PreviewWindowState { + /// Create a new PreviewWindowState + pub fn new(text: Vec) -> Self { + Self { text, scroll: 0 } + } } impl CustomList { @@ -89,6 +109,8 @@ impl CustomList { inner_tree: tree, visit_stack: vec![root_id], list_state: ListState::default().with_selected(Some(0)), + // By default the PreviewWindowState is set to None, so it is not being shown + preview_window_state: None, } } @@ -135,6 +157,33 @@ impl CustomList { // Render it frame.render_stateful_widget(list, area, &mut self.list_state); + + // Draw the preview window if it's active + if let Some(pw_state) = &self.preview_window_state { + // Set the window to be floating + let floating_area = floating_window(area); + + // Draw the preview windows lines + let lines: Vec = pw_state + .text + .iter() + .skip(pw_state.scroll) + .take(floating_area.height as usize) + .map(|line| Line::from(line.as_str())) + .collect(); + + // Create list widget + let list = List::new(lines) + .block( + Block::default() + .borders(Borders::ALL) + .title("Action preview"), + ) + .highlight_style(Style::default().reversed()); + + // Finally render the preview window + frame.render_widget(list, floating_area); + } } /// Handle key events, we are only interested in `Press` and `Repeat` events @@ -145,17 +194,62 @@ impl CustomList { match event.code { // Damm you Up arrow, use vim lol KeyCode::Char('j') | KeyCode::Down => { + // If the preview window is active, scroll down and consume the scroll action, + // so the scroll does not happen in the main window as well + if self.preview_window_state.is_some() { + self.scroll_preview_window_down(); + return None; + } + self.try_scroll_down(); None } KeyCode::Char('k') | KeyCode::Up => { + // If the preview window is active, scroll up and consume the scroll action, + // so the scroll does not happen in the main window as well + if self.preview_window_state.is_some() { + self.scroll_preview_window_up(); + return None; + } + self.try_scroll_up(); None } + // The 'p' key toggles the preview on and off + KeyCode::Char('p') => { + self.toggle_preview_window(); + None + } KeyCode::Enter => self.handle_enter(), _ => None, } } + fn toggle_preview_window(&mut self) { + // If the preview window is active, disable it + if self.preview_window_state.is_some() { + self.preview_window_state = None; + } else { + // If the preview windows is not active, show it + + // Get the selected command + if let Some(selected_command) = self.get_selected_command() { + // If command is a folder, we don't display a preview + if selected_command == "" { + return; + } + + // Reconstruct the line breaks and file formatting after the + // 'include_str!()' call in the node + let lines: Vec = selected_command + .lines() + .map(|line| line.to_string()) + .collect(); + + // Show the preview window with the text lines + self.preview_window_state = Some(PreviewWindowState::new(lines)); + } + } + } fn try_scroll_up(&mut self) { self.list_state .select(Some(self.list_state.selected().unwrap().saturating_sub(1))); @@ -180,6 +274,52 @@ impl CustomList { } } + /// Scroll the preview window down + fn scroll_preview_window_down(&mut self) { + if let Some(pw_state) = &mut self.preview_window_state { + if pw_state.scroll + 1 < pw_state.text.len() { + pw_state.scroll += 1; + } + } + } + + /// Scroll the preview window up + fn scroll_preview_window_up(&mut self) { + if let Some(pw_state) = &mut self.preview_window_state { + if pw_state.scroll > 0 { + pw_state.scroll = pw_state.scroll.saturating_sub(1); + } + } + } + + /// This method return the currently selected command, or None if no command is selected. + /// It was extracted from the 'handle_enter()' + /// + /// This could probably be integrated into the 'handle_enter()' method as to avoid code + /// duplication, but I don't want to make too major changes to the codebase. + fn get_selected_command(&self) -> Option<&'static str> { + let curr = self + .inner_tree + .get(*self.visit_stack.last().unwrap()) + .unwrap(); + let selected = self.list_state.selected().unwrap(); + + // If we are not at the root and the first item is selected, it's the `..` item + if !self.at_root() && selected == 0 { + return None; + } + + for (mut idx, node) in curr.children().enumerate() { + if !self.at_root() { + idx += 1; + } + if idx == selected { + return Some(node.value().command); + } + } + None + } + /// Handles the key. This key can do 3 things: /// - Run a command, if it is the currently selected item, /// - Go up a directory