Compare commits

...

13 Commits

Author SHA1 Message Date
nyx
85b4a171a7
Merge e583fc7864 into fa69885b6c 2024-11-16 17:21:12 -08:00
Jeevitha Kannan K S
fa69885b6c
Use vt100-ctt instead of patching the dep (#952)
Thanks @a-kenji
2024-11-16 15:07:22 -06:00
nnyyxxxx
e583fc7864
fix confirmation prompt closing via rmb 2024-11-13 09:16:03 -05:00
nnyyxxxx
563676db7c
change to false
run clippy
2024-11-13 09:16:03 -05:00
nnyyxxxx
ee4f28fed2
w.i.p. left click confirmation 2024-11-13 09:16:02 -05:00
nnyyxxxx
685f71e53c
combine crates 2024-11-13 09:16:02 -05:00
nnyyxxxx
778372bb4a
make right click close floating windows 2024-11-13 09:16:02 -05:00
nnyyxxxx
55e547f9d0
add click and drag 2024-11-13 09:16:01 -05:00
nnyyxxxx
f41257d55b
bring up description on scroll wheel pressdown, bring up script preview on Right Mouse Button pressdown 2024-11-13 09:16:01 -05:00
nnyyxxxx
093ae2cfb3
allow selection of search bar 2024-11-13 09:16:01 -05:00
nnyyxxxx
f6b8358fcc
fix subdir bug 2024-11-13 09:16:01 -05:00
nnyyxxxx
d7da907a9c
improve mouse position calculation 2024-11-13 09:16:00 -05:00
nnyyxxxx
87f065e8e6
w.i.p. concept for more mouse interaction within the tui
run fmt
2024-11-13 09:15:58 -05:00
8 changed files with 178 additions and 47 deletions

8
Cargo.lock generated
View File

@ -425,6 +425,7 @@ dependencies = [
"tree-sitter-highlight", "tree-sitter-highlight",
"tui-term", "tui-term",
"unicode-width 0.2.0", "unicode-width 0.2.0",
"vt100-ctt",
"zips", "zips",
] ]
@ -1096,7 +1097,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0" checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0"
dependencies = [ dependencies = [
"ratatui", "ratatui",
"vt100",
] ]
[[package]] [[package]]
@ -1147,9 +1147,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "vt100" name = "vt100-ctt"
version = "0.15.2" version = "0.15.3"
source = "git+https://github.com/ChrisTitusTech/vt100-rust#e41fb3d8fb5fd01dd2d076c9a25823a31656012f" source = "git+https://github.com/ChrisTitusTech/vt100-rust#39136a6232d043d8447afa7d47805b6f6baa09ee"
dependencies = [ dependencies = [
"itoa", "itoa",
"log", "log",

View File

@ -8,9 +8,6 @@ members = ["tui", "core", "xtask"]
default-members = ["tui", "core"] default-members = ["tui", "core"]
resolver = "2" resolver = "2"
[patch.crates-io]
vt100 = { git = "https://github.com/ChrisTitusTech/vt100-rust" }
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"
debug = false debug = false

View File

@ -18,7 +18,7 @@ clap = { version = "4.5.20", features = ["derive", "std"], default-features = fa
oneshot = { version = "0.1.8", features = ["std"], default-features = false } oneshot = { version = "0.1.8", features = ["std"], default-features = false }
portable-pty = "0.8.1" portable-pty = "0.8.1"
ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false } ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false }
tui-term = "0.2.0" tui-term = { version = "0.2.0", default-features = false }
temp-dir = "0.1.14" temp-dir = "0.1.14"
time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false } time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false }
unicode-width = { version = "0.2.0", default-features = false } unicode-width = { version = "0.2.0", default-features = false }
@ -31,6 +31,7 @@ anstyle = { version = "1.0.8", default-features = false }
ansi-to-tui = { version = "7.0.0", default-features = false } ansi-to-tui = { version = "7.0.0", default-features = false }
zips = "0.1.7" zips = "0.1.7"
nix = { version = "0.29.0", features = [ "user" ] } nix = { version = "0.29.0", features = [ "user" ] }
vt100-ctt = { git = "https://github.com/ChrisTitusTech/vt100-rust" }
[[bin]] [[bin]]
name = "linutil" name = "linutil"

View File

@ -3,7 +3,7 @@ use std::borrow::Cow;
use crate::{float::FloatContent, hint::Shortcut}; use crate::{float::FloatContent, hint::Shortcut};
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind},
layout::Alignment, layout::Alignment,
prelude::*, prelude::*,
widgets::{Block, Borders, Clear, List}, widgets::{Block, Borders, Clear, List},
@ -87,15 +87,24 @@ impl FloatContent for ConfirmPrompt {
fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool { fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
match event.kind { match event.kind {
MouseEventKind::Down(MouseButton::Left) => {
self.status = ConfirmStatus::Confirm;
true
}
MouseEventKind::Down(MouseButton::Right) => {
self.status = ConfirmStatus::Abort;
false
}
MouseEventKind::ScrollDown => { MouseEventKind::ScrollDown => {
self.scroll_down(); self.scroll_down();
false
} }
MouseEventKind::ScrollUp => { MouseEventKind::ScrollUp => {
self.scroll_up(); self.scroll_up();
false
} }
_ => {} _ => false,
} }
false
} }
fn handle_key_event(&mut self, key: &KeyEvent) -> bool { fn handle_key_event(&mut self, key: &KeyEvent) -> bool {

View File

@ -4,7 +4,7 @@ use ratatui::{
Frame, Frame,
}; };
use crate::hint::Shortcut; use crate::{event::MouseButton, event::MouseEventKind, hint::Shortcut};
pub trait FloatContent { pub trait FloatContent {
fn draw(&mut self, frame: &mut Frame, area: Rect); fn draw(&mut self, frame: &mut Frame, area: Rect);
@ -54,8 +54,14 @@ impl<Content: FloatContent + ?Sized> Float<Content> {
self.content.draw(frame, popup_area); self.content.draw(frame, popup_area);
} }
pub fn handle_mouse_event(&mut self, event: &MouseEvent) { pub fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
self.content.handle_mouse_event(event); match event.kind {
MouseEventKind::Down(MouseButton::Right) => {
self.content.handle_mouse_event(event);
true
}
_ => self.content.handle_mouse_event(event),
}
} }
// Returns true if the floating window is finished. // Returns true if the floating window is finished.

View File

@ -9,7 +9,7 @@ use crate::{float::FloatContent, hint::Shortcut};
use linutil_core::Command; use linutil_core::Command;
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind},
layout::Rect, layout::Rect,
style::{Style, Stylize}, style::{Style, Stylize},
text::Line, text::Line,
@ -33,6 +33,8 @@ pub struct FloatingText {
mode_title: String, mode_title: String,
wrap_words: bool, wrap_words: bool,
frame_height: usize, frame_height: usize,
drag_start_y: Option<u16>,
drag_start_scroll: Option<usize>,
} }
macro_rules! style { macro_rules! style {
@ -141,6 +143,8 @@ impl FloatingText {
h_scroll: 0, h_scroll: 0,
wrap_words, wrap_words,
frame_height: 0, frame_height: 0,
drag_start_y: None,
drag_start_scroll: None,
} }
} }
@ -165,6 +169,8 @@ impl FloatingText {
v_scroll: 0, v_scroll: 0,
wrap_words: false, wrap_words: false,
frame_height: 0, frame_height: 0,
drag_start_y: None,
drag_start_scroll: None,
}) })
} }
@ -206,6 +212,19 @@ impl FloatingText {
}; };
} }
} }
fn handle_drag(&mut self, current_y: u16) {
if let (Some(start_y), Some(start_scroll)) = (self.drag_start_y, self.drag_start_scroll) {
let delta = start_y as i32 - current_y as i32;
let new_scroll = start_scroll as i32 + delta;
let max_scroll = self
.wrapped_lines
.len()
.saturating_sub(self.frame_height.saturating_sub(2));
self.v_scroll = new_scroll.clamp(0, max_scroll as i32) as usize;
}
}
} }
impl FloatContent for FloatingText { impl FloatContent for FloatingText {
@ -285,6 +304,17 @@ impl FloatContent for FloatingText {
fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool { fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
match event.kind { match event.kind {
MouseEventKind::Down(MouseButton::Left) => {
self.drag_start_y = Some(event.row);
self.drag_start_scroll = Some(self.v_scroll);
}
MouseEventKind::Up(MouseButton::Left) => {
self.drag_start_y = None;
self.drag_start_scroll = None;
}
MouseEventKind::Drag(MouseButton::Left) => {
self.handle_drag(event.row);
}
MouseEventKind::ScrollDown => self.scroll_down(), MouseEventKind::ScrollDown => self.scroll_down(),
MouseEventKind::ScrollUp => self.scroll_up(), MouseEventKind::ScrollUp => self.scroll_up(),
MouseEventKind::ScrollLeft => self.scroll_left(), MouseEventKind::ScrollLeft => self.scroll_left(),

View File

@ -18,10 +18,9 @@ use std::{
thread::JoinHandle, thread::JoinHandle,
}; };
use time::{macros::format_description, OffsetDateTime}; use time::{macros::format_description, OffsetDateTime};
use tui_term::{ use tui_term::widget::PseudoTerminal;
vt100::{self, Screen}, use vt100_ctt::{Parser, Screen};
widget::PseudoTerminal,
};
pub struct RunningCommand { pub struct RunningCommand {
/// A buffer to save all the command output (accumulates, until the command exits) /// A buffer to save all the command output (accumulates, until the command exits)
buffer: Arc<Mutex<Vec<u8>>>, buffer: Arc<Mutex<Vec<u8>>>,
@ -114,7 +113,7 @@ impl FloatContent for RunningCommand {
} }
_ => {} _ => {}
} }
true false
} }
/// Handle key events of the running command "window". Returns true when the "window" should be /// Handle key events of the running command "window". Returns true when the "window" should be
/// closed /// closed
@ -285,7 +284,7 @@ impl RunningCommand {
// Process the buffer with a parser with the current screen size // Process the buffer with a parser with the current screen size
// We don't actually need to create a new parser every time, but it is so much easier this // We don't actually need to create a new parser every time, but it is so much easier this
// way, and doesn't cost that much // way, and doesn't cost that much
let mut parser = vt100::Parser::new(size.height, size.width, 1000); let mut parser = Parser::new(size.height, size.width, 1000);
let mutex = self.buffer.lock(); let mutex = self.buffer.lock();
let buffer = mutex.as_ref().unwrap(); let buffer = mutex.as_ref().unwrap();
parser.process(buffer); parser.process(buffer);

View File

@ -13,7 +13,9 @@ use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList};
#[cfg(feature = "tips")] #[cfg(feature = "tips")]
use rand::Rng; use rand::Rng;
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind}, crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
},
layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect}, layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect},
style::{Style, Stylize}, style::{Style, Stylize},
text::{Line, Span, Text}, text::{Line, Span, Text},
@ -481,7 +483,7 @@ impl AppState {
match &mut self.focus { match &mut self.focus {
Focus::FloatingWindow(float) => float.draw(frame, chunks[1]), Focus::FloatingWindow(float) => float.draw(frame, chunks[1]),
Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1]), Focus::ConfirmationPrompt(confirm) => confirm.draw(frame, chunks[1]),
_ => {} _ => {}
} }
@ -493,51 +495,138 @@ impl AppState {
return true; return true;
} }
if matches!(self.focus, Focus::TabList | Focus::List) { match &mut self.focus {
Focus::FloatingWindow(float) => {
if float.handle_mouse_event(event) {
self.focus = Focus::List;
}
return true;
}
Focus::ConfirmationPrompt(confirm) => {
if confirm.handle_mouse_event(event) {
match confirm.content.status {
ConfirmStatus::Abort => {
self.focus = Focus::List;
if !self.multi_select {
self.selected_commands.clear()
} else if let Some(node) = self.get_selected_node() {
if !node.multi_select {
self.selected_commands.retain(|cmd| cmd.name != node.name);
}
}
}
ConfirmStatus::Confirm => self.handle_confirm_command(),
ConfirmStatus::None => {}
}
}
return true;
}
_ => {}
}
if matches!(self.focus, Focus::TabList | Focus::List | Focus::Search) {
let position = Position::new(event.column, event.row); let position = Position::new(event.column, event.row);
let mouse_in_tab_list = self.areas.as_ref().unwrap().tab_list.contains(position); let mouse_in_tab_list = self.areas.as_ref().unwrap().tab_list.contains(position);
let mouse_in_list = self.areas.as_ref().unwrap().list.contains(position); let mouse_in_list = self.areas.as_ref().unwrap().list.contains(position);
let mouse_in_search = if let Some(areas) = &self.areas {
position.y >= areas.list.y
&& position.y < areas.list.y + 3
&& position.x >= areas.list.x
&& position.x < areas.list.x + areas.list.width
} else {
false
};
match event.kind { match event.kind {
MouseEventKind::Moved => { MouseEventKind::Moved => {
if mouse_in_list { if mouse_in_search {
self.focus = Focus::List if !matches!(self.focus, Focus::Search) {
self.focus = Focus::Search;
self.filter.activate_search();
}
} else if mouse_in_list {
if matches!(self.focus, Focus::Search) {
self.exit_search();
}
self.focus = Focus::List;
if let Some(areas) = &self.areas {
let list_start = areas.list.y + 4;
let relative_y = position.y.saturating_sub(list_start);
let list_len = self.filter.item_list().len();
let adjusted_len = if self.at_root() {
list_len
} else {
list_len + 1
};
if relative_y < adjusted_len as u16 {
self.selection.select(Some(relative_y as usize));
}
}
} else if mouse_in_tab_list { } else if mouse_in_tab_list {
self.focus = Focus::TabList if matches!(self.focus, Focus::Search) {
self.exit_search();
}
self.focus = Focus::TabList;
if let Some(areas) = &self.areas {
let relative_y = position.y.saturating_sub(areas.tab_list.y + 1);
if relative_y < self.tabs.len() as u16 {
self.current_tab.select(Some(relative_y as usize));
self.refresh_tab();
}
}
} }
} }
MouseEventKind::ScrollDown => { MouseEventKind::Down(button) => match button {
MouseButton::Left => {
if mouse_in_search {
self.enter_search();
} else if mouse_in_list {
if matches!(self.focus, Focus::Search) {
self.exit_search();
}
self.handle_enter();
} else if mouse_in_tab_list {
if matches!(self.focus, Focus::Search) {
self.exit_search();
}
self.focus = Focus::TabList;
}
}
MouseButton::Right if mouse_in_list => {
if matches!(self.focus, Focus::Search) {
self.exit_search();
}
self.enable_preview();
}
MouseButton::Middle if mouse_in_list => {
if matches!(self.focus, Focus::Search) {
self.exit_search();
}
self.enable_description();
}
_ => {}
},
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
if matches!(self.focus, Focus::Search) {
self.exit_search();
}
if mouse_in_tab_list { if mouse_in_tab_list {
if self.current_tab.selected().unwrap() != self.tabs.len() - 1 { if self.current_tab.selected().unwrap() != self.tabs.len() - 1 {
self.current_tab.select_next(); self.current_tab.select_next();
} }
self.refresh_tab(); self.refresh_tab();
} else if mouse_in_list { } else if mouse_in_list {
self.selection.select_next() if event.kind == MouseEventKind::ScrollDown {
} self.scroll_down();
} } else {
MouseEventKind::ScrollUp => { self.scroll_up();
if mouse_in_tab_list {
if self.current_tab.selected().unwrap() != 0 {
self.current_tab.select_previous();
} }
self.refresh_tab();
} else if mouse_in_list {
self.selection.select_previous()
} }
} }
_ => {} _ => {}
} }
} }
match &mut self.focus {
Focus::FloatingWindow(float) => {
float.content.handle_mouse_event(event);
}
Focus::ConfirmationPrompt(confirm) => {
confirm.content.handle_mouse_event(event);
}
_ => {}
}
true true
} }