Add the hint

This commit is contained in:
Andrii Dokhniak 2024-09-06 23:36:12 +02:00
parent 412806a853
commit ec76dcd99b
6 changed files with 282 additions and 14 deletions

View File

@ -4,10 +4,13 @@ use ratatui::{
Frame, Frame,
}; };
use crate::hint::ShortcutList;
pub trait FloatContent { pub trait FloatContent {
fn draw(&mut self, frame: &mut Frame, area: Rect); fn draw(&mut self, frame: &mut Frame, area: Rect);
fn handle_key_event(&mut self, key: &KeyEvent) -> bool; fn handle_key_event(&mut self, key: &KeyEvent) -> bool;
fn is_finished(&self) -> bool; fn is_finished(&self) -> bool;
fn get_shortcut_list(&self) -> ShortcutList;
} }
pub struct Float { pub struct Float {
@ -69,4 +72,8 @@ impl Float {
_ => self.content.handle_key_event(key), _ => self.content.handle_key_event(key),
} }
} }
pub fn get_shortcut_list(&self) -> ShortcutList {
self.content.get_shortcut_list()
}
} }

View File

@ -1,4 +1,8 @@
use crate::{float::FloatContent, running_command::Command}; use crate::{
float::FloatContent,
hint::{Shortcut, ShortcutList},
running_command::Command,
};
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
@ -103,4 +107,15 @@ impl FloatContent for FloatingText {
fn is_finished(&self) -> bool { fn is_finished(&self) -> bool {
true true
} }
fn get_shortcut_list(&self) -> ShortcutList {
ShortcutList {
scope_name: "Floating text",
hints: vec![
Shortcut::new(vec!["j", "Down"], "Scroll down"),
Shortcut::new(vec!["k", "Up"], "Scroll up"),
Shortcut::new(vec!["Enter", "q"], "Close window"),
],
}
}
} }

161
src/hint.rs Normal file
View File

@ -0,0 +1,161 @@
use ratatui::{
layout::{Margin, Rect},
style::{Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::state::{AppState, Focus};
pub const SHORTCUT_LINES: usize = 2;
pub struct ShortcutList {
pub scope_name: &'static str,
pub hints: Vec<Shortcut>,
}
pub struct Shortcut {
pub key_sequenses: Vec<Span<'static>>,
pub desc: &'static str,
}
pub fn span_vec_len(span_vec: &[Span]) -> usize {
span_vec.iter().rfold(0, |init, s| init + s.width())
}
impl ShortcutList {
pub fn draw(&self, frame: &mut Frame, area: Rect) {
let block = Block::default()
.title(self.scope_name)
.borders(Borders::all());
let inner_area = area.inner(Margin::new(1, 1));
let mut shortcut_list: Vec<Vec<Span>> = self.hints.iter().map(|h| h.to_spans()).collect();
let mut lines = vec![Line::default(); SHORTCUT_LINES];
let mut idx = 0;
while idx < SHORTCUT_LINES - 1 {
let split_idx = shortcut_list
.iter()
.scan(0usize, |total_len, s| {
*total_len += span_vec_len(s);
if *total_len > inner_area.width as usize {
None
} else {
*total_len += 4;
Some(1)
}
})
.count();
let new_shortcut_list = shortcut_list.split_off(split_idx);
let line: Vec<_> = shortcut_list
.into_iter()
.flat_map(|mut s| {
s.push(Span::default().content(" "));
s
})
.collect();
shortcut_list = new_shortcut_list;
lines[idx] = line.into();
idx += 1;
}
lines[idx] = shortcut_list
.into_iter()
.flat_map(|mut s| {
s.push(Span::default().content(" "));
s
})
.collect();
let p = Paragraph::new(lines).block(block);
frame.render_widget(p, area);
}
}
impl Shortcut {
pub fn new(key_sequences: Vec<&'static str>, desc: &'static str) -> Self {
Self {
key_sequenses: key_sequences
.iter()
.map(|s| Span::styled(*s, Style::default().bold()))
.collect(),
desc,
}
}
fn to_spans(&self) -> Vec<Span> {
let mut ret: Vec<_> = self
.key_sequenses
.iter()
.flat_map(|seq| {
[
Span::default().content("["),
seq.clone(),
Span::default().content("] "),
]
})
.collect();
ret.push(Span::styled(self.desc, Style::default().italic()));
ret
}
}
fn get_list_item_shortcut(state: &AppState) -> Shortcut {
if state.selected_item_is_dir() {
Shortcut::new(vec!["l", "Right", "Enter"], "Go to selected dir")
} else {
Shortcut::new(vec!["l", "Right", "Enter"], "Run selected command")
}
}
pub fn draw_shortcuts(state: &AppState, frame: &mut Frame, area: Rect) {
match state.focus {
Focus::Search => ShortcutList {
scope_name: "Search bar",
hints: vec![Shortcut::new(vec!["Enter"], "Finish search")],
},
Focus::List => {
let mut hints = Vec::new();
hints.push(Shortcut::new(vec!["q", "CTRL-c"], "Exit linutil"));
if state.at_root() {
hints.push(Shortcut::new(vec!["h", "Left", "Tab"], "Focus tab list"));
hints.push(get_list_item_shortcut(state));
} else {
if state.selected_item_is_up_dir() {
hints.push(Shortcut::new(
vec!["l", "Right", "Enter", "h", "Left"],
"Go to parrent directory",
));
} else {
hints.push(Shortcut::new(vec!["h", "Left"], "Go to parrent directory"));
hints.push(get_list_item_shortcut(state));
if state.selected_item_is_cmd() {
hints.push(Shortcut::new(vec!["p"], "Enable preview"));
}
}
hints.push(Shortcut::new(vec!["Tab"], "Focus tab list"));
};
hints.push(Shortcut::new(vec!["k", "Up"], "Select item above"));
hints.push(Shortcut::new(vec!["j", "Down"], "Select item below"));
hints.push(Shortcut::new(vec!["t"], "Next theme"));
hints.push(Shortcut::new(vec!["T"], "Previous theme"));
ShortcutList {
scope_name: "Item list",
hints,
}
}
Focus::TabList => ShortcutList {
scope_name: "Tab list",
hints: vec![
Shortcut::new(vec!["q", "CTRL-c"], "Exit linutil"),
Shortcut::new(vec!["l", "Right", "Tab", "Enter"], "Focus action list"),
Shortcut::new(vec!["k", "Up"], "Select item above"),
Shortcut::new(vec!["j", "Down"], "Select item below"),
Shortcut::new(vec!["t"], "Next theme"),
Shortcut::new(vec!["T"], "Previous theme"),
],
},
Focus::FloatingWindow(ref float) => float.get_shortcut_list(),
}
.draw(frame, area);
}

View File

@ -1,6 +1,7 @@
mod filter; mod filter;
mod float; mod float;
mod floating_text; mod floating_text;
mod hint;
mod running_command; mod running_command;
pub mod state; pub mod state;
mod tabs; mod tabs;
@ -71,7 +72,6 @@ fn main() -> std::io::Result<()> {
fn run<B: Backend>(terminal: &mut Terminal<B>, state: &mut AppState) -> io::Result<()> { fn run<B: Backend>(terminal: &mut Terminal<B>, state: &mut AppState) -> io::Result<()> {
loop { loop {
terminal.draw(|frame| state.draw(frame)).unwrap(); terminal.draw(|frame| state.draw(frame)).unwrap();
// Wait for an event // Wait for an event
if !event::poll(Duration::from_millis(10))? { if !event::poll(Duration::from_millis(10))? {
continue; continue;

View File

@ -1,4 +1,7 @@
use crate::float::FloatContent; use crate::{
float::FloatContent,
hint::{Shortcut, ShortcutList},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use oneshot::{channel, Receiver}; use oneshot::{channel, Receiver};
use portable_pty::{ use portable_pty::{
@ -123,6 +126,20 @@ impl FloatContent for RunningCommand {
true true
} }
} }
fn get_shortcut_list(&self) -> ShortcutList {
if self.is_finished() {
ShortcutList {
scope_name: "Finished command",
hints: vec![Shortcut::new(vec!["Enter", "q"], "Close window")],
}
} else {
ShortcutList {
scope_name: "Running command",
hints: vec![Shortcut::new(vec!["CTRL-c"], "Kill the command")],
}
}
}
} }
impl RunningCommand { impl RunningCommand {

View File

@ -2,6 +2,7 @@ use crate::{
filter::{Filter, SearchAction}, filter::{Filter, SearchAction},
float::{Float, FloatContent}, float::{Float, FloatContent},
floating_text::FloatingText, floating_text::FloatingText,
hint::{draw_shortcuts, SHORTCUT_LINES},
running_command::{Command, RunningCommand}, running_command::{Command, RunningCommand},
tabs::{ListNode, Tab}, tabs::{ListNode, Tab},
theme::Theme, theme::Theme,
@ -21,7 +22,7 @@ pub struct AppState {
/// Selected theme /// Selected theme
theme: Theme, theme: Theme,
/// Currently focused area /// Currently focused area
focus: Focus, pub focus: Focus,
/// List of tabs /// List of tabs
tabs: Vec<Tab>, tabs: Vec<Tab>,
/// Current tab /// Current tab
@ -72,13 +73,23 @@ impl AppState {
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(100),
Constraint::Min(2 + SHORTCUT_LINES as u16),
])
.margin(0)
.split(frame.size());
let horizontal = Layout::default() let horizontal = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([ .constraints([
Constraint::Min(longest_tab_display_len as u16 + 5), Constraint::Min(longest_tab_display_len as u16 + 5),
Constraint::Percentage(100), Constraint::Percentage(100),
]) ])
.split(frame.size()); .split(vertical[0]);
let left_chunks = Layout::default() let left_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(1)]) .constraints([Constraint::Length(3), Constraint::Min(1)])
@ -148,6 +159,8 @@ impl AppState {
if let Focus::FloatingWindow(float) = &mut self.focus { if let Focus::FloatingWindow(float) = &mut self.focus {
float.draw(frame, chunks[1]); float.draw(frame, chunks[1]);
} }
draw_shortcuts(self, frame, vertical[1]);
} }
pub fn handle_key(&mut self, key: &KeyEvent) -> bool { pub fn handle_key(&mut self, key: &KeyEvent) -> bool {
match &mut self.focus { match &mut self.focus {
@ -213,7 +226,7 @@ impl AppState {
/// Checks ehther the current tree node is the root node (can we go up the tree or no) /// Checks ehther the current tree node is the root node (can we go up the tree or no)
/// Returns `true` if we can't go up the tree (we are at the tree root) /// Returns `true` if we can't go up the tree (we are at the tree root)
/// else returns `false` /// else returns `false`
fn at_root(&self) -> bool { pub fn at_root(&self) -> bool {
self.visit_stack.len() == 1 self.visit_stack.len() == 1
} }
fn enter_parent_directory(&mut self) { fn enter_parent_directory(&mut self) {
@ -221,13 +234,10 @@ impl AppState {
self.selection.select(Some(0)); self.selection.select(Some(0));
self.update_items(); self.update_items();
} }
fn get_selected_command(&mut self, change_directory: bool) -> Option<Command> { pub fn get_selected_command(&self) -> Option<Command> {
let mut selected_index = self.selection.selected().unwrap_or(0); let mut selected_index = self.selection.selected().unwrap_or(0);
if !self.at_root() && selected_index == 0 { if !self.at_root() && selected_index == 0 {
if change_directory {
self.enter_parent_directory();
}
return None; return None;
} }
if !self.at_root() { if !self.at_root() {
@ -237,25 +247,83 @@ impl AppState {
if let Some(item) = self.filter.item_list().get(selected_index) { if let Some(item) = self.filter.item_list().get(selected_index) {
if !item.has_children { if !item.has_children {
return Some(item.node.command.clone()); return Some(item.node.command.clone());
} else if change_directory { }
}
None
}
pub fn go_to_selected_dir(&mut self) {
let mut selected_index = self.selection.selected().unwrap_or(0);
if !self.at_root() && selected_index == 0 {
self.enter_parent_directory();
return;
}
if !self.at_root() {
selected_index = selected_index.saturating_sub(1);
}
if let Some(item) = self.filter.item_list().get(selected_index) {
if item.has_children {
self.visit_stack.push(item.id); self.visit_stack.push(item.id);
self.selection.select(Some(0)); self.selection.select(Some(0));
self.update_items(); self.update_items();
} }
} }
None }
pub fn selected_item_is_dir(&self) -> bool {
let mut selected_index = self.selection.selected().unwrap_or(0);
if !self.at_root() && selected_index == 0 {
return false;
}
if !self.at_root() {
selected_index = selected_index.saturating_sub(1);
}
if let Some(item) = self.filter.item_list().get(selected_index) {
item.has_children
} else {
false
}
}
pub fn selected_item_is_cmd(&self) -> bool {
let mut selected_index = self.selection.selected().unwrap_or(0);
if !self.at_root() && selected_index == 0 {
return false;
}
if !self.at_root() {
selected_index = selected_index.saturating_sub(1);
}
if let Some(item) = self.filter.item_list().get(selected_index) {
!item.has_children
} else {
false
}
}
pub fn selected_item_is_up_dir(&self) -> bool {
let selected_index = self.selection.selected().unwrap_or(0);
!self.at_root() && selected_index == 0
} }
fn enable_preview(&mut self) { fn enable_preview(&mut self) {
if let Some(command) = self.get_selected_command(false) { if let Some(command) = self.get_selected_command() {
if let Some(preview) = FloatingText::from_command(&command) { if let Some(preview) = FloatingText::from_command(&command) {
self.spawn_float(preview, 80, 80); self.spawn_float(preview, 80, 80);
} }
} }
} }
fn handle_enter(&mut self) { fn handle_enter(&mut self) {
if let Some(cmd) = self.get_selected_command(true) { if let Some(cmd) = self.get_selected_command() {
let command = RunningCommand::new(cmd); let command = RunningCommand::new(cmd);
self.spawn_float(command, 80, 80); self.spawn_float(command, 80, 80);
} else {
self.go_to_selected_dir();
} }
} }
fn spawn_float<T: FloatContent + 'static>(&mut self, float: T, width: u16, height: u16) { fn spawn_float<T: FloatContent + 'static>(&mut self, float: T, width: u16, height: u16) {