fix(core): clearer tui colors on light themes (#31095)
<!-- 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 --> Light themes are not super clear with the new TUI. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Light themes are much clearer with the new TUI. Internal loom shared on slack for full details. ## 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
b65216387e
commit
0d53604b5a
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -94,7 +94,7 @@ jobs:
|
|||||||
pnpm nx run-many -t check-imports check-commit check-lock-files check-codeowners --parallel=1 --no-dte &
|
pnpm nx run-many -t check-imports check-commit check-lock-files check-codeowners --parallel=1 --no-dte &
|
||||||
pids+=($!)
|
pids+=($!)
|
||||||
|
|
||||||
pnpm nx affected --targets=lint,test,build,e2e,e2e-ci,format-native &
|
pnpm nx affected --targets=lint,test,build,e2e,e2e-ci,format-native,lint-native &
|
||||||
pids+=($!)
|
pids+=($!)
|
||||||
|
|
||||||
for pid in "${pids[@]}"; do
|
for pid in "${pids[@]}"; do
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
pnpm check-lock-files
|
pnpm check-lock-files
|
||||||
|
pnpm pretty-quick --check
|
||||||
|
NX_TUI=false pnpm nx format-native nx
|
||||||
|
NX_TUI=false pnpm nx lint-native nx
|
||||||
pnpm check-commit
|
pnpm check-commit
|
||||||
pnpm documentation
|
pnpm documentation
|
||||||
pnpm pretty-quick --check
|
|
||||||
pnpm nx format-native nx
|
|
||||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -2202,6 +2202,7 @@ dependencies = [
|
|||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"terminal-colorsaurus",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@ -3550,6 +3551,32 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal-colorsaurus"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7afe4c174a3cbfb52ebcb11b28965daf74fe9111d4e07e40689d05af06e26e8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"mio 1.0.3",
|
||||||
|
"terminal-trx",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
"xterm-color",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal-trx"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "975b4233aefa1b02456d5e53b22c61653c743e308c51cf4181191d8ce41753ab"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termios"
|
name = "termios"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -4636,6 +4663,12 @@ dependencies = [
|
|||||||
"rustix 1.0.5",
|
"rustix 1.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xterm-color"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xxhash-rust"
|
name = "xxhash-rust"
|
||||||
version = "0.8.10"
|
version = "0.8.10"
|
||||||
|
|||||||
4
clippy.toml
Normal file
4
clippy.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
disallowed-types = [
|
||||||
|
# We need to ensure adjustments for light and dark themes are applied appropriately
|
||||||
|
{ path = "ratatui::style::Color", reason = "Use our utils from crate::native::tui::colors instead to ensure appropriate light/dark theme support" },
|
||||||
|
]
|
||||||
@ -47,6 +47,7 @@ swc_ecma_ast = "0.107.0"
|
|||||||
sysinfo = "0.33.1"
|
sysinfo = "0.33.1"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
|
terminal-colorsaurus = "0.4.0"
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||||
|
|||||||
@ -152,6 +152,19 @@
|
|||||||
"args": ["--all"]
|
"args": ["--all"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"lint-native": {
|
||||||
|
"command": "cargo clippy",
|
||||||
|
"cache": true,
|
||||||
|
"options": {
|
||||||
|
"cwd": "{projectRoot}/src/native",
|
||||||
|
"args": ["--frozen"]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"fix": {
|
||||||
|
"args": []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
#![deny(clippy::disallowed_types)]
|
||||||
#![cfg_attr(target_os = "wasi", feature(wasi_ext))]
|
#![cfg_attr(target_os = "wasi", feature(wasi_ext))]
|
||||||
// add all the napi macros globally
|
// add all the napi macros globally
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use watchexec::command;
|
|
||||||
|
|
||||||
use super::child_process::ChildProcess;
|
use super::child_process::ChildProcess;
|
||||||
use super::os;
|
use super::os;
|
||||||
use super::pseudo_terminal::PseudoTerminal;
|
use super::pseudo_terminal::{PseudoTerminal, PseudoTerminalOptions};
|
||||||
use crate::native::logger::enable_logger;
|
use crate::native::logger::enable_logger;
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
@ -18,7 +17,7 @@ impl RustPseudoTerminal {
|
|||||||
pub fn new() -> napi::Result<Self> {
|
pub fn new() -> napi::Result<Self> {
|
||||||
enable_logger();
|
enable_logger();
|
||||||
|
|
||||||
let pseudo_terminal = PseudoTerminal::default()?;
|
let pseudo_terminal = PseudoTerminal::new(PseudoTerminalOptions::default())?;
|
||||||
|
|
||||||
Ok(Self { pseudo_terminal })
|
Ok(Self { pseudo_terminal })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use tracing::trace;
|
|||||||
|
|
||||||
use super::child_process::ChildProcess;
|
use super::child_process::ChildProcess;
|
||||||
use super::os;
|
use super::os;
|
||||||
use super::pseudo_terminal::PseudoTerminal;
|
use super::pseudo_terminal::{PseudoTerminal, PseudoTerminalOptions};
|
||||||
use crate::native::logger::enable_logger;
|
use crate::native::logger::enable_logger;
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
@ -18,7 +18,7 @@ impl RustPseudoTerminal {
|
|||||||
pub fn new() -> napi::Result<Self> {
|
pub fn new() -> napi::Result<Self> {
|
||||||
enable_logger();
|
enable_logger();
|
||||||
|
|
||||||
let pseudo_terminal = PseudoTerminal::default()?;
|
let pseudo_terminal = PseudoTerminal::new(PseudoTerminalOptions::default())?;
|
||||||
|
|
||||||
Ok(Self { pseudo_terminal })
|
Ok(Self { pseudo_terminal })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,7 +130,7 @@ impl PseudoTerminal {
|
|||||||
let write_buf = content.as_bytes();
|
let write_buf = content.as_bytes();
|
||||||
debug!("Escaped Stdout: {:?}", write_buf.escape_ascii().to_string());
|
debug!("Escaped Stdout: {:?}", write_buf.escape_ascii().to_string());
|
||||||
|
|
||||||
while let Err(e) = stdout.write_all(&write_buf) {
|
while let Err(e) = stdout.write_all(write_buf) {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
std::io::ErrorKind::Interrupted => {
|
std::io::ErrorKind::Interrupted => {
|
||||||
if !logged_interrupted_error {
|
if !logged_interrupted_error {
|
||||||
@ -172,10 +172,6 @@ impl PseudoTerminal {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default() -> Result<PseudoTerminal> {
|
|
||||||
Self::new(PseudoTerminalOptions::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_command(
|
pub fn run_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
command: String,
|
command: String,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use napi::bindgen_prelude::External;
|
|||||||
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
|
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
|
||||||
use ratatui::layout::{Alignment, Rect, Size};
|
use ratatui::layout::{Alignment, Rect, Size};
|
||||||
use ratatui::style::Modifier;
|
use ratatui::style::Modifier;
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::Style;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -35,6 +35,7 @@ use super::components::Component;
|
|||||||
use super::config::TuiConfig;
|
use super::config::TuiConfig;
|
||||||
use super::lifecycle::RunMode;
|
use super::lifecycle::RunMode;
|
||||||
use super::pty::PtyInstance;
|
use super::pty::PtyInstance;
|
||||||
|
use super::theme::THEME;
|
||||||
use super::tui;
|
use super::tui;
|
||||||
use super::utils::normalize_newlines;
|
use super::utils::normalize_newlines;
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ impl App {
|
|||||||
layout_manager: LayoutManager::new(task_count),
|
layout_manager: LayoutManager::new(task_count),
|
||||||
frame_area: None,
|
frame_area: None,
|
||||||
layout_areas: None,
|
layout_areas: None,
|
||||||
terminal_pane_data: [main_terminal_pane_data, TerminalPaneData::default()],
|
terminal_pane_data: [main_terminal_pane_data, TerminalPaneData::new()],
|
||||||
spacebar_mode: false,
|
spacebar_mode: false,
|
||||||
pane_tasks: [None, None],
|
pane_tasks: [None, None],
|
||||||
task_list_hidden: false,
|
task_list_hidden: false,
|
||||||
@ -255,7 +256,7 @@ impl App {
|
|||||||
let pty = self
|
let pty = self
|
||||||
.pty_instances
|
.pty_instances
|
||||||
.get_mut(&task_id)
|
.get_mut(&task_id)
|
||||||
.expect(&format!("{} has not been registered yet.", task_id));
|
.unwrap_or_else(|| panic!("{} has not been registered yet.", task_id));
|
||||||
pty.process_output(output.as_bytes());
|
pty.process_output(output.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,11 +753,11 @@ impl App {
|
|||||||
tui.draw(|f| {
|
tui.draw(|f| {
|
||||||
let area = f.area();
|
let area = f.area();
|
||||||
// Cache the frame area if it's never been set before (will be updated in subsequent resize events if necessary)
|
// Cache the frame area if it's never been set before (will be updated in subsequent resize events if necessary)
|
||||||
if !self.frame_area.is_some() {
|
if self.frame_area.is_none() {
|
||||||
self.frame_area = Some(area);
|
self.frame_area = Some(area);
|
||||||
}
|
}
|
||||||
// Determine the required layout areas for the tasks list and terminal panes using the LayoutManager
|
// Determine the required layout areas for the tasks list and terminal panes using the LayoutManager
|
||||||
if !self.layout_areas.is_some() {
|
if self.layout_areas.is_none() {
|
||||||
self.recalculate_layout_areas();
|
self.recalculate_layout_areas();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -777,8 +778,8 @@ impl App {
|
|||||||
" NX ",
|
" NX ",
|
||||||
Style::reset()
|
Style::reset()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.bg(Color::Red)
|
.bg(THEME.error)
|
||||||
.fg(Color::Black),
|
.fg(THEME.primary_fg),
|
||||||
),
|
),
|
||||||
Span::raw(" Terminal too small "),
|
Span::raw(" Terminal too small "),
|
||||||
]);
|
]);
|
||||||
@ -879,9 +880,9 @@ impl App {
|
|||||||
Block::default()
|
Block::default()
|
||||||
.title(format!(" Output {} ", pane_idx + 1))
|
.title(format!(" Output {} ", pane_idx + 1))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::DarkGray)),
|
.border_style(Style::default().fg(THEME.secondary_fg)),
|
||||||
)
|
)
|
||||||
.style(Style::default().fg(Color::DarkGray))
|
.style(Style::default().fg(THEME.secondary_fg))
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
f.render_widget(placeholder, *pane_area);
|
f.render_widget(placeholder, *pane_area);
|
||||||
@ -1348,7 +1349,7 @@ impl App {
|
|||||||
|
|
||||||
/// Actually processes the resize event by updating PTY dimensions.
|
/// Actually processes the resize event by updating PTY dimensions.
|
||||||
fn handle_pty_resize(&mut self) -> io::Result<()> {
|
fn handle_pty_resize(&mut self) -> io::Result<()> {
|
||||||
if !self.layout_areas.is_some() {
|
if self.layout_areas.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, BorderType, Borders, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
|
Block, BorderType, Borders, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
|
||||||
@ -12,6 +12,8 @@ use ratatui::{
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::native::tui::theme::THEME;
|
||||||
|
|
||||||
use super::Component;
|
use super::Component;
|
||||||
|
|
||||||
pub struct CountdownPopup {
|
pub struct CountdownPopup {
|
||||||
@ -143,27 +145,27 @@ impl CountdownPopup {
|
|||||||
|
|
||||||
let time_remaining = seconds_remaining + 1;
|
let time_remaining = seconds_remaining + 1;
|
||||||
|
|
||||||
let content = vec![
|
let content = [
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("• Press ", Style::default().fg(Color::DarkGray)),
|
Span::styled("• Press ", Style::default().fg(THEME.secondary_fg)),
|
||||||
Span::styled("q to exit immediately ", Style::default().fg(Color::Cyan)),
|
Span::styled("q to exit immediately ", Style::default().fg(THEME.info)),
|
||||||
Span::styled("or ", Style::default().fg(Color::DarkGray)),
|
Span::styled("or ", Style::default().fg(THEME.secondary_fg)),
|
||||||
Span::styled("any other key ", Style::default().fg(Color::Cyan)),
|
Span::styled("any other key ", Style::default().fg(THEME.info)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"to keep the TUI running and interactively explore the results.",
|
"to keep the TUI running and interactively explore the results.",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(THEME.secondary_fg),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"• Learn how to configure auto-exit and more in the docs: ",
|
"• Learn how to configure auto-exit and more in the docs: ",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(THEME.secondary_fg),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
// NOTE: I tried OSC 8 sequences here but they broke the layout, see: https://github.com/ratatui/ratatui/issues/1028
|
// NOTE: I tried OSC 8 sequences here but they broke the layout, see: https://github.com/ratatui/ratatui/issues/1028
|
||||||
"https://nx.dev/terminal-ui",
|
"https://nx.dev/terminal-ui",
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(THEME.info),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
@ -175,20 +177,20 @@ impl CountdownPopup {
|
|||||||
" NX ",
|
" NX ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.bg(Color::Cyan)
|
.bg(THEME.info)
|
||||||
.fg(Color::Black),
|
.fg(THEME.primary_fg),
|
||||||
),
|
),
|
||||||
Span::styled(" Exiting in ", Style::default().fg(Color::White)),
|
Span::styled(" Exiting in ", Style::default().fg(THEME.primary_fg)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}", time_remaining),
|
format!("{}", time_remaining),
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(THEME.info),
|
||||||
),
|
),
|
||||||
Span::styled("... ", Style::default().fg(Color::White)),
|
Span::styled("... ", Style::default().fg(THEME.primary_fg)),
|
||||||
]))
|
]))
|
||||||
.title_alignment(Alignment::Left)
|
.title_alignment(Alignment::Left)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Plain)
|
.border_type(BorderType::Plain)
|
||||||
.border_style(Style::default().fg(Color::Cyan))
|
.border_style(Style::default().fg(THEME.info))
|
||||||
.padding(Padding::proportional(1));
|
.padding(Padding::proportional(1));
|
||||||
|
|
||||||
// Get the inner area
|
// Get the inner area
|
||||||
@ -264,14 +266,14 @@ impl CountdownPopup {
|
|||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(top_text)
|
Paragraph::new(top_text)
|
||||||
.alignment(Alignment::Right)
|
.alignment(Alignment::Right)
|
||||||
.style(Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(THEME.info)),
|
||||||
top_right_area,
|
top_right_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(bottom_text)
|
Paragraph::new(bottom_text)
|
||||||
.alignment(Alignment::Right)
|
.alignment(Alignment::Right)
|
||||||
.style(Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(THEME.info)),
|
||||||
bottom_right_area,
|
bottom_right_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -279,27 +281,13 @@ impl CountdownPopup {
|
|||||||
.orientation(ScrollbarOrientation::VerticalRight)
|
.orientation(ScrollbarOrientation::VerticalRight)
|
||||||
.begin_symbol(Some("↑"))
|
.begin_symbol(Some("↑"))
|
||||||
.end_symbol(Some("↓"))
|
.end_symbol(Some("↓"))
|
||||||
.style(Style::default().fg(Color::Cyan));
|
.style(Style::default().fg(THEME.info));
|
||||||
|
|
||||||
f.render_stateful_widget(scrollbar, popup_area, &mut self.scrollbar_state);
|
f.render_stateful_widget(scrollbar, popup_area, &mut self.scrollbar_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for CountdownPopup {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
visible: self.visible,
|
|
||||||
start_time: self.start_time,
|
|
||||||
duration: self.duration,
|
|
||||||
scroll_offset: self.scroll_offset,
|
|
||||||
scrollbar_state: self.scrollbar_state,
|
|
||||||
content_height: self.content_height,
|
|
||||||
viewport_height: self.viewport_height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for CountdownPopup {
|
impl Component for CountdownPopup {
|
||||||
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> {
|
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> {
|
||||||
if self.visible {
|
if self.visible {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, BorderType, Borders, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
|
Block, BorderType, Borders, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
|
||||||
@ -11,9 +11,9 @@ use ratatui::{
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::native::tui::action::Action;
|
|
||||||
|
|
||||||
use super::{Component, Frame};
|
use super::{Component, Frame};
|
||||||
|
use crate::native::tui::action::Action;
|
||||||
|
use crate::native::tui::theme::THEME;
|
||||||
|
|
||||||
pub struct HelpPopup {
|
pub struct HelpPopup {
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
@ -154,29 +154,29 @@ impl HelpPopup {
|
|||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"Thanks for using Nx! To get the most out of this terminal UI, please check out the docs: ",
|
"Thanks for using Nx! To get the most out of this terminal UI, please check out the docs: ",
|
||||||
Style::default().fg(Color::White),
|
Style::default().fg(THEME.primary_fg),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
// NOTE: I tried OSC 8 sequences here but they broke the layout, see: https://github.com/ratatui/ratatui/issues/1028
|
// NOTE: I tried OSC 8 sequences here but they broke the layout, see: https://github.com/ratatui/ratatui/issues/1028
|
||||||
"https://nx.dev/terminal-ui",
|
"https://nx.dev/terminal-ui",
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(THEME.info),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"If you are finding Nx useful, please consider giving it a star on GitHub, it means a lot: ",
|
"If you are finding Nx useful, please consider giving it a star on GitHub, it means a lot: ",
|
||||||
Style::default().fg(Color::White),
|
Style::default().fg(THEME.primary_fg),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
// NOTE: I tried OSC 8 sequences here but they broke the layout, see: https://github.com/ratatui/ratatui/issues/1028
|
// NOTE: I tried OSC 8 sequences here but they broke the layout, see: https://github.com/ratatui/ratatui/issues/1028
|
||||||
"https://github.com/nrwl/nx",
|
"https://github.com/nrwl/nx",
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(THEME.info),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
Line::from(""), // Empty line for spacing
|
Line::from(""), // Empty line for spacing
|
||||||
Line::from(vec![Span::styled(
|
Line::from(vec![Span::styled(
|
||||||
"Available keyboard shortcuts:",
|
"Available keyboard shortcuts:",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(THEME.secondary_fg),
|
||||||
)]),
|
)]),
|
||||||
Line::from(""), // Empty line for spacing
|
Line::from(""), // Empty line for spacing
|
||||||
];
|
];
|
||||||
@ -201,12 +201,12 @@ impl HelpPopup {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
" or ",
|
" or ",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(THEME.secondary_fg),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
part.to_string(),
|
part.to_string(),
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(THEME.info),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,8 +215,11 @@ impl HelpPopup {
|
|||||||
spans.push(Span::raw(padding));
|
spans.push(Span::raw(padding));
|
||||||
|
|
||||||
// Add the separator and description
|
// Add the separator and description
|
||||||
spans.push(Span::styled("= ", Style::default().fg(Color::DarkGray)));
|
spans.push(Span::styled(
|
||||||
spans.push(Span::styled(desc, Style::default().fg(Color::White)));
|
"= ",
|
||||||
|
Style::default().fg(THEME.secondary_fg),
|
||||||
|
));
|
||||||
|
spans.push(Span::styled(desc, Style::default().fg(THEME.primary_fg)));
|
||||||
|
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
}
|
}
|
||||||
@ -232,15 +235,15 @@ impl HelpPopup {
|
|||||||
" NX ",
|
" NX ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.bg(Color::Cyan)
|
.bg(THEME.info)
|
||||||
.fg(Color::Black),
|
.fg(THEME.primary_fg),
|
||||||
),
|
),
|
||||||
Span::styled(" Help ", Style::default().fg(Color::White)),
|
Span::styled(" Help ", Style::default().fg(THEME.primary_fg)),
|
||||||
]))
|
]))
|
||||||
.title_alignment(Alignment::Left)
|
.title_alignment(Alignment::Left)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Plain)
|
.border_type(BorderType::Plain)
|
||||||
.border_style(Style::default().fg(Color::Cyan))
|
.border_style(Style::default().fg(THEME.info))
|
||||||
.padding(Padding::proportional(1));
|
.padding(Padding::proportional(1));
|
||||||
|
|
||||||
let inner_area = block.inner(popup_area);
|
let inner_area = block.inner(popup_area);
|
||||||
@ -318,14 +321,14 @@ impl HelpPopup {
|
|||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(top_text)
|
Paragraph::new(top_text)
|
||||||
.alignment(Alignment::Right)
|
.alignment(Alignment::Right)
|
||||||
.style(Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(THEME.info)),
|
||||||
top_right_area,
|
top_right_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(bottom_text)
|
Paragraph::new(bottom_text)
|
||||||
.alignment(Alignment::Right)
|
.alignment(Alignment::Right)
|
||||||
.style(Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(THEME.info)),
|
||||||
bottom_right_area,
|
bottom_right_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -333,26 +336,13 @@ impl HelpPopup {
|
|||||||
.orientation(ScrollbarOrientation::VerticalRight)
|
.orientation(ScrollbarOrientation::VerticalRight)
|
||||||
.begin_symbol(Some("↑"))
|
.begin_symbol(Some("↑"))
|
||||||
.end_symbol(Some("↓"))
|
.end_symbol(Some("↓"))
|
||||||
.style(Style::default().fg(Color::Cyan));
|
.style(Style::default().fg(THEME.info));
|
||||||
|
|
||||||
f.render_stateful_widget(scrollbar, popup_area, &mut self.scrollbar_state);
|
f.render_stateful_widget(scrollbar, popup_area, &mut self.scrollbar_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for HelpPopup {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
scroll_offset: self.scroll_offset,
|
|
||||||
scrollbar_state: self.scrollbar_state,
|
|
||||||
content_height: self.content_height,
|
|
||||||
viewport_height: self.viewport_height,
|
|
||||||
visible: self.visible,
|
|
||||||
action_tx: self.action_tx.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for HelpPopup {
|
impl Component for HelpPopup {
|
||||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||||
self.action_tx = Some(tx);
|
self.action_tx = Some(tx);
|
||||||
@ -367,12 +357,9 @@ impl Component for HelpPopup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||||
match action {
|
if let Action::Resize(w, h) = action {
|
||||||
Action::Resize(w, h) => {
|
|
||||||
self.handle_resize(w, h);
|
self.handle_resize(w, h);
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::native::tui::theme::THEME;
|
||||||
|
|
||||||
pub struct HelpText {
|
pub struct HelpText {
|
||||||
collapsed_mode: bool,
|
collapsed_mode: bool,
|
||||||
is_dimmed: bool,
|
is_dimmed: bool,
|
||||||
@ -48,15 +50,17 @@ impl HelpText {
|
|||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
|
let key_style = base_style.fg(THEME.info);
|
||||||
|
let label_style = base_style.fg(THEME.secondary_fg);
|
||||||
|
|
||||||
if self.collapsed_mode {
|
if self.collapsed_mode {
|
||||||
// Show minimal hint
|
// Show minimal hint
|
||||||
let hint = vec![
|
let hint = vec![
|
||||||
Span::styled("quit: ", base_style.fg(Color::DarkGray)),
|
Span::styled("quit: ", label_style),
|
||||||
Span::styled("q", base_style.fg(Color::Cyan)),
|
Span::styled("q", key_style),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", label_style),
|
||||||
Span::styled("help: ", base_style.fg(Color::DarkGray)),
|
Span::styled("help: ", label_style),
|
||||||
Span::styled("?", base_style.fg(Color::Cyan)),
|
Span::styled("?", key_style),
|
||||||
];
|
];
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(Line::from(hint)).alignment(if self.align_left {
|
Paragraph::new(Line::from(hint)).alignment(if self.align_left {
|
||||||
@ -69,26 +73,26 @@ impl HelpText {
|
|||||||
} else {
|
} else {
|
||||||
// Show full shortcuts
|
// Show full shortcuts
|
||||||
let shortcuts = vec![
|
let shortcuts = vec![
|
||||||
Span::styled("quit: ", base_style.fg(Color::DarkGray)),
|
Span::styled("quit: ", label_style),
|
||||||
Span::styled("q", base_style.fg(Color::Cyan)),
|
Span::styled("q", key_style),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", label_style),
|
||||||
Span::styled("help: ", base_style.fg(Color::DarkGray)),
|
Span::styled("help: ", label_style),
|
||||||
Span::styled("?", base_style.fg(Color::Cyan)),
|
Span::styled("?", key_style),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", label_style),
|
||||||
Span::styled("navigate: ", base_style.fg(Color::DarkGray)),
|
Span::styled("navigate: ", label_style),
|
||||||
Span::styled("↑ ↓", base_style.fg(Color::Cyan)),
|
Span::styled("↑ ↓", key_style),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", label_style),
|
||||||
Span::styled("filter: ", base_style.fg(Color::DarkGray)),
|
Span::styled("filter: ", label_style),
|
||||||
Span::styled("/", base_style.fg(Color::Cyan)),
|
Span::styled("/", key_style),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", label_style),
|
||||||
Span::styled("pin output: ", base_style.fg(Color::DarkGray)),
|
Span::styled("pin output: ", label_style),
|
||||||
Span::styled("", base_style.fg(Color::DarkGray)),
|
Span::styled("", label_style),
|
||||||
Span::styled("1", base_style.fg(Color::Cyan)),
|
Span::styled("1", key_style),
|
||||||
Span::styled(" or ", base_style.fg(Color::DarkGray)),
|
Span::styled(" or ", label_style),
|
||||||
Span::styled("2", base_style.fg(Color::Cyan)),
|
Span::styled("2", key_style),
|
||||||
Span::styled(" ", base_style.fg(Color::DarkGray)),
|
Span::styled(" ", label_style),
|
||||||
Span::styled("show output: ", base_style.fg(Color::DarkGray)),
|
Span::styled("show output: ", label_style),
|
||||||
Span::styled("<enter>", base_style.fg(Color::Cyan)),
|
Span::styled("<enter>", key_style),
|
||||||
];
|
];
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
|
|||||||
@ -739,7 +739,6 @@ mod tests {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod visual_tests {
|
mod visual_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ratatui::style::{Color, Style};
|
|
||||||
use ratatui::widgets::{Block, Borders};
|
use ratatui::widgets::{Block, Borders};
|
||||||
use ratatui::{backend::TestBackend, Terminal};
|
use ratatui::{backend::TestBackend, Terminal};
|
||||||
|
|
||||||
@ -758,10 +757,8 @@ mod tests {
|
|||||||
|
|
||||||
// Render task list if visible
|
// Render task list if visible
|
||||||
if let Some(task_list_area) = areas.task_list {
|
if let Some(task_list_area) = areas.task_list {
|
||||||
let task_list_block = Block::default()
|
let task_list_block =
|
||||||
.title("Task List")
|
Block::default().title("Task List").borders(Borders::ALL);
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Green));
|
|
||||||
|
|
||||||
frame.render_widget(task_list_block, task_list_area);
|
frame.render_widget(task_list_block, task_list_area);
|
||||||
}
|
}
|
||||||
@ -770,8 +767,7 @@ mod tests {
|
|||||||
for (i, pane_area) in areas.terminal_panes.iter().enumerate() {
|
for (i, pane_area) in areas.terminal_panes.iter().enumerate() {
|
||||||
let pane_block = Block::default()
|
let pane_block = Block::default()
|
||||||
.title(format!("Terminal Pane {}", i + 1))
|
.title(format!("Terminal Pane {}", i + 1))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL);
|
||||||
.border_style(Style::default().fg(Color::Yellow));
|
|
||||||
|
|
||||||
frame.render_widget(pane_block, *pane_area);
|
frame.render_widget(pane_block, *pane_area);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
use crate::native::tui::theme::THEME;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Frame,
|
Frame,
|
||||||
@ -52,9 +53,9 @@ impl Pagination {
|
|||||||
|
|
||||||
// Left arrow - dim if we're on the first page
|
// Left arrow - dim if we're on the first page
|
||||||
let left_arrow = if current_page == 0 {
|
let left_arrow = if current_page == 0 {
|
||||||
Span::styled("←", base_style.fg(Color::Cyan).add_modifier(Modifier::DIM))
|
Span::styled("←", base_style.fg(THEME.info).add_modifier(Modifier::DIM))
|
||||||
} else {
|
} else {
|
||||||
Span::styled("←", base_style.fg(Color::Cyan))
|
Span::styled("←", base_style.fg(THEME.info))
|
||||||
};
|
};
|
||||||
spans.push(left_arrow);
|
spans.push(left_arrow);
|
||||||
|
|
||||||
@ -62,15 +63,15 @@ impl Pagination {
|
|||||||
spans.push(Span::raw(" "));
|
spans.push(Span::raw(" "));
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
format!("{}/{}", current_page + 1, total_pages),
|
format!("{}/{}", current_page + 1, total_pages),
|
||||||
base_style.fg(Color::DarkGray),
|
base_style.fg(THEME.secondary_fg),
|
||||||
));
|
));
|
||||||
spans.push(Span::raw(" "));
|
spans.push(Span::raw(" "));
|
||||||
|
|
||||||
// Right arrow - dim if we're on the last page
|
// Right arrow - dim if we're on the last page
|
||||||
let right_arrow = if current_page >= total_pages.saturating_sub(1) {
|
let right_arrow = if current_page >= total_pages.saturating_sub(1) {
|
||||||
Span::styled("→", base_style.fg(Color::Cyan).add_modifier(Modifier::DIM))
|
Span::styled("→", base_style.fg(THEME.info).add_modifier(Modifier::DIM))
|
||||||
} else {
|
} else {
|
||||||
Span::styled("→", base_style.fg(Color::Cyan))
|
Span::styled("→", base_style.fg(THEME.info))
|
||||||
};
|
};
|
||||||
spans.push(right_arrow);
|
spans.push(right_arrow);
|
||||||
|
|
||||||
|
|||||||
@ -214,7 +214,7 @@ impl TaskSelectionManager {
|
|||||||
pub fn is_selected(&self, task_name: &str) -> bool {
|
pub fn is_selected(&self, task_name: &str) -> bool {
|
||||||
self.selected_task_name
|
self.selected_task_name
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |selected| selected == task_name)
|
.is_some_and(|selected| selected == task_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected_task_name(&self) -> Option<&String> {
|
pub fn get_selected_task_name(&self) -> Option<&String> {
|
||||||
@ -222,7 +222,7 @@ impl TaskSelectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn total_pages(&self) -> usize {
|
pub fn total_pages(&self) -> usize {
|
||||||
(self.entries.len() + self.items_per_page - 1) / self.items_per_page
|
self.entries.len().div_ceil(self.items_per_page)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_page(&self) -> usize {
|
pub fn get_current_page(&self) -> usize {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use color_eyre::eyre::Result;
|
|||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Modifier, Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Cell, Paragraph, Row, Table},
|
widgets::{Block, Cell, Paragraph, Row, Table},
|
||||||
Frame,
|
Frame,
|
||||||
@ -13,6 +13,10 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use super::help_text::HelpText;
|
||||||
|
use super::pagination::Pagination;
|
||||||
|
use super::task_selection_manager::{SelectionMode, TaskSelectionManager};
|
||||||
|
use crate::native::tui::theme::THEME;
|
||||||
use crate::native::{
|
use crate::native::{
|
||||||
tasks::types::{Task, TaskResult},
|
tasks::types::{Task, TaskResult},
|
||||||
tui::{
|
tui::{
|
||||||
@ -24,10 +28,6 @@ use crate::native::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::help_text::HelpText;
|
|
||||||
use super::pagination::Pagination;
|
|
||||||
use super::task_selection_manager::{SelectionMode, TaskSelectionManager};
|
|
||||||
|
|
||||||
const CACHE_STATUS_LOCAL_KEPT_EXISTING: &str = "Kept Existing";
|
const CACHE_STATUS_LOCAL_KEPT_EXISTING: &str = "Kept Existing";
|
||||||
const CACHE_STATUS_LOCAL: &str = "Local";
|
const CACHE_STATUS_LOCAL: &str = "Local";
|
||||||
const CACHE_STATUS_REMOTE: &str = "Remote";
|
const CACHE_STATUS_REMOTE: &str = "Remote";
|
||||||
@ -65,7 +65,7 @@ impl Clone for TaskItem {
|
|||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
duration: self.duration.clone(),
|
duration: self.duration.clone(),
|
||||||
cache_status: self.cache_status.clone(),
|
cache_status: self.cache_status.clone(),
|
||||||
status: self.status.clone(),
|
status: self.status,
|
||||||
continuous: self.continuous,
|
continuous: self.continuous,
|
||||||
terminal_output: self.terminal_output.clone(),
|
terminal_output: self.terminal_output.clone(),
|
||||||
start_time: self.start_time,
|
start_time: self.start_time,
|
||||||
@ -313,7 +313,10 @@ impl TasksList {
|
|||||||
if in_progress_count < self.max_parallel {
|
if in_progress_count < self.max_parallel {
|
||||||
// When we have fewer InProgress tasks than self.max_parallel, fill the remaining slots
|
// When we have fewer InProgress tasks than self.max_parallel, fill the remaining slots
|
||||||
// with empty placeholder rows to maintain the fixed height
|
// with empty placeholder rows to maintain the fixed height
|
||||||
entries.extend(std::iter::repeat(None).take(self.max_parallel - in_progress_count));
|
entries.extend(std::iter::repeat_n(
|
||||||
|
None,
|
||||||
|
self.max_parallel - in_progress_count,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always add a separator after the parallel tasks section with a bottom cap
|
// Always add a separator after the parallel tasks section with a bottom cap
|
||||||
@ -483,9 +486,9 @@ impl TasksList {
|
|||||||
/// 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 {
|
||||||
if self.is_task_list_focused() {
|
if self.is_task_list_focused() {
|
||||||
Style::default()
|
Style::default().fg(THEME.secondary_fg)
|
||||||
} else {
|
} else {
|
||||||
Style::default().dim()
|
Style::default().dim().fg(THEME.secondary_fg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,9 +557,9 @@ impl TasksList {
|
|||||||
/// Shows either filter input or task status based on current state.
|
/// Shows either filter input or task status based on current state.
|
||||||
fn get_header_cells(&self, has_narrow_area_width: bool) -> Vec<Cell> {
|
fn get_header_cells(&self, has_narrow_area_width: bool) -> Vec<Cell> {
|
||||||
let status_style = if !self.is_task_list_focused() {
|
let status_style = if !self.is_task_list_focused() {
|
||||||
Style::default().fg(Color::DarkGray).dim()
|
Style::default().fg(THEME.secondary_fg).dim()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(THEME.secondary_fg)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if all tasks are completed and the status color to use
|
// Determine if all tasks are completed and the status color to use
|
||||||
@ -579,12 +582,12 @@ impl TasksList {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|t| matches!(t.status, TaskStatus::Failure));
|
.any(|t| matches!(t.status, TaskStatus::Failure));
|
||||||
if has_failures {
|
if has_failures {
|
||||||
Color::Red
|
THEME.error
|
||||||
} else {
|
} else {
|
||||||
Color::Green
|
THEME.success
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Color::Cyan
|
THEME.info
|
||||||
};
|
};
|
||||||
|
|
||||||
// Leave first cell empty for the logo
|
// Leave first cell empty for the logo
|
||||||
@ -685,9 +688,9 @@ impl TasksList {
|
|||||||
let should_dim = !self.is_task_list_focused();
|
let should_dim = !self.is_task_list_focused();
|
||||||
|
|
||||||
let filter_style = if should_dim {
|
let filter_style = if should_dim {
|
||||||
Style::default().fg(Color::Yellow).dim()
|
Style::default().fg(THEME.warning).dim()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Yellow)
|
Style::default().fg(THEME.warning)
|
||||||
};
|
};
|
||||||
|
|
||||||
let instruction_text = if hidden_tasks > 0 {
|
let instruction_text = if hidden_tasks > 0 {
|
||||||
@ -725,7 +728,7 @@ impl TasksList {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.get_current_page_entries();
|
.get_current_page_entries();
|
||||||
let selected_style = Style::default()
|
let selected_style = Style::default()
|
||||||
.fg(Color::White)
|
.fg(THEME.primary_fg)
|
||||||
.add_modifier(Modifier::BOLD);
|
.add_modifier(Modifier::BOLD);
|
||||||
let normal_style = Style::default();
|
let normal_style = Style::default();
|
||||||
|
|
||||||
@ -747,7 +750,7 @@ impl TasksList {
|
|||||||
// Determine the color of the NX logo based on task status
|
// Determine the color of the NX logo based on task status
|
||||||
let logo_color = if self.tasks.is_empty() {
|
let logo_color = if self.tasks.is_empty() {
|
||||||
// No tasks
|
// No tasks
|
||||||
Color::Cyan
|
THEME.info
|
||||||
} else if all_tasks_completed {
|
} else if all_tasks_completed {
|
||||||
// All tasks are completed, check if any failed
|
// All tasks are completed, check if any failed
|
||||||
let has_failures = self
|
let has_failures = self
|
||||||
@ -755,13 +758,13 @@ impl TasksList {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|t| matches!(t.status, TaskStatus::Failure));
|
.any(|t| matches!(t.status, TaskStatus::Failure));
|
||||||
if has_failures {
|
if has_failures {
|
||||||
Color::Red
|
THEME.error
|
||||||
} else {
|
} else {
|
||||||
Color::Green
|
THEME.success
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Tasks are still running
|
// Tasks are still running
|
||||||
Color::Cyan
|
THEME.info
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get header cells using the existing method but add NX logo to first cell
|
// Get header cells using the existing method but add NX logo to first cell
|
||||||
@ -772,7 +775,7 @@ impl TasksList {
|
|||||||
// Use the logo color for the title text as well
|
// Use the logo color for the title text as well
|
||||||
logo_color
|
logo_color
|
||||||
} else {
|
} else {
|
||||||
Color::White
|
THEME.primary_fg
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply modifiers based on focus state
|
// Apply modifiers based on focus state
|
||||||
@ -803,7 +806,7 @@ impl TasksList {
|
|||||||
// First cell: Just the NX logo and box corner if needed
|
// First cell: Just the NX logo and box corner if needed
|
||||||
let mut first_cell_spans = vec![Span::styled(
|
let mut first_cell_spans = vec![Span::styled(
|
||||||
" NX ",
|
" NX ",
|
||||||
title_style.bold().bg(logo_color).fg(Color::Black),
|
Style::reset().bold().bg(logo_color).fg(THEME.primary_fg),
|
||||||
)];
|
)];
|
||||||
|
|
||||||
// Add box corner if needed
|
// Add box corner if needed
|
||||||
@ -872,7 +875,7 @@ impl TasksList {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
// Add vertical line for visual continuity, only on first page
|
// Add vertical line for visual continuity, only on first page
|
||||||
if is_first_page && self.max_parallel > 0 {
|
if is_first_page && self.max_parallel > 0 {
|
||||||
Span::styled("│", Style::default().fg(Color::Cyan))
|
Span::styled("│", Style::default().fg(THEME.info))
|
||||||
} else {
|
} else {
|
||||||
Span::raw(" ")
|
Span::raw(" ")
|
||||||
},
|
},
|
||||||
@ -888,7 +891,7 @@ impl TasksList {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
// Add vertical line for visual continuity, only on first page
|
// Add vertical line for visual continuity, only on first page
|
||||||
if is_first_page && self.max_parallel > 0 {
|
if is_first_page && self.max_parallel > 0 {
|
||||||
Span::styled("│", Style::default().fg(Color::Cyan))
|
Span::styled("│", Style::default().fg(THEME.info))
|
||||||
} else {
|
} else {
|
||||||
Span::raw(" ")
|
Span::raw(" ")
|
||||||
},
|
},
|
||||||
@ -918,7 +921,7 @@ impl TasksList {
|
|||||||
.selection_manager
|
.selection_manager
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_selected(&task_name);
|
.is_selected(task_name);
|
||||||
|
|
||||||
// Use the helper method to check if we should show the parallel section
|
// Use the helper method to check if we should show the parallel section
|
||||||
let show_parallel = self.should_show_parallel_section();
|
let show_parallel = self.should_show_parallel_section();
|
||||||
@ -933,19 +936,19 @@ impl TasksList {
|
|||||||
| TaskStatus::RemoteCache => Cell::from(Line::from(vec![
|
| TaskStatus::RemoteCache => Cell::from(Line::from(vec![
|
||||||
Span::raw(if is_selected { ">" } else { " " }),
|
Span::raw(if is_selected { ">" } else { " " }),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("✔", Style::default().fg(Color::Green)),
|
Span::styled("✔", Style::default().fg(THEME.success)),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
])),
|
])),
|
||||||
TaskStatus::Failure => Cell::from(Line::from(vec![
|
TaskStatus::Failure => Cell::from(Line::from(vec![
|
||||||
Span::raw(if is_selected { ">" } else { " " }),
|
Span::raw(if is_selected { ">" } else { " " }),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("✖", Style::default().fg(Color::Red)),
|
Span::styled("✖", Style::default().fg(THEME.error)),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
])),
|
])),
|
||||||
TaskStatus::Skipped => Cell::from(Line::from(vec![
|
TaskStatus::Skipped => Cell::from(Line::from(vec![
|
||||||
Span::raw(if is_selected { ">" } else { " " }),
|
Span::raw(if is_selected { ">" } else { " " }),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("⏭", Style::default().fg(Color::Yellow)),
|
Span::styled("⏭", Style::default().fg(THEME.warning)),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
])),
|
])),
|
||||||
TaskStatus::InProgress | TaskStatus::Shared => {
|
TaskStatus::InProgress | TaskStatus::Shared => {
|
||||||
@ -959,7 +962,7 @@ impl TasksList {
|
|||||||
if is_in_parallel_section
|
if is_in_parallel_section
|
||||||
&& self.selection_manager.lock().unwrap().get_current_page() == 0
|
&& self.selection_manager.lock().unwrap().get_current_page() == 0
|
||||||
{
|
{
|
||||||
spans.push(Span::styled("│", Style::default().fg(Color::Cyan)));
|
spans.push(Span::styled("│", Style::default().fg(THEME.info)));
|
||||||
} else {
|
} else {
|
||||||
spans.push(Span::raw(" "));
|
spans.push(Span::raw(" "));
|
||||||
}
|
}
|
||||||
@ -967,7 +970,7 @@ impl TasksList {
|
|||||||
// Add the spinner with consistent spacing
|
// Add the spinner with consistent spacing
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
throbber_char.to_string(),
|
throbber_char.to_string(),
|
||||||
Style::default().fg(Color::LightCyan),
|
Style::default().fg(THEME.info_light),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Add trailing space to maintain consistent width
|
// Add trailing space to maintain consistent width
|
||||||
@ -978,14 +981,14 @@ impl TasksList {
|
|||||||
TaskStatus::Stopped => Cell::from(Line::from(vec![
|
TaskStatus::Stopped => Cell::from(Line::from(vec![
|
||||||
Span::raw(if is_selected { ">" } else { " " }),
|
Span::raw(if is_selected { ">" } else { " " }),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("◼", Style::default().fg(Color::DarkGray)),
|
Span::styled("◼", Style::default().fg(THEME.secondary_fg)),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
])),
|
])),
|
||||||
TaskStatus::NotStarted => Cell::from(Line::from(vec![
|
TaskStatus::NotStarted => Cell::from(Line::from(vec![
|
||||||
Span::raw(if is_selected { ">" } else { " " }),
|
Span::raw(if is_selected { ">" } else { " " }),
|
||||||
// No need for parallel section check for pending tasks
|
// No need for parallel section check for pending tasks
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("·", Style::default().fg(Color::DarkGray)),
|
Span::styled("·", Style::default().fg(THEME.secondary_fg)),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
])),
|
])),
|
||||||
};
|
};
|
||||||
@ -1136,7 +1139,7 @@ impl TasksList {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
// Add space and vertical line for parallel section (fixed position)
|
// Add space and vertical line for parallel section (fixed position)
|
||||||
if is_first_page && self.max_parallel > 0 {
|
if is_first_page && self.max_parallel > 0 {
|
||||||
Span::styled("│", Style::default().fg(Color::Cyan))
|
Span::styled("│", Style::default().fg(THEME.info))
|
||||||
} else {
|
} else {
|
||||||
Span::raw(" ")
|
Span::raw(" ")
|
||||||
},
|
},
|
||||||
@ -1152,7 +1155,7 @@ impl TasksList {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
// Add space and vertical line for parallel section (fixed position)
|
// Add space and vertical line for parallel section (fixed position)
|
||||||
if is_first_page && self.max_parallel > 0 {
|
if is_first_page && self.max_parallel > 0 {
|
||||||
Span::styled("│", Style::default().fg(Color::Cyan))
|
Span::styled("│", Style::default().fg(THEME.info))
|
||||||
} else {
|
} else {
|
||||||
Span::raw(" ")
|
Span::raw(" ")
|
||||||
},
|
},
|
||||||
@ -1176,7 +1179,7 @@ impl TasksList {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
// Add bottom corner for the box, or just spaces if not on first page
|
// Add bottom corner for the box, or just spaces if not on first page
|
||||||
if is_first_page {
|
if is_first_page {
|
||||||
Span::styled("└", Style::default().fg(Color::Cyan))
|
Span::styled("└", Style::default().fg(THEME.info))
|
||||||
} else {
|
} else {
|
||||||
Span::raw(" ")
|
Span::raw(" ")
|
||||||
},
|
},
|
||||||
@ -1192,7 +1195,7 @@ impl TasksList {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
// Add bottom corner for the box, or just spaces if not on first page
|
// Add bottom corner for the box, or just spaces if not on first page
|
||||||
if is_first_page {
|
if is_first_page {
|
||||||
Span::styled("└", Style::default().fg(Color::Cyan))
|
Span::styled("└", Style::default().fg(THEME.info))
|
||||||
} else {
|
} else {
|
||||||
Span::raw(" ")
|
Span::raw(" ")
|
||||||
},
|
},
|
||||||
@ -1292,9 +1295,9 @@ impl TasksList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let message_style = if is_dimmed {
|
let message_style = if is_dimmed {
|
||||||
Style::default().fg(Color::DarkGray).dim()
|
Style::default().fg(THEME.secondary_fg).dim()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(THEME.secondary_fg)
|
||||||
};
|
};
|
||||||
|
|
||||||
// No URL present in the message, render the message as is if it fits, otherwise truncate
|
// No URL present in the message, render the message as is if it fits, otherwise truncate
|
||||||
@ -1330,9 +1333,9 @@ impl TasksList {
|
|||||||
let mut spans = vec![];
|
let mut spans = vec![];
|
||||||
|
|
||||||
let url_style = if is_dimmed {
|
let url_style = if is_dimmed {
|
||||||
Style::default().fg(Color::LightCyan).underlined().dim()
|
Style::default().fg(THEME.info).underlined().dim()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::LightCyan).underlined()
|
Style::default().fg(THEME.info).underlined()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine what fits, prioritizing the URL
|
// Determine what fits, prioritizing the URL
|
||||||
@ -1558,7 +1561,7 @@ impl Component for TasksList {
|
|||||||
.split(paghelp_or_help_vertical_area);
|
.split(paghelp_or_help_vertical_area);
|
||||||
|
|
||||||
// Render components with safety checks
|
// Render components with safety checks
|
||||||
if row_chunks.len() > 0
|
if !row_chunks.is_empty()
|
||||||
&& row_chunks[0].height > 0
|
&& row_chunks[0].height > 0
|
||||||
&& row_chunks[0].width > 0
|
&& row_chunks[0].width > 0
|
||||||
&& row_chunks[0].y < f.area().height
|
&& row_chunks[0].y < f.area().height
|
||||||
@ -1658,7 +1661,7 @@ impl Component for TasksList {
|
|||||||
.split(paghelp_or_help_vertical_area);
|
.split(paghelp_or_help_vertical_area);
|
||||||
|
|
||||||
// Render components with safety checks
|
// Render components with safety checks
|
||||||
if row_chunks.len() > 0
|
if !row_chunks.is_empty()
|
||||||
&& row_chunks[0].height > 0
|
&& row_chunks[0].height > 0
|
||||||
&& row_chunks[0].width > 0
|
&& row_chunks[0].width > 0
|
||||||
&& row_chunks[0].y < f.area().height
|
&& row_chunks[0].y < f.area().height
|
||||||
@ -1723,7 +1726,7 @@ impl Component for TasksList {
|
|||||||
.split(paghelp_or_help_vertical_area);
|
.split(paghelp_or_help_vertical_area);
|
||||||
|
|
||||||
// Render components with safety checks
|
// Render components with safety checks
|
||||||
if row_chunks.len() > 0
|
if !row_chunks.is_empty()
|
||||||
&& row_chunks[0].height > 0
|
&& row_chunks[0].height > 0
|
||||||
&& row_chunks[0].width > 0
|
&& row_chunks[0].width > 0
|
||||||
&& row_chunks[0].y < f.area().height
|
&& row_chunks[0].y < f.area().height
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, BorderType, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
|
Block, BorderType, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
|
||||||
@ -13,9 +13,9 @@ use ratatui::{
|
|||||||
use std::{io, sync::Arc};
|
use std::{io, sync::Arc};
|
||||||
use tui_term::widget::PseudoTerminal;
|
use tui_term::widget::PseudoTerminal;
|
||||||
|
|
||||||
use crate::native::tui::pty::PtyInstance;
|
|
||||||
|
|
||||||
use super::tasks_list::TaskStatus;
|
use super::tasks_list::TaskStatus;
|
||||||
|
use crate::native::tui::pty::PtyInstance;
|
||||||
|
use crate::native::tui::theme::THEME;
|
||||||
|
|
||||||
pub struct TerminalPaneData {
|
pub struct TerminalPaneData {
|
||||||
pub pty: Option<Arc<PtyInstance>>,
|
pub pty: Option<Arc<PtyInstance>>,
|
||||||
@ -199,35 +199,35 @@ impl<'a> TerminalPane<'a> {
|
|||||||
| TaskStatus::RemoteCache => Span::styled(
|
| TaskStatus::RemoteCache => Span::styled(
|
||||||
" ✔ ",
|
" ✔ ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Green)
|
.fg(THEME.success)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
TaskStatus::Failure => Span::styled(
|
TaskStatus::Failure => Span::styled(
|
||||||
" ✖ ",
|
" ✖ ",
|
||||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
|
.fg(THEME.error)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
TaskStatus::Skipped => Span::styled(
|
TaskStatus::Skipped => Span::styled(
|
||||||
" ⏭ ",
|
" ⏭ ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Yellow)
|
.fg(THEME.warning)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
TaskStatus::InProgress | TaskStatus::Shared => Span::styled(
|
TaskStatus::InProgress | TaskStatus::Shared => Span::styled(
|
||||||
" ● ",
|
" ● ",
|
||||||
Style::default()
|
Style::default().fg(THEME.info).add_modifier(Modifier::BOLD),
|
||||||
.fg(Color::LightCyan)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
),
|
),
|
||||||
TaskStatus::Stopped => Span::styled(
|
TaskStatus::Stopped => Span::styled(
|
||||||
" ◼ ",
|
" ◼ ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::DarkGray)
|
.fg(THEME.secondary_fg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
TaskStatus::NotStarted => Span::styled(
|
TaskStatus::NotStarted => Span::styled(
|
||||||
" · ",
|
" · ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::DarkGray)
|
.fg(THEME.secondary_fg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -238,11 +238,11 @@ impl<'a> TerminalPane<'a> {
|
|||||||
TaskStatus::Success
|
TaskStatus::Success
|
||||||
| TaskStatus::LocalCacheKeptExisting
|
| TaskStatus::LocalCacheKeptExisting
|
||||||
| TaskStatus::LocalCache
|
| TaskStatus::LocalCache
|
||||||
| TaskStatus::RemoteCache => Color::Green,
|
| TaskStatus::RemoteCache => THEME.success,
|
||||||
TaskStatus::Failure => Color::Red,
|
TaskStatus::Failure => THEME.error,
|
||||||
TaskStatus::Skipped => Color::Yellow,
|
TaskStatus::Skipped => THEME.warning,
|
||||||
TaskStatus::InProgress | TaskStatus::Shared => Color::LightCyan,
|
TaskStatus::InProgress | TaskStatus::Shared => THEME.info,
|
||||||
TaskStatus::NotStarted | TaskStatus::Stopped => Color::DarkGray,
|
TaskStatus::NotStarted | TaskStatus::Stopped => THEME.secondary_fg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,6 +275,8 @@ impl<'a> TerminalPane<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This lifetime is needed for our terminal pane data, it breaks without it
|
||||||
|
#[allow(clippy::needless_lifetimes)]
|
||||||
impl<'a> StatefulWidget for TerminalPane<'a> {
|
impl<'a> StatefulWidget for TerminalPane<'a> {
|
||||||
type State = TerminalPaneState;
|
type State = TerminalPaneState;
|
||||||
|
|
||||||
@ -294,7 +296,7 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
// Only attempt to render if we have a valid area
|
// Only attempt to render if we have a valid area
|
||||||
let text = "...";
|
let text = "...";
|
||||||
let paragraph = Paragraph::new(text)
|
let paragraph = Paragraph::new(text)
|
||||||
.style(Style::default().fg(Color::Gray))
|
.style(Style::default().fg(THEME.secondary_fg))
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
Widget::render(paragraph, safe_area, buf);
|
Widget::render(paragraph, safe_area, buf);
|
||||||
}
|
}
|
||||||
@ -317,20 +319,28 @@ 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 block = Block::default()
|
||||||
.title(Line::from(if state.is_focused {
|
.title(Line::from(if state.is_focused {
|
||||||
vec![
|
vec![
|
||||||
status_icon.clone(),
|
status_icon.clone(),
|
||||||
Span::raw(format!("{} ", state.task_name))
|
Span::raw(format!("{} ", state.task_name))
|
||||||
.style(Style::default().fg(Color::White)),
|
.style(Style::default().fg(THEME.primary_fg)),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
vec![
|
vec![
|
||||||
status_icon.clone(),
|
status_icon.clone(),
|
||||||
Span::raw(format!("{} ", state.task_name))
|
Span::raw(format!("{} ", state.task_name))
|
||||||
.style(Style::default().fg(Color::White)),
|
.style(Style::default().fg(THEME.secondary_fg)),
|
||||||
if state.is_next_tab_target {
|
if state.is_next_tab_target {
|
||||||
Span::raw("Press <tab> to focus ")
|
let tab_target_text = 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
|
||||||
|
if !THEME.is_dark_mode {
|
||||||
|
tab_target_text.fg(THEME.primary_fg)
|
||||||
|
} else {
|
||||||
|
tab_target_text
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Span::raw("")
|
Span::raw("")
|
||||||
},
|
},
|
||||||
@ -338,15 +348,26 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
}))
|
}))
|
||||||
.title_alignment(Alignment::Left)
|
.title_alignment(Alignment::Left)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Plain)
|
.border_type(if state.is_focused {
|
||||||
|
BorderType::Thick
|
||||||
|
} else {
|
||||||
|
BorderType::Plain
|
||||||
|
})
|
||||||
.border_style(border_style)
|
.border_style(border_style)
|
||||||
.padding(Padding::new(2, 2, 1, 1));
|
.padding(Padding::new(2, 2, 1, 1));
|
||||||
|
|
||||||
// If task hasn't started yet, show pending message
|
// If task hasn't started yet, show pending message
|
||||||
if matches!(state.task_status, TaskStatus::NotStarted) {
|
if matches!(state.task_status, TaskStatus::NotStarted) {
|
||||||
|
let message_style = if state.is_focused {
|
||||||
|
Style::default().fg(THEME.secondary_fg)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
.fg(THEME.secondary_fg)
|
||||||
|
.add_modifier(Modifier::DIM)
|
||||||
|
};
|
||||||
let message = vec![Line::from(vec![Span::styled(
|
let message = vec![Line::from(vec![Span::styled(
|
||||||
"Task is pending...",
|
"Task is pending...",
|
||||||
Style::default().fg(Color::DarkGray),
|
message_style,
|
||||||
)])];
|
)])];
|
||||||
|
|
||||||
let paragraph = Paragraph::new(message)
|
let paragraph = Paragraph::new(message)
|
||||||
@ -470,19 +491,19 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
let bottom_text = if self.is_currently_interactive() {
|
let bottom_text = if self.is_currently_interactive() {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("<ctrl>+z", Style::default().fg(Color::Cyan)),
|
Span::styled("<ctrl>+z", Style::default().fg(THEME.info)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
" to exit interactive ",
|
" to exit interactive ",
|
||||||
Style::default().fg(Color::White),
|
Style::default().fg(THEME.primary_fg),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("i", Style::default().fg(Color::Cyan)),
|
Span::styled("i", Style::default().fg(THEME.info)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
" to make interactive ",
|
" to make interactive ",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(THEME.secondary_fg),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
@ -512,12 +533,12 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
|
|||||||
let top_text = if self.is_currently_interactive() {
|
let top_text = if self.is_currently_interactive() {
|
||||||
Line::from(vec![Span::styled(
|
Line::from(vec![Span::styled(
|
||||||
" INTERACTIVE ",
|
" INTERACTIVE ",
|
||||||
Style::default().fg(Color::White),
|
Style::default().fg(THEME.primary_fg),
|
||||||
)])
|
)])
|
||||||
} else {
|
} else {
|
||||||
Line::from(vec![Span::styled(
|
Line::from(vec![Span::styled(
|
||||||
" NON-INTERACTIVE ",
|
" NON-INTERACTIVE ",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(THEME.secondary_fg),
|
||||||
)])
|
)])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,13 @@ use napi::JsObject;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::native::logger::enable_logger;
|
|
||||||
use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc};
|
|
||||||
use crate::native::tasks::types::{Task, TaskResult};
|
|
||||||
|
|
||||||
use super::app::App;
|
use super::app::App;
|
||||||
use super::components::tasks_list::TaskStatus;
|
use super::components::tasks_list::TaskStatus;
|
||||||
use super::config::{AutoExit, TuiCliArgs as RustTuiCliArgs, TuiConfig as RustTuiConfig};
|
use super::config::{AutoExit, TuiCliArgs as RustTuiCliArgs, TuiConfig as RustTuiConfig};
|
||||||
use super::tui::Tui;
|
use super::tui::Tui;
|
||||||
|
use crate::native::logger::enable_logger;
|
||||||
|
use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc};
|
||||||
|
use crate::native::tasks::types::{Task, TaskResult};
|
||||||
|
|
||||||
#[napi(object)]
|
#[napi(object)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -49,7 +48,7 @@ impl From<(TuiConfig, &RustTuiCliArgs)> for RustTuiConfig {
|
|||||||
Either::B(int_value) => AutoExit::Integer(int_value),
|
Either::B(int_value) => AutoExit::Integer(int_value),
|
||||||
});
|
});
|
||||||
// Pass the converted JSON config value(s) and cli_args to instantiate the config with
|
// Pass the converted JSON config value(s) and cli_args to instantiate the config with
|
||||||
RustTuiConfig::new(js_auto_exit, &rust_tui_cli_args)
|
RustTuiConfig::new(js_auto_exit, rust_tui_cli_args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +87,7 @@ impl AppLifeCycle {
|
|||||||
Self {
|
Self {
|
||||||
app: Arc::new(std::sync::Mutex::new(
|
app: Arc::new(std::sync::Mutex::new(
|
||||||
App::new(
|
App::new(
|
||||||
tasks.into_iter().map(|t| t.into()).collect(),
|
tasks.into_iter().collect(),
|
||||||
initiating_tasks,
|
initiating_tasks,
|
||||||
run_mode,
|
run_mode,
|
||||||
pinned_tasks,
|
pinned_tasks,
|
||||||
@ -162,8 +161,8 @@ impl AppLifeCycle {
|
|||||||
&self,
|
&self,
|
||||||
done_callback: ThreadsafeFunction<(), ErrorStrategy::Fatal>,
|
done_callback: ThreadsafeFunction<(), ErrorStrategy::Fatal>,
|
||||||
) -> napi::Result<()> {
|
) -> napi::Result<()> {
|
||||||
debug!("Initializing Terminal UI");
|
|
||||||
enable_logger();
|
enable_logger();
|
||||||
|
debug!("Initializing Terminal UI");
|
||||||
|
|
||||||
let app_mutex = self.app.clone();
|
let app_mutex = self.app.clone();
|
||||||
|
|
||||||
@ -287,7 +286,7 @@ impl AppLifeCycle {
|
|||||||
#[napi(js_name = "__setCloudMessage")]
|
#[napi(js_name = "__setCloudMessage")]
|
||||||
pub async fn __set_cloud_message(&self, message: String) -> napi::Result<()> {
|
pub async fn __set_cloud_message(&self, message: String) -> napi::Result<()> {
|
||||||
if let Ok(mut app) = self.app.lock() {
|
if let Ok(mut app) = self.app.lock() {
|
||||||
let _ = app.set_cloud_message(Some(message));
|
app.set_cloud_message(Some(message));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,5 +4,7 @@ pub mod components;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
|
pub mod theme;
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
72
packages/nx/src/native/tui/theme.rs
Normal file
72
packages/nx/src/native/tui/theme.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Sadly clippy doesn't seem to support only allowing the ratatui type, we have to disable the whole lint
|
||||||
|
#![allow(clippy::disallowed_types)]
|
||||||
|
|
||||||
|
// This is the only file we should use the `ratatui::style::Color` type in
|
||||||
|
use ratatui::style::Color;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use terminal_colorsaurus::{color_scheme, ColorScheme, QueryOptions};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
pub static THEME: LazyLock<Theme> = LazyLock::new(Theme::init);
|
||||||
|
|
||||||
|
/// Holds theme-dependent colors calculated based on dark/light mode.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub is_dark_mode: bool,
|
||||||
|
pub primary_fg: Color,
|
||||||
|
pub secondary_fg: Color,
|
||||||
|
pub error: Color,
|
||||||
|
pub success: Color,
|
||||||
|
pub warning: Color,
|
||||||
|
pub info: Color,
|
||||||
|
pub info_light: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
fn init() -> Self {
|
||||||
|
if Self::is_dark_mode() {
|
||||||
|
debug!("Initializing dark theme");
|
||||||
|
Self::dark()
|
||||||
|
} else {
|
||||||
|
debug!("Initializing light theme");
|
||||||
|
Self::light()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dark() -> Self {
|
||||||
|
Self {
|
||||||
|
is_dark_mode: true,
|
||||||
|
primary_fg: Color::White,
|
||||||
|
secondary_fg: Color::DarkGray,
|
||||||
|
error: Color::Red,
|
||||||
|
success: Color::Green,
|
||||||
|
warning: Color::Yellow,
|
||||||
|
info: Color::Cyan,
|
||||||
|
info_light: Color::LightCyan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn light() -> Self {
|
||||||
|
Self {
|
||||||
|
is_dark_mode: false,
|
||||||
|
primary_fg: Color::Black,
|
||||||
|
secondary_fg: Color::Gray,
|
||||||
|
error: Color::Red,
|
||||||
|
success: Color::Green,
|
||||||
|
warning: Color::Yellow,
|
||||||
|
info: Color::Cyan,
|
||||||
|
info_light: Color::LightCyan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detects if the current terminal likely uses a dark theme background.
|
||||||
|
/// NOTE: This requires raw mode access and might not work correctly once the TUI is fully running.
|
||||||
|
/// It should ideally be called once during initialization.
|
||||||
|
fn is_dark_mode() -> bool {
|
||||||
|
match color_scheme(QueryOptions::default()) {
|
||||||
|
Ok(ColorScheme::Dark) => true,
|
||||||
|
Ok(ColorScheme::Light) => false,
|
||||||
|
Err(_) => true, // Default to dark mode if detection fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::native::tui::theme::THEME;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor,
|
cursor,
|
||||||
@ -165,6 +166,8 @@ impl Tui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn enter(&mut self) -> Result<()> {
|
pub fn enter(&mut self) -> Result<()> {
|
||||||
|
// Ensure the theme is set before entering raw mode because it won't work properly once we're in raw 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)?;
|
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user