fix(core): make running a single task more transparent (#31163)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> Running a single task is in this uncanny framed state and doesn't exit immediately. <img width="1317" alt="image" src="https://github.com/user-attachments/assets/13e7463f-9eb0-48db-95f2-c09e203d494f" /> ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Running a single task isn't framed and exits immediately. This also fixes scrolling in interactive mode based on whether or not the underlying terminal is in application cursor mode or not.  ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # --------- Co-authored-by: JamesHenry <james@henry.sc>
This commit is contained in:
parent
a31226437e
commit
3d73fd30a0
@ -1,5 +1,5 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEventKind};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use napi::bindgen_prelude::External;
|
use napi::bindgen_prelude::External;
|
||||||
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
|
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
|
||||||
@ -214,14 +214,22 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.begin_exit_countdown()
|
if self.tasks.len() > 1 {
|
||||||
|
self.begin_exit_countdown()
|
||||||
|
} else {
|
||||||
|
self.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quit(&mut self) {
|
||||||
|
self.quit_at = Some(std::time::Instant::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn begin_exit_countdown(&mut self) {
|
fn begin_exit_countdown(&mut self) {
|
||||||
let countdown_duration = self.tui_config.auto_exit.countdown_seconds();
|
let countdown_duration = self.tui_config.auto_exit.countdown_seconds();
|
||||||
// If countdown is disabled, exit immediately
|
// If countdown is disabled, exit immediately
|
||||||
if countdown_duration.is_none() {
|
if countdown_duration.is_none() {
|
||||||
self.quit_at = Some(std::time::Instant::now());
|
self.quit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,27 +716,32 @@ impl App {
|
|||||||
self.user_has_interacted = true;
|
self.user_has_interacted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(self.focus, Focus::MultipleOutput(_)) {
|
||||||
|
self.handle_mouse_event(mouse).ok();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
match mouse.kind {
|
match mouse.kind {
|
||||||
MouseEventKind::ScrollUp => {
|
MouseEventKind::ScrollUp => {
|
||||||
if matches!(self.focus, Focus::MultipleOutput(_)) {
|
if matches!(self.focus, Focus::TaskList) {
|
||||||
|
self.dispatch_action(Action::PreviousTask);
|
||||||
|
} else {
|
||||||
self.handle_key_event(KeyEvent::new(
|
self.handle_key_event(KeyEvent::new(
|
||||||
KeyCode::Up,
|
KeyCode::Up,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
))
|
))
|
||||||
.ok();
|
.ok();
|
||||||
} else if matches!(self.focus, Focus::TaskList) {
|
|
||||||
self.dispatch_action(Action::PreviousTask);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseEventKind::ScrollDown => {
|
MouseEventKind::ScrollDown => {
|
||||||
if matches!(self.focus, Focus::MultipleOutput(_)) {
|
if matches!(self.focus, Focus::TaskList) {
|
||||||
|
self.dispatch_action(Action::NextTask);
|
||||||
|
} else {
|
||||||
self.handle_key_event(KeyEvent::new(
|
self.handle_key_event(KeyEvent::new(
|
||||||
KeyCode::Down,
|
KeyCode::Down,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
))
|
))
|
||||||
.ok();
|
.ok();
|
||||||
} else if matches!(self.focus, Focus::TaskList) {
|
|
||||||
self.dispatch_action(Action::NextTask);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -790,6 +803,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let frame_area = self.frame_area.unwrap();
|
let frame_area = self.frame_area.unwrap();
|
||||||
|
let tasks_list_hidden = self.is_task_list_hidden();
|
||||||
let layout_areas = self.layout_areas.as_mut().unwrap();
|
let layout_areas = self.layout_areas.as_mut().unwrap();
|
||||||
|
|
||||||
if self.debug_mode {
|
if self.debug_mode {
|
||||||
@ -970,6 +984,7 @@ impl App {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let terminal_pane = TerminalPane::new()
|
let terminal_pane = TerminalPane::new()
|
||||||
|
.minimal(tasks_list_hidden && self.tasks.len() == 1)
|
||||||
.pty_data(terminal_pane_data)
|
.pty_data(terminal_pane_data)
|
||||||
.continuous(task.continuous);
|
.continuous(task.continuous);
|
||||||
|
|
||||||
@ -1330,6 +1345,15 @@ impl App {
|
|||||||
let _ = self.handle_pty_resize();
|
let _ = self.handle_pty_resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_mouse_event(&mut self, mouse_event: MouseEvent) -> io::Result<()> {
|
||||||
|
if let Focus::MultipleOutput(pane_idx) = self.focus {
|
||||||
|
let terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
|
||||||
|
terminal_pane_data.handle_mouse_event(mouse_event)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Forward key events to the currently focused pane, if any.
|
/// Forward key events to the currently focused pane, if any.
|
||||||
fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> {
|
fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> {
|
||||||
if let Focus::MultipleOutput(pane_idx) = self.focus {
|
if let Focus::MultipleOutput(pane_idx) = self.focus {
|
||||||
|
|||||||
@ -138,7 +138,7 @@ impl LayoutManager {
|
|||||||
self.mode = match self.mode {
|
self.mode = match self.mode {
|
||||||
LayoutMode::Auto => {
|
LayoutMode::Auto => {
|
||||||
// If we are in auto mode, we need to figure out our current orientation and set the mode to the opposite.
|
// If we are in auto mode, we need to figure out our current orientation and set the mode to the opposite.
|
||||||
if self.is_vertical_layout_preferred(area.width, area.height, self.task_count) {
|
if self.is_vertical_layout_preferred(area.width, area.height) {
|
||||||
LayoutMode::Horizontal
|
LayoutMode::Horizontal
|
||||||
} else {
|
} else {
|
||||||
LayoutMode::Vertical
|
LayoutMode::Vertical
|
||||||
@ -249,9 +249,7 @@ impl LayoutManager {
|
|||||||
fn calculate_layout_visible_task_list(&self, area: Rect) -> LayoutAreas {
|
fn calculate_layout_visible_task_list(&self, area: Rect) -> LayoutAreas {
|
||||||
// Determine whether to use vertical or horizontal layout
|
// Determine whether to use vertical or horizontal layout
|
||||||
let use_vertical = match self.mode {
|
let use_vertical = match self.mode {
|
||||||
LayoutMode::Auto => {
|
LayoutMode::Auto => self.is_vertical_layout_preferred(area.width, area.height),
|
||||||
self.is_vertical_layout_preferred(area.width, area.height, self.task_count)
|
|
||||||
}
|
|
||||||
LayoutMode::Vertical => true,
|
LayoutMode::Vertical => true,
|
||||||
LayoutMode::Horizontal => false,
|
LayoutMode::Horizontal => false,
|
||||||
};
|
};
|
||||||
@ -401,17 +399,7 @@ impl LayoutManager {
|
|||||||
/// - Terminal aspect ratio
|
/// - Terminal aspect ratio
|
||||||
/// - Number of tasks (single task prefers vertical layout)
|
/// - Number of tasks (single task prefers vertical layout)
|
||||||
/// - Minimum dimensions requirements
|
/// - Minimum dimensions requirements
|
||||||
fn is_vertical_layout_preferred(
|
fn is_vertical_layout_preferred(&self, terminal_width: u16, terminal_height: u16) -> bool {
|
||||||
&self,
|
|
||||||
terminal_width: u16,
|
|
||||||
terminal_height: u16,
|
|
||||||
task_count: usize,
|
|
||||||
) -> bool {
|
|
||||||
// If there's only a single task, prefer vertical layout
|
|
||||||
if task_count <= 1 {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate aspect ratio (width/height)
|
// Calculate aspect ratio (width/height)
|
||||||
let aspect_ratio = terminal_width as f32 / terminal_height as f32;
|
let aspect_ratio = terminal_width as f32 / terminal_height as f32;
|
||||||
|
|
||||||
@ -557,27 +545,6 @@ mod tests {
|
|||||||
assert_eq!(task_list.height, 100 / 3);
|
assert_eq!(task_list.height, 100 / 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_auto_mode_prefers_vertical_for_single_task() {
|
|
||||||
let mut layout_manager = LayoutManager::new(5);
|
|
||||||
let area = create_test_area(200, 60); // Wide terminal that would normally use horizontal
|
|
||||||
|
|
||||||
layout_manager.set_mode(LayoutMode::Auto);
|
|
||||||
layout_manager.set_task_list_visibility(TaskListVisibility::Visible);
|
|
||||||
layout_manager.set_pane_arrangement(PaneArrangement::Single);
|
|
||||||
layout_manager.set_task_count(1); // Single task
|
|
||||||
|
|
||||||
let layout = layout_manager.calculate_layout(area);
|
|
||||||
assert!(layout.task_list.is_some());
|
|
||||||
|
|
||||||
// Even though terminal is wide, layout should be vertical for a single task
|
|
||||||
let task_list = layout.task_list.unwrap();
|
|
||||||
assert_eq!(task_list.x, 0);
|
|
||||||
assert_eq!(task_list.y, 0);
|
|
||||||
assert_eq!(task_list.width, 200);
|
|
||||||
assert_eq!(task_list.height, 60 / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_forced_vertical_mode() {
|
fn test_forced_vertical_mode() {
|
||||||
let mut layout_manager = LayoutManager::new(5);
|
let mut layout_manager = LayoutManager::new(5);
|
||||||
@ -840,19 +807,6 @@ mod tests {
|
|||||||
insta::assert_snapshot!(terminal.backend());
|
insta::assert_snapshot!(terminal.backend());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visual test for auto mode with single task (should be vertical regardless of terminal size)
|
|
||||||
#[test]
|
|
||||||
fn test_visualize_auto_mode_single_task() {
|
|
||||||
let mut layout_manager = LayoutManager::new(5);
|
|
||||||
layout_manager.set_mode(LayoutMode::Auto);
|
|
||||||
layout_manager.set_task_list_visibility(TaskListVisibility::Visible);
|
|
||||||
layout_manager.set_pane_arrangement(PaneArrangement::Single);
|
|
||||||
layout_manager.set_task_count(1);
|
|
||||||
|
|
||||||
let terminal = render_layout(120, 30, &layout_manager);
|
|
||||||
insta::assert_snapshot!(terminal.backend());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Visual test for auto mode with tall terminal
|
/// Visual test for auto mode with tall terminal
|
||||||
#[test]
|
#[test]
|
||||||
fn test_visualize_auto_mode_tall_terminal() {
|
fn test_visualize_auto_mode_tall_terminal() {
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
source: packages/nx/src/native/tui/components/layout_manager.rs
|
|
||||||
expression: terminal.backend()
|
|
||||||
---
|
|
||||||
"┌Task List─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘"
|
|
||||||
" "
|
|
||||||
"┌Terminal Pane 1───────────────────────────────────────────────────────────────────────────────────────────────────────┐"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"│ │"
|
|
||||||
"└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘"
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@ -97,11 +97,8 @@ impl TerminalPaneData {
|
|||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
pty_mut.write_input(c.to_string().as_bytes())?;
|
pty_mut.write_input(c.to_string().as_bytes())?;
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up | KeyCode::Down => {
|
||||||
pty_mut.write_input(b"\x1b[A")?;
|
pty_mut.handle_arrow_keys(key);
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
|
||||||
pty_mut.write_input(b"\x1b[B")?;
|
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
pty_mut.write_input(b"\r")?;
|
pty_mut.write_input(b"\r")?;
|
||||||
@ -120,6 +117,26 @@ impl TerminalPaneData {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_mouse_event(&mut self, event: MouseEvent) -> io::Result<()> {
|
||||||
|
if let Some(pty) = &mut self.pty {
|
||||||
|
let mut pty_mut = pty.as_ref().clone();
|
||||||
|
if self.is_interactive {
|
||||||
|
pty_mut.send_mouse_event(event);
|
||||||
|
} else {
|
||||||
|
match event.kind {
|
||||||
|
MouseEventKind::ScrollUp => {
|
||||||
|
pty_mut.scroll_up();
|
||||||
|
}
|
||||||
|
MouseEventKind::ScrollDown => {
|
||||||
|
pty_mut.scroll_down();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_interactive(&mut self, interactive: bool) {
|
pub fn set_interactive(&mut self, interactive: bool) {
|
||||||
self.is_interactive = interactive;
|
self.is_interactive = interactive;
|
||||||
}
|
}
|
||||||
@ -171,6 +188,7 @@ impl TerminalPaneState {
|
|||||||
pub struct TerminalPane<'a> {
|
pub struct TerminalPane<'a> {
|
||||||
pty_data: Option<&'a mut TerminalPaneData>,
|
pty_data: Option<&'a mut TerminalPaneData>,
|
||||||
is_continuous: bool,
|
is_continuous: bool,
|
||||||
|
minimal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TerminalPane<'a> {
|
impl<'a> TerminalPane<'a> {
|
||||||
@ -178,6 +196,7 @@ impl<'a> TerminalPane<'a> {
|
|||||||
Self {
|
Self {
|
||||||
pty_data: None,
|
pty_data: None,
|
||||||
is_continuous: false,
|
is_continuous: false,
|
||||||
|
minimal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +210,11 @@ impl<'a> TerminalPane<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn minimal(mut self, minimal: bool) -> Self {
|
||||||
|
self.minimal = minimal;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_status_icon(&self, status: TaskStatus) -> Span {
|
fn get_status_icon(&self, status: TaskStatus) -> Span {
|
||||||
match status {
|
match status {
|
||||||
TaskStatus::Success
|
TaskStatus::Success
|
||||||
@ -320,34 +344,47 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
|
|
||||||
let status_icon = self.get_status_icon(state.task_status);
|
let status_icon = self.get_status_icon(state.task_status);
|
||||||
|
|
||||||
let block = Block::default()
|
let mut title = vec![];
|
||||||
.title(Line::from(if state.is_focused {
|
|
||||||
vec![
|
if self.minimal {
|
||||||
status_icon.clone(),
|
title.push(Span::styled(
|
||||||
Span::raw(format!("{} ", state.task_name))
|
" NX ",
|
||||||
.style(Style::default().fg(THEME.primary_fg)),
|
Style::default().fg(THEME.primary_fg).bold().bg(base_style
|
||||||
]
|
.fg
|
||||||
|
.expect("Base style should have foreground color")),
|
||||||
|
));
|
||||||
|
title.push(Span::raw(" "));
|
||||||
|
} else {
|
||||||
|
title.push(status_icon.clone());
|
||||||
|
}
|
||||||
|
title.push(Span::styled(
|
||||||
|
format!("{} ", state.task_name),
|
||||||
|
Style::default().fg(if state.is_focused {
|
||||||
|
THEME.primary_fg
|
||||||
} else {
|
} else {
|
||||||
vec![
|
THEME.secondary_fg
|
||||||
status_icon.clone(),
|
}),
|
||||||
Span::raw(format!("{} ", state.task_name))
|
));
|
||||||
.style(Style::default().fg(THEME.secondary_fg)),
|
|
||||||
if state.is_next_tab_target {
|
if state.is_next_tab_target {
|
||||||
let tab_target_text = Span::raw("Press <tab> to focus output ")
|
let tab_target_text =
|
||||||
.remove_modifier(Modifier::DIM);
|
Span::raw("Press <tab> to focus output ").remove_modifier(Modifier::DIM);
|
||||||
// In light themes, use the primary fg color for the tab target text to make sure it's clearly visible
|
// In light themes, use the primary fg color for the tab target text to make sure it's clearly visible
|
||||||
if !THEME.is_dark_mode {
|
if !THEME.is_dark_mode {
|
||||||
tab_target_text.fg(THEME.primary_fg)
|
title.push(tab_target_text.fg(THEME.primary_fg));
|
||||||
} else {
|
} else {
|
||||||
tab_target_text
|
title.push(tab_target_text);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Span::raw("")
|
|
||||||
},
|
let block = Block::default()
|
||||||
]
|
.title(title)
|
||||||
}))
|
|
||||||
.title_alignment(Alignment::Left)
|
.title_alignment(Alignment::Left)
|
||||||
.borders(Borders::ALL)
|
.borders(if self.minimal {
|
||||||
|
Borders::NONE
|
||||||
|
} else {
|
||||||
|
Borders::ALL
|
||||||
|
})
|
||||||
.border_type(if state.is_focused {
|
.border_type(if state.is_focused {
|
||||||
BorderType::Thick
|
BorderType::Thick
|
||||||
} else {
|
} else {
|
||||||
@ -485,8 +522,40 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
scrollbar.render(safe_area, buf, &mut state.scrollbar_state);
|
scrollbar.render(safe_area, buf, &mut state.scrollbar_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show interactive/readonly status for focused, in progress tasks
|
// Show instructions to quit in minimal mode if somehow terminal became non-interactive
|
||||||
if state.task_status == TaskStatus::InProgress && state.is_focused {
|
if self.minimal && !self.is_currently_interactive() {
|
||||||
|
let top_text = Line::from(vec![
|
||||||
|
Span::styled("quit: ", Style::default().fg(THEME.primary_fg)),
|
||||||
|
Span::styled("q ", Style::default().fg(THEME.info)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mode_width = top_text
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.map(|span| span.content.len())
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
// Ensure text doesn't extend past safe area
|
||||||
|
if mode_width as u16 + 3 < safe_area.width {
|
||||||
|
let top_right_area = Rect {
|
||||||
|
x: safe_area.x + safe_area.width - mode_width as u16 - 3,
|
||||||
|
y: safe_area.y,
|
||||||
|
width: mode_width as u16 + 2,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
Paragraph::new(top_text)
|
||||||
|
.alignment(Alignment::Right)
|
||||||
|
.style(border_style)
|
||||||
|
.render(top_right_area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show interactive/readonly status for focused, in progress tasks, when not in minimal mode
|
||||||
|
} else if state.task_status == TaskStatus::InProgress
|
||||||
|
&& state.is_focused
|
||||||
|
&& pty_data.can_be_interactive
|
||||||
|
&& !self.minimal
|
||||||
|
{
|
||||||
// Bottom right status
|
// Bottom right status
|
||||||
let bottom_text = if self.is_currently_interactive() {
|
let bottom_text = if self.is_currently_interactive() {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
|
use super::utils::normalize_newlines;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind};
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
sync::{Arc, Mutex, RwLock},
|
sync::{Arc, Mutex, RwLock},
|
||||||
};
|
};
|
||||||
|
use tracing::debug;
|
||||||
use vt100_ctt::Parser;
|
use vt100_ctt::Parser;
|
||||||
|
|
||||||
use super::utils::normalize_newlines;
|
|
||||||
|
|
||||||
/// A wrapper that provides access to the terminal screen without cloning
|
/// A wrapper that provides access to the terminal screen without cloning
|
||||||
///
|
///
|
||||||
/// This struct uses a read lock guard internally to maintain the lock on the parser while
|
/// This struct uses a read lock guard internally to maintain the lock on the parser while
|
||||||
@ -93,6 +94,58 @@ impl PtyInstance {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_arrow_keys(&mut self, event: KeyEvent) {
|
||||||
|
let alternative_screen = self.parser.read().unwrap().screen().alternate_screen();
|
||||||
|
debug!("Alternate Screen: {:?}", alternative_screen);
|
||||||
|
if !alternative_screen {
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
self.scroll_up();
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
self.scroll_down();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
self.write_input(b"\x1b[A").ok();
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
self.write_input(b"\x1b[B").ok();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_mouse_event(&mut self, event: MouseEvent) {
|
||||||
|
let alternative_screen = self.parser.read().unwrap().screen().alternate_screen();
|
||||||
|
debug!("Alternate Screen: {:?}", alternative_screen);
|
||||||
|
if !alternative_screen {
|
||||||
|
match event.kind {
|
||||||
|
MouseEventKind::ScrollUp => {
|
||||||
|
self.scroll_up();
|
||||||
|
}
|
||||||
|
MouseEventKind::ScrollDown => {
|
||||||
|
self.scroll_down();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match event.kind {
|
||||||
|
MouseEventKind::ScrollUp => {
|
||||||
|
self.write_input(b"\x1b[A").ok();
|
||||||
|
}
|
||||||
|
MouseEventKind::ScrollDown => {
|
||||||
|
self.write_input(b"\x1b[B").ok();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_screen(&self) -> Option<PtyScreenRef> {
|
pub fn get_screen(&self) -> Option<PtyScreenRef> {
|
||||||
self.parser
|
self.parser
|
||||||
.read()
|
.read()
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use color_eyre::eyre::Result;
|
|||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor,
|
cursor,
|
||||||
event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent},
|
event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent},
|
||||||
|
execute,
|
||||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
@ -170,7 +171,7 @@ impl Tui {
|
|||||||
let _ = THEME.is_dark_mode;
|
let _ = THEME.is_dark_mode;
|
||||||
debug!("Enabling Raw Mode");
|
debug!("Enabling Raw Mode");
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
|
execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
|
||||||
self.start();
|
self.start();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -179,7 +180,7 @@ impl Tui {
|
|||||||
self.stop()?;
|
self.stop()?;
|
||||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||||
self.flush()?;
|
self.flush()?;
|
||||||
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user