diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs index 28732e35..8ce0d88c 100644 --- a/tui/src/confirmation.rs +++ b/tui/src/confirmation.rs @@ -3,11 +3,7 @@ use std::borrow::Cow; use crate::{float::FloatContent, hint::Shortcut}; use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - layout::Alignment, - prelude::*, - widgets::{Block, Borders, Clear, List}, -}; +use ratatui::{prelude::*, widgets::List}; pub enum ConfirmStatus { Confirm, @@ -57,19 +53,15 @@ impl ConfirmPrompt { } impl FloatContent for ConfirmPrompt { + fn top_title(&self) -> Option> { + Some(Line::from(" Confirm selections ").style(Style::default().bold())) + } + + fn bottom_title(&self) -> Option> { + Some(Line::from(" [y] to continue, [n] to abort ").italic()) + } + fn draw(&mut self, frame: &mut Frame, area: Rect) { - let block = Block::default() - .borders(Borders::ALL) - .title(" Confirm selections ") - .title_bottom(" [y] to continue, [n] to abort ") - .title_alignment(Alignment::Center) - .title_style(Style::default().bold()) - .style(Style::default()); - - frame.render_widget(block.clone(), area); - - let inner_area = block.inner(area); - let paths_text = self .names .iter() @@ -80,8 +72,7 @@ impl FloatContent for ConfirmPrompt { }) .collect::(); - frame.render_widget(Clear, inner_area); - frame.render_widget(List::new(paths_text), inner_area); + frame.render_widget(List::new(paths_text), area); } fn handle_key_event(&mut self, key: &KeyEvent) -> bool { diff --git a/tui/src/float.rs b/tui/src/float.rs index 7b569752..4129b7a1 100644 --- a/tui/src/float.rs +++ b/tui/src/float.rs @@ -1,6 +1,9 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Style, Stylize}, + text::Line, + widgets::{Block, Borders, Clear}, Frame, }; @@ -8,6 +11,8 @@ use crate::hint::Shortcut; pub trait FloatContent { fn draw(&mut self, frame: &mut Frame, area: Rect); + fn top_title(&self) -> Option>; + fn bottom_title(&self) -> Option>; fn handle_key_event(&mut self, key: &KeyEvent) -> bool; fn is_finished(&self) -> bool; fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>); @@ -50,7 +55,22 @@ impl Float { pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect) { let popup_area = self.floating_window(parent_area); - self.content.draw(frame, popup_area); + let mut block = Block::new() + .borders(Borders::ALL) + .title_alignment(Alignment::Center) + .style(Style::new().reset()); + + if let Some(top_title) = self.content.top_title() { + block = block.title_top(top_title); + } + + if let Some(bottom_title) = self.content.bottom_title() { + block = block.title_bottom(bottom_title); + } + + frame.render_widget(Clear, popup_area); + frame.render_widget(&block, popup_area); + self.content.draw(frame, block.inner(popup_area)); } // Returns true if the floating window is finished. diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index 879fcbc5..d653940e 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -14,7 +14,7 @@ use ratatui::{ layout::Rect, style::{Style, Stylize}, text::Line, - widgets::{Block, Borders, Clear, List}, + widgets::{Block, List}, Frame, }; @@ -29,7 +29,7 @@ pub struct FloatingText { max_line_width: usize, v_scroll: usize, h_scroll: usize, - mode_title: String, + title: String, } macro_rules! style { @@ -124,7 +124,7 @@ fn get_lines_owned(s: &str) -> Vec { } impl FloatingText { - pub fn new(text: String, title: &str) -> Self { + pub fn new(text: String, title: String) -> Self { let src = get_lines(&text) .into_iter() .map(|s| s.to_string()) @@ -133,7 +133,7 @@ impl FloatingText { let max_line_width = max_width!(src); Self { src, - mode_title: title.to_string(), + title, max_line_width, v_scroll: 0, h_scroll: 0, @@ -146,6 +146,7 @@ impl FloatingText { // just apply highlights directly (max_width!(get_lines(cmd)), Some(cmd.clone())) } + Command::LocalFile { file, .. } => { // have to read from tmp dir to get cmd src let raw = std::fs::read_to_string(file) @@ -163,7 +164,7 @@ impl FloatingText { Some(Self { src, - mode_title: title, + title, max_line_width, h_scroll: 0, v_scroll: 0, @@ -196,21 +197,22 @@ impl FloatingText { } impl FloatContent for FloatingText { + fn top_title(&self) -> Option> { + let title_text = format!(" {} ", self.title); + + let title_line = Line::from(title_text) + .centered() + .style(Style::default().reversed()); + + Some(title_line) + } + + fn bottom_title(&self) -> Option> { + None + } + fn draw(&mut self, frame: &mut Frame, area: Rect) { - // Define the Block with a border and background color - let block = Block::default() - .borders(Borders::ALL) - .title(self.mode_title.clone()) - .title_alignment(ratatui::layout::Alignment::Center) - .title_style(Style::default().reversed()) - .style(Style::default()); - - // Draw the Block first - frame.render_widget(block.clone(), area); - - // Calculate the inner area to ensure text is not drawn over the border - let inner_area = block.inner(area); - let Rect { height, .. } = inner_area; + let Rect { height, .. } = area; let lines = self .src .iter() @@ -253,11 +255,8 @@ impl FloatContent for FloatingText { .block(Block::default()) .highlight_style(Style::default().reversed()); - // Clear the text underneath the floats rendered area - frame.render_widget(Clear, inner_area); - // Render the list inside the bordered area - frame.render_widget(list, inner_area); + frame.render_widget(list, area); } fn handle_key_event(&mut self, key: &KeyEvent) -> bool { @@ -278,7 +277,7 @@ impl FloatContent for FloatingText { fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>) { ( - &self.mode_title, + &self.title, Box::new([ Shortcut::new("Scroll down", ["j", "Down"]), Shortcut::new("Scroll up", ["k", "Up"]), diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index 89daa755..164bae30 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -1,32 +1,39 @@ -use crate::{float::FloatContent, hint::Shortcut}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use linutil_core::Command; -use oneshot::{channel, Receiver}; -use portable_pty::{ - ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem, -}; -use ratatui::{ - layout::{Rect, Size}, - style::{Color, Style, Stylize}, - text::{Line, Span}, - widgets::{Block, Borders}, - Frame, -}; use std::{ + cell::{Cell, RefCell}, io::Write, sync::{Arc, Mutex}, thread::JoinHandle, }; + +use crate::{float::FloatContent, hint::Shortcut}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +use oneshot::{channel, Receiver}; + +use portable_pty::{ + ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem, +}; + +use ratatui::{ + layout::Rect, + style::{Style, Stylize}, + text::Line, + Frame, +}; + use tui_term::{ vt100::{self, Screen}, widget::PseudoTerminal, }; +use linutil_core::Command; + pub struct RunningCommand { /// A buffer to save all the command output (accumulates, until the command exits) buffer: Arc>>, /// A handle for the thread running the command - command_thread: Option>, + command_thread: Cell>>, /// A handle to kill the running process; it's an option because it can only be used once child_killer: Option>>, /// A join handle for the thread that reads command output and sends it to the main thread @@ -36,56 +43,35 @@ pub struct RunningCommand { /// Used for sending keys to the emulated terminal writer: Box, /// Only set after the process has ended - status: Option, + status: RefCell>, scroll_offset: usize, } impl FloatContent for RunningCommand { - fn draw(&mut self, frame: &mut Frame, area: Rect) { - // Calculate the inner size of the terminal area, considering borders - let inner_size = Size { - width: area.width - 2, // Adjust for border width - height: area.height - 2, - }; - - // Define the block for the terminal display - let block = if !self.is_finished() { - // Display a block indicating the command is running - Block::default() - .borders(Borders::ALL) - .title_top(Line::from("Running the command....").centered()) - .title_style(Style::default().reversed()) - .title_bottom(Line::from("Press Ctrl-C to KILL the command")) + fn top_title(&self) -> Option> { + let (content, content_style) = if !self.is_finished() { + (" Running command... ", Style::default().reversed()) + } else if self.wait_command().success() { + (" Success ", Style::default().bold().green().reversed()) } else { - // Display a block with the command's exit status - let mut title_line = if self.get_exit_status().success() { - Line::from( - Span::default() - .content("SUCCESS!") - .style(Style::default().fg(Color::Green).reversed()), - ) - } else { - Line::from( - Span::default() - .content("FAILED!") - .style(Style::default().fg(Color::Red).reversed()), - ) - }; - - title_line.push_span( - Span::default() - .content(" press to close this window ") - .style(Style::default()), - ); - - Block::default() - .borders(Borders::ALL) - .title_top(title_line.centered()) + (" Failed ", Style::default().bold().red().reversed()) }; + Some(Line::from(content).style(content_style)) + } + + fn bottom_title(&self) -> Option> { + Some(Line::from(if !self.is_finished() { + " Press [CTRL-c] to KILL the command " + } else { + " Press [Enter] to close this window " + })) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) { // Process the buffer and create the pseudo-terminal widget - let screen = self.screen(inner_size); - let pseudo_term = PseudoTerminal::new(&screen).block(block); + let screen = self.screen(area); + let pseudo_term = PseudoTerminal::new(&screen); // Render the widget on the frame frame.render_widget(pseudo_term, area); @@ -116,12 +102,7 @@ impl FloatContent for RunningCommand { } fn is_finished(&self) -> bool { - // Check if the command thread has finished - if let Some(command_thread) = &self.command_thread { - command_thread.is_finished() - } else { - true - } + self.status.borrow().is_some() } fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>) { @@ -229,17 +210,17 @@ impl RunningCommand { let writer = pair.master.take_writer().unwrap(); Self { buffer: command_buffer, - command_thread: Some(command_handle), - child_killer: Some(rx), + command_thread: Some(command_handle).into(), + child_killer: rx.into(), _reader_thread: reader_handle, pty_master: pair.master, writer, - status: None, + status: None.into(), scroll_offset: 0, } } - fn screen(&mut self, size: Size) -> Screen { + fn screen(&mut self, size: Rect) -> Screen { // Resize the emulated pty self.pty_master .resize(PtySize { @@ -263,14 +244,16 @@ impl RunningCommand { } /// This function will block if the command is not finished - fn get_exit_status(&mut self) -> ExitStatus { - if self.command_thread.is_some() { - let handle = self.command_thread.take().unwrap(); - let exit_status = handle.join().unwrap(); - self.status = Some(exit_status.clone()); - exit_status - } else { - self.status.as_ref().unwrap().clone() + fn wait_command(&self) -> ExitStatus { + let status = { self.status.borrow().clone() }; + match status { + Some(status) => status, + None => { + let handle = self.command_thread.take().unwrap(); + let exit_status = handle.join().unwrap(); + self.status.replace(Some(exit_status.clone())); + exit_status + } } } @@ -279,6 +262,7 @@ impl RunningCommand { if !self.is_finished() { let mut killer = self.child_killer.take().unwrap().recv().unwrap(); killer.kill().unwrap(); + self.wait_command(); } } diff --git a/tui/src/state.rs b/tui/src/state.rs index 9ed61771..1348089b 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -598,11 +598,6 @@ impl AppState { None } - fn get_selected_description(&self) -> Option { - self.get_selected_node() - .map(|node| node.description.clone()) - } - pub fn go_to_selected_dir(&mut self) { let mut selected_index = self.selection.selected().unwrap_or(0); @@ -654,18 +649,19 @@ impl AppState { } fn enable_preview(&mut self) { - if let Some(list_node) = self.get_selected_node() { - let mut preview_title = "[Preview] - ".to_string(); - preview_title.push_str(list_node.name.as_str()); - if let Some(preview) = FloatingText::from_command(&list_node.command, preview_title) { + if let Some(node) = self.get_selected_node() { + let preview_title = format!("Command Preview - {}", node.name.as_str()); + if let Some(preview) = FloatingText::from_command(&node.command, preview_title) { self.spawn_float(preview, 80, 80); } } } fn enable_description(&mut self) { - if let Some(command_description) = self.get_selected_description() { - let description = FloatingText::new(command_description, "Command Description"); + if let Some(node) = self.get_selected_node() { + let desc_title = format!("Command Description - {}", &node.name); + + let description = FloatingText::new(node.description.clone(), desc_title); self.spawn_float(description, 80, 80); } } @@ -731,7 +727,10 @@ impl AppState { fn toggle_task_list_guide(&mut self) { self.spawn_float( - FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide"), + FloatingText::new( + ACTIONS_GUIDE.to_string(), + "Important Actions Guide".to_string(), + ), 80, 80, );