feat(core): add enter and q keybindings (#30786)
<!-- 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 --> The TUI is missing some standard keybindings: Q to quit Enter to show task output ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Q now triggers a confirmation to exit. Because Q is close to 1, users might often accidentally hit Q so this gives them a chance to cancel the exit. Enter will show the task output. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
85bc540a15
commit
2c8aba2fc2
@ -5,7 +5,6 @@ use crossterm::{
|
|||||||
terminal::{disable_raw_mode, enable_raw_mode},
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
tty::IsTty,
|
tty::IsTty,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
|
||||||
use napi::bindgen_prelude::*;
|
use napi::bindgen_prelude::*;
|
||||||
use portable_pty::{CommandBuilder, NativePtySystem, PtyPair, PtySize, PtySystem};
|
use portable_pty::{CommandBuilder, NativePtySystem, PtyPair, PtySize, PtySystem};
|
||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ use ratatui::widgets::Paragraph;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc};
|
use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc};
|
||||||
use crate::native::tasks::types::{Task, TaskResult};
|
use crate::native::tasks::types::{Task, TaskResult};
|
||||||
@ -154,6 +155,10 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.begin_exit_countdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
@ -241,20 +246,29 @@ impl App {
|
|||||||
// Record that the user has interacted with the app
|
// Record that the user has interacted with the app
|
||||||
self.user_has_interacted = true;
|
self.user_has_interacted = true;
|
||||||
|
|
||||||
// Handle Ctrl+C to quit
|
|
||||||
if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL {
|
|
||||||
self.is_forced_shutdown = true;
|
|
||||||
// Quit immediately
|
|
||||||
self.quit_at = Some(std::time::Instant::now());
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tasks list component to check interactive mode before handling '?' key
|
// Get tasks list component to check interactive mode before handling '?' key
|
||||||
if let Some(tasks_list) = self
|
if let Some(tasks_list) = self
|
||||||
.components
|
.components
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find_map(|c| c.as_any_mut().downcast_mut::<TasksList>())
|
.find_map(|c| c.as_any_mut().downcast_mut::<TasksList>())
|
||||||
{
|
{
|
||||||
|
if matches!(self.focus, Focus::MultipleOutput(_))
|
||||||
|
&& tasks_list.is_interactive_mode()
|
||||||
|
{
|
||||||
|
return match key.code {
|
||||||
|
KeyCode::Char('z') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
|
// Disable interactive mode when Ctrl+Z is pressed
|
||||||
|
tasks_list.set_interactive_mode(false);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// The TasksList will forward the key event to the focused terminal pane
|
||||||
|
tasks_list.handle_key_event(key).ok();
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Only handle '?' key if we're not in interactive mode and the countdown popup is not open
|
// Only handle '?' key if we're not in interactive mode and the countdown popup is not open
|
||||||
if matches!(key.code, KeyCode::Char('?'))
|
if matches!(key.code, KeyCode::Char('?'))
|
||||||
&& !tasks_list.is_interactive_mode()
|
&& !tasks_list.is_interactive_mode()
|
||||||
@ -286,18 +300,26 @@ impl App {
|
|||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find_map(|c| c.as_any_mut().downcast_mut::<CountdownPopup>())
|
.find_map(|c| c.as_any_mut().downcast_mut::<CountdownPopup>())
|
||||||
{
|
{
|
||||||
if !countdown_popup.is_scrollable() {
|
|
||||||
countdown_popup.cancel_countdown();
|
|
||||||
self.quit_at = None;
|
|
||||||
self.focus = self.previous_focus;
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Char('q') => {
|
||||||
|
// Quit immediately
|
||||||
|
trace!("Confirming shutdown");
|
||||||
|
self.quit_at = Some(std::time::Instant::now());
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
|
// Quit immediately
|
||||||
|
trace!("Confirming shutdown");
|
||||||
|
self.quit_at = Some(std::time::Instant::now());
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
KeyCode::Up | KeyCode::Char('k') if countdown_popup.is_scrollable() => {
|
||||||
countdown_popup.scroll_up();
|
countdown_popup.scroll_up();
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j') => {
|
KeyCode::Down | KeyCode::Char('j')
|
||||||
|
if countdown_popup.is_scrollable() =>
|
||||||
|
{
|
||||||
countdown_popup.scroll_down();
|
countdown_popup.scroll_down();
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@ -312,6 +334,21 @@ impl App {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(tasks_list) = self
|
||||||
|
.components
|
||||||
|
.iter_mut()
|
||||||
|
.find_map(|c| c.as_any_mut().downcast_mut::<TasksList>())
|
||||||
|
{
|
||||||
|
// Handle Q or Ctrl+C to trigger countdown
|
||||||
|
if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL
|
||||||
|
|| (!tasks_list.filter_mode && key.code == KeyCode::Char('q'))
|
||||||
|
{
|
||||||
|
self.is_forced_shutdown = true;
|
||||||
|
self.begin_exit_countdown();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If shortcuts popup is open, handle its keyboard events
|
// If shortcuts popup is open, handle its keyboard events
|
||||||
if matches!(self.focus, Focus::HelpPopup) {
|
if matches!(self.focus, Focus::HelpPopup) {
|
||||||
match key.code {
|
match key.code {
|
||||||
@ -375,37 +412,36 @@ impl App {
|
|||||||
|
|
||||||
match tasks_list.get_focus() {
|
match tasks_list.get_focus() {
|
||||||
Focus::MultipleOutput(_) => {
|
Focus::MultipleOutput(_) => {
|
||||||
if tasks_list.is_interactive_mode() {
|
// Handle navigation and special actions
|
||||||
// Send all other keys to the task list (and ultimately through the terminal pane to the PTY)
|
match key.code {
|
||||||
tasks_list.handle_key_event(key).ok();
|
KeyCode::Tab => {
|
||||||
} else {
|
tasks_list.focus_next();
|
||||||
// Handle navigation and special actions
|
self.focus = tasks_list.get_focus();
|
||||||
match key.code {
|
}
|
||||||
KeyCode::Tab => {
|
KeyCode::BackTab => {
|
||||||
tasks_list.focus_next();
|
tasks_list.focus_previous();
|
||||||
self.focus = tasks_list.get_focus();
|
self.focus = tasks_list.get_focus();
|
||||||
}
|
}
|
||||||
KeyCode::BackTab => {
|
KeyCode::Esc => {
|
||||||
tasks_list.focus_previous();
|
tasks_list.set_focus(Focus::TaskList);
|
||||||
self.focus = tasks_list.get_focus();
|
self.focus = Focus::TaskList;
|
||||||
}
|
}
|
||||||
// Add our new shortcuts here
|
// Add our new shortcuts here
|
||||||
KeyCode::Char('c') => {
|
KeyCode::Char('c') => {
|
||||||
tasks_list.handle_key_event(key).ok();
|
tasks_list.handle_key_event(key).ok();
|
||||||
}
|
}
|
||||||
KeyCode::Char('u') | KeyCode::Char('d')
|
KeyCode::Char('u') | KeyCode::Char('d')
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL) =>
|
if key.modifiers.contains(KeyModifiers::CONTROL) =>
|
||||||
{
|
{
|
||||||
tasks_list.handle_key_event(key).ok();
|
tasks_list.handle_key_event(key).ok();
|
||||||
}
|
}
|
||||||
KeyCode::Char('b') => {
|
KeyCode::Char('b') => {
|
||||||
tasks_list.toggle_task_list();
|
tasks_list.toggle_task_list();
|
||||||
self.focus = tasks_list.get_focus();
|
self.focus = tasks_list.get_focus();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Forward other keys for interactivity, scrolling (j/k) etc
|
// Forward other keys for interactivity, scrolling (j/k) etc
|
||||||
tasks_list.handle_key_event(key).ok();
|
tasks_list.handle_key_event(key).ok();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
@ -462,7 +498,7 @@ impl App {
|
|||||||
match c {
|
match c {
|
||||||
'/' => {
|
'/' => {
|
||||||
if tasks_list.filter_mode {
|
if tasks_list.filter_mode {
|
||||||
tasks_list.exit_filter_mode();
|
tasks_list.persist_filter();
|
||||||
} else {
|
} else {
|
||||||
tasks_list.enter_filter_mode();
|
tasks_list.enter_filter_mode();
|
||||||
}
|
}
|
||||||
@ -509,6 +545,13 @@ impl App {
|
|||||||
self.focus = tasks_list.get_focus();
|
self.focus = tasks_list.get_focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::Enter if is_filter_mode => {
|
||||||
|
tasks_list.persist_filter();
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
tasks_list.focus_current_task_terminal_pane();
|
||||||
|
self.focus = tasks_list.get_focus();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Focus::MultipleOutput(_idx) => match key.code {
|
Focus::MultipleOutput(_idx) => match key.code {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ impl HelpPopup {
|
|||||||
let keybindings = vec![
|
let keybindings = vec![
|
||||||
// Misc
|
// Misc
|
||||||
("?", "Toggle this popup"),
|
("?", "Toggle this popup"),
|
||||||
("<ctrl>+c", "Quit the TUI"),
|
("q or <ctrl>+c", "Quit the TUI"),
|
||||||
("", ""),
|
("", ""),
|
||||||
// Navigation
|
// Navigation
|
||||||
("↑ or k", "Navigate/scroll task output up"),
|
("↑ or k", "Navigate/scroll task output up"),
|
||||||
@ -106,6 +106,8 @@ impl HelpPopup {
|
|||||||
("<esc>", "Clear filter"),
|
("<esc>", "Clear filter"),
|
||||||
("", ""),
|
("", ""),
|
||||||
// Output Controls
|
// Output Controls
|
||||||
|
("<enter>", "Open and focus terminal for task"),
|
||||||
|
("<esc>", "Set focus back to task list"),
|
||||||
("<space>", "Quick toggle a single output pane"),
|
("<space>", "Quick toggle a single output pane"),
|
||||||
("b", "Toggle task list visibility"),
|
("b", "Toggle task list visibility"),
|
||||||
("1", "Pin task to be shown in output pane 1"),
|
("1", "Pin task to be shown in output pane 1"),
|
||||||
@ -166,12 +168,7 @@ impl HelpPopup {
|
|||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
|
|
||||||
// Calculate the total visible length (excluding color codes)
|
// Calculate the total visible length (excluding color codes)
|
||||||
let visible_length = if key_parts.len() > 1 {
|
let visible_length = key.chars().count();
|
||||||
key_parts.iter().map(|s| s.len()).sum::<usize>() + 2
|
|
||||||
// for alignment
|
|
||||||
} else {
|
|
||||||
key.len()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add each key part with the appropriate styling
|
// Add each key part with the appropriate styling
|
||||||
for (i, part) in key_parts.iter().enumerate() {
|
for (i, part) in key_parts.iter().enumerate() {
|
||||||
@ -188,7 +185,7 @@ impl HelpPopup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add padding to align all descriptions
|
// Add padding to align all descriptions
|
||||||
let padding = " ".repeat(11usize.saturating_sub(visible_length));
|
let padding = " ".repeat(14usize.saturating_sub(visible_length));
|
||||||
spans.push(Span::raw(padding));
|
spans.push(Span::raw(padding));
|
||||||
|
|
||||||
// Add the separator and description
|
// Add the separator and description
|
||||||
|
|||||||
@ -36,7 +36,7 @@ impl HelpText {
|
|||||||
// Show minimal hint
|
// Show minimal hint
|
||||||
let hint = vec![
|
let hint = vec![
|
||||||
Span::styled("quit: ", base_style.fg(Color::DarkGray)),
|
Span::styled("quit: ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("<ctrl>+c", base_style.fg(Color::Cyan)),
|
Span::styled("q", base_style.fg(Color::Cyan)),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("help: ", base_style.fg(Color::DarkGray)),
|
Span::styled("help: ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("? ", base_style.fg(Color::Cyan)),
|
Span::styled("? ", base_style.fg(Color::Cyan)),
|
||||||
@ -53,7 +53,7 @@ impl HelpText {
|
|||||||
// Show full shortcuts
|
// Show full shortcuts
|
||||||
let shortcuts = vec![
|
let shortcuts = vec![
|
||||||
Span::styled("quit: ", base_style.fg(Color::DarkGray)),
|
Span::styled("quit: ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("<ctrl>+c", base_style.fg(Color::Cyan)),
|
Span::styled("q", base_style.fg(Color::Cyan)),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("help: ", base_style.fg(Color::DarkGray)),
|
Span::styled("help: ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("?", base_style.fg(Color::Cyan)),
|
Span::styled("?", base_style.fg(Color::Cyan)),
|
||||||
@ -70,8 +70,8 @@ impl HelpText {
|
|||||||
Span::styled(" or ", base_style.fg(Color::DarkGray)),
|
Span::styled(" or ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("2", base_style.fg(Color::Cyan)),
|
Span::styled("2", base_style.fg(Color::Cyan)),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("focus output: ", base_style.fg(Color::DarkGray)),
|
Span::styled("show output: ", base_style.fg(Color::DarkGray)),
|
||||||
Span::styled("<tab>", base_style.fg(Color::Cyan)),
|
Span::styled("<enter>", base_style.fg(Color::Cyan)),
|
||||||
];
|
];
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex, RwLock};
|
|||||||
use std::{any::Any, io};
|
use std::{any::Any, io};
|
||||||
use vt100_ctt::Parser;
|
use vt100_ctt::Parser;
|
||||||
|
|
||||||
use crate::native::tui::utils::{is_cache_hit, normalize_newlines, sort_task_items};
|
use crate::native::tui::utils::{normalize_newlines, sort_task_items};
|
||||||
use crate::native::tui::{
|
use crate::native::tui::{
|
||||||
action::Action, app::Focus, components::Component, pty::PtyInstance, utils,
|
action::Action, app::Focus, components::Component, pty::PtyInstance, utils,
|
||||||
};
|
};
|
||||||
@ -385,9 +385,7 @@ impl TasksList {
|
|||||||
/// If there is existing filter text that isn't persisted, persists it instead.
|
/// If there is existing filter text that isn't persisted, persists it instead.
|
||||||
pub fn enter_filter_mode(&mut self) {
|
pub fn enter_filter_mode(&mut self) {
|
||||||
if !self.filter_text.is_empty() && !self.filter_persisted {
|
if !self.filter_text.is_empty() && !self.filter_persisted {
|
||||||
// If we have filter text and it's not persisted, pressing / should persist it
|
self.persist_filter();
|
||||||
self.filter_persisted = true;
|
|
||||||
self.filter_mode = false;
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise enter normal filter mode
|
// Otherwise enter normal filter mode
|
||||||
self.filter_persisted = false;
|
self.filter_persisted = false;
|
||||||
@ -401,6 +399,11 @@ impl TasksList {
|
|||||||
self.filter_persisted = false;
|
self.filter_persisted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn persist_filter(&mut self) {
|
||||||
|
self.filter_persisted = true;
|
||||||
|
self.filter_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Clears the current filter and resets filter-related state.
|
/// Clears the current filter and resets filter-related state.
|
||||||
pub fn clear_filter(&mut self) {
|
pub fn clear_filter(&mut self) {
|
||||||
self.filter_mode = false;
|
self.filter_mode = false;
|
||||||
@ -676,6 +679,23 @@ impl TasksList {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focus_current_task_terminal_pane(&mut self) {
|
||||||
|
if let Some(task_name) = self.selection_manager.get_selected_task_name() {
|
||||||
|
// Find which pane contains this task
|
||||||
|
let pane_idx = self
|
||||||
|
.pane_tasks
|
||||||
|
.iter()
|
||||||
|
.position(|t| t.as_deref() == Some(task_name.as_str()))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.assign_current_task_to_pane(0);
|
||||||
|
0
|
||||||
|
});
|
||||||
|
// Set focus to this pane
|
||||||
|
self.focus = Focus::MultipleOutput(pane_idx);
|
||||||
|
self.focused_pane = Some(pane_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the table style based on the current focus state.
|
/// Gets the table style based on the current focus state.
|
||||||
/// Returns a dimmed style when focus is not on the task list.
|
/// Returns a dimmed style when focus is not on the task list.
|
||||||
fn get_table_style(&self) -> Style {
|
fn get_table_style(&self) -> Style {
|
||||||
@ -702,6 +722,13 @@ impl TasksList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the currently focused pane is in interactive mode.
|
||||||
|
pub fn set_interactive_mode(&mut self, interactive: bool) {
|
||||||
|
if let Focus::MultipleOutput(pane_idx) = self.focus {
|
||||||
|
self.terminal_pane_data[pane_idx].set_interactive(interactive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if the currently focused pane is in interactive mode.
|
/// Returns true if the currently focused pane is in interactive mode.
|
||||||
pub fn is_interactive_mode(&self) -> bool {
|
pub fn is_interactive_mode(&self) -> bool {
|
||||||
match self.focus {
|
match self.focus {
|
||||||
@ -938,6 +965,16 @@ impl TasksList {
|
|||||||
task_item.update_status(status);
|
task_item.update_status(status);
|
||||||
self.sort_tasks();
|
self.sort_tasks();
|
||||||
}
|
}
|
||||||
|
for (i, data) in self.terminal_pane_data.iter_mut().enumerate() {
|
||||||
|
if self.pane_tasks.as_ref()[i].clone().is_some_and(|id| id == task_id) {
|
||||||
|
let in_progress = status == TaskStatus::InProgress;
|
||||||
|
data.can_be_interactive = in_progress;
|
||||||
|
if !in_progress {
|
||||||
|
data.set_interactive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end_tasks(&mut self, task_results: Vec<TaskResult>) {
|
pub fn end_tasks(&mut self, task_results: Vec<TaskResult>) {
|
||||||
@ -1732,7 +1769,7 @@ impl Component for TasksList {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
" -> {} tasks filtered out. Press / to persist, <esc> to clear",
|
" -> {} tasks filtered out. Press <enter> to persist, <esc> to clear",
|
||||||
hidden_tasks
|
hidden_tasks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -2041,7 +2078,7 @@ impl Component for TasksList {
|
|||||||
{
|
{
|
||||||
let mut terminal_pane_data = &mut self.terminal_pane_data[1];
|
let mut terminal_pane_data = &mut self.terminal_pane_data[1];
|
||||||
terminal_pane_data.is_continuous = task.continuous;
|
terminal_pane_data.is_continuous = task.continuous;
|
||||||
terminal_pane_data.is_cache_hit = is_cache_hit(task.status);
|
// terminal_pane_data.is_cache_hit = is_cache_hit(task.status);
|
||||||
|
|
||||||
let mut has_pty = false;
|
let mut has_pty = false;
|
||||||
if let Some(pty) = self.pty_instances.get(task_name) {
|
if let Some(pty) = self.pty_instances.get(task_name) {
|
||||||
@ -2084,7 +2121,7 @@ impl Component for TasksList {
|
|||||||
if let Some(task) = self.tasks.iter_mut().find(|t| t.name == *task_name) {
|
if let Some(task) = self.tasks.iter_mut().find(|t| t.name == *task_name) {
|
||||||
let mut terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
|
let mut terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
|
||||||
terminal_pane_data.is_continuous = task.continuous;
|
terminal_pane_data.is_continuous = task.continuous;
|
||||||
terminal_pane_data.is_cache_hit = is_cache_hit(task.status);
|
// terminal_pane_data.is_cache_hit = is_cache_hit(task.status);
|
||||||
|
|
||||||
let mut has_pty = false;
|
let mut has_pty = false;
|
||||||
if let Some(pty) = self.pty_instances.get(task_name) {
|
if let Some(pty) = self.pty_instances.get(task_name) {
|
||||||
@ -2126,7 +2163,7 @@ impl Component for TasksList {
|
|||||||
{
|
{
|
||||||
let mut terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
|
let mut terminal_pane_data = &mut self.terminal_pane_data[pane_idx];
|
||||||
terminal_pane_data.is_continuous = task.continuous;
|
terminal_pane_data.is_continuous = task.continuous;
|
||||||
terminal_pane_data.is_cache_hit = is_cache_hit(task.status);
|
// terminal_pane_data.is_cache_hit = is_cache_hit(task.status);
|
||||||
|
|
||||||
let mut has_pty = false;
|
let mut has_pty = false;
|
||||||
if let Some(pty) = self.pty_instances.get(task_name) {
|
if let Some(pty) = self.pty_instances.get(task_name) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ pub struct TerminalPaneData {
|
|||||||
pub pty: Option<Arc<PtyInstance>>,
|
pub pty: Option<Arc<PtyInstance>>,
|
||||||
pub is_interactive: bool,
|
pub is_interactive: bool,
|
||||||
pub is_continuous: bool,
|
pub is_continuous: bool,
|
||||||
pub is_cache_hit: bool,
|
pub can_be_interactive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalPaneData {
|
impl TerminalPaneData {
|
||||||
@ -30,7 +30,7 @@ impl TerminalPaneData {
|
|||||||
pty: None,
|
pty: None,
|
||||||
is_interactive: false,
|
is_interactive: false,
|
||||||
is_continuous: false,
|
is_continuous: false,
|
||||||
is_cache_hit: false,
|
can_be_interactive: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,20 +91,11 @@ impl TerminalPaneData {
|
|||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Handle 'i' to enter interactive mode for non cache hit tasks
|
// Handle 'i' to enter interactive mode for in progress tasks
|
||||||
KeyCode::Char('i') if !self.is_cache_hit && !self.is_interactive => {
|
KeyCode::Char('i') if self.can_be_interactive && !self.is_interactive => {
|
||||||
self.set_interactive(true);
|
self.set_interactive(true);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Handle Ctrl+Z to exit interactive mode
|
|
||||||
KeyCode::Char('z')
|
|
||||||
if key.modifiers == KeyModifiers::CONTROL
|
|
||||||
&& !self.is_cache_hit
|
|
||||||
&& self.is_interactive =>
|
|
||||||
{
|
|
||||||
self.set_interactive(false);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// Only send input to PTY if we're in interactive mode
|
// Only send input to PTY if we're in interactive mode
|
||||||
_ if self.is_interactive => match key.code {
|
_ if self.is_interactive => match key.code {
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
@ -435,7 +426,7 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show interactive/readonly status for focused, non-cache hit, tasks
|
// Show interactive/readonly status for focused, non-cache hit, tasks
|
||||||
if state.is_focused && !pty_data.is_cache_hit {
|
if state.task_status == TaskStatus::InProgress && state.is_focused {
|
||||||
// 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![
|
||||||
|
|||||||
@ -202,11 +202,7 @@ impl AppLifeCycle {
|
|||||||
// Handle events using our Tui abstraction
|
// Handle events using our Tui abstraction
|
||||||
if let Some(event) = tui.next().await {
|
if let Some(event) = tui.next().await {
|
||||||
if let Ok(mut app) = app_mutex.lock() {
|
if let Ok(mut app) = app_mutex.lock() {
|
||||||
if let Ok(true) = app.handle_event(event, &action_tx) {
|
let _ = app.handle_event(event, &action_tx);
|
||||||
tui.exit().ok();
|
|
||||||
app.call_done_callback();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should quit based on the timer
|
// Check if we should quit based on the timer
|
||||||
if let Some(quit_time) = app.quit_at {
|
if let Some(quit_time) = app.quit_at {
|
||||||
|
|||||||
@ -32,13 +32,6 @@ pub fn normalize_newlines(input: &[u8]) -> Vec<u8> {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_cache_hit(status: TaskStatus) -> bool {
|
|
||||||
matches!(
|
|
||||||
status,
|
|
||||||
TaskStatus::LocalCacheKeptExisting | TaskStatus::LocalCache | TaskStatus::RemoteCache
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sorts a list of TaskItems with a stable, total ordering.
|
/// Sorts a list of TaskItems with a stable, total ordering.
|
||||||
///
|
///
|
||||||
/// The sort order is:
|
/// The sort order is:
|
||||||
|
|||||||
@ -680,6 +680,12 @@ export class TaskOrchestrator {
|
|||||||
|
|
||||||
this.runningContinuousTasks.set(task.id, runningTask);
|
this.runningContinuousTasks.set(task.id, runningTask);
|
||||||
runningTask.onExit(() => {
|
runningTask.onExit(() => {
|
||||||
|
if (this.tuiEnabled) {
|
||||||
|
this.options.lifeCycle.setTaskStatus(
|
||||||
|
task.id,
|
||||||
|
NativeTaskStatus.Stopped
|
||||||
|
);
|
||||||
|
}
|
||||||
this.runningContinuousTasks.delete(task.id);
|
this.runningContinuousTasks.delete(task.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -744,6 +750,9 @@ export class TaskOrchestrator {
|
|||||||
this.runningContinuousTasks.set(task.id, childProcess);
|
this.runningContinuousTasks.set(task.id, childProcess);
|
||||||
|
|
||||||
childProcess.onExit(() => {
|
childProcess.onExit(() => {
|
||||||
|
if (this.tuiEnabled) {
|
||||||
|
this.options.lifeCycle.setTaskStatus(task.id, NativeTaskStatus.Stopped);
|
||||||
|
}
|
||||||
this.runningTasksService.removeRunningTask(task.id);
|
this.runningTasksService.removeRunningTask(task.id);
|
||||||
this.runningContinuousTasks.delete(task.id);
|
this.runningContinuousTasks.delete(task.id);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user